Skip to content

Commit 5c226c0

Browse files
committed
Initial commit
1 parent d0c78cf commit 5c226c0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2668
-2
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
# flask-labthings
2-
Python implementation of LabThings, based on the Flask microframework
1+
# python-labthings
2+
Python implementation of the LabThings API structure, based on the Flask microframework.

labthings/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .core import tasks, lock

labthings/consumer/__init__.py

Whitespace-only changes.

labthings/core/__init__.py

Whitespace-only changes.

labthings/core/exceptions.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from threading import ThreadError
2+
3+
4+
class LockError(ThreadError):
5+
ERROR_CODES = {
6+
"ACQUIRE_ERROR": "Unable to acquire. Lock in use by another thread.",
7+
"IN_USE_ERROR": "Lock in use by another thread.",
8+
}
9+
10+
def __init__(self, code, lock):
11+
self.code = code
12+
if code in LockError.ERROR_CODES:
13+
self.message = LockError.ERROR_CODES[code]
14+
else:
15+
self.message = "Unknown error."
16+
17+
self.string = "{}: {}".format(self.code, self.message)
18+
print(self.string)
19+
20+
ThreadError.__init__(self)
21+
22+
def __str__(self):
23+
return self.string

labthings/core/lock.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from threading import RLock
2+
3+
from .exceptions import LockError
4+
5+
6+
class StrictLock(object):
7+
"""
8+
Class that behaves like a Python RLock, but with stricter timeout conditions and custom exceptions.
9+
10+
Args:
11+
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
12+
13+
Attributes:
14+
_lock (:py:class:`threading.RLock`): Parent RLock object
15+
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
16+
"""
17+
18+
def __init__(self, timeout=1):
19+
self._lock = RLock()
20+
self.timeout = timeout
21+
22+
def locked(self):
23+
return self._lock.locked()
24+
25+
def acquire(self, blocking=True):
26+
return self._lock.acquire(blocking, timeout=self.timeout)
27+
28+
def __enter__(self):
29+
result = self._lock.acquire(blocking=True, timeout=self.timeout)
30+
if result:
31+
return result
32+
else:
33+
raise LockError("ACQUIRE_ERROR", self)
34+
35+
def __exit__(self, *args):
36+
self._lock.release()
37+
38+
def release(self):
39+
self._lock.release()
40+
41+
42+
class CompositeLock(object):
43+
"""
44+
Class that behaves like a :py:class:`labthings.core.lock.StrictLock`,
45+
but allows multiple locks to be acquired and released.
46+
47+
Args:
48+
locks (list): List of parent RLock objects
49+
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
50+
51+
Attributes:
52+
locks (list): List of parent RLock objects
53+
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
54+
"""
55+
56+
def __init__(self, locks, timeout=1):
57+
self.locks = locks
58+
self.timeout = timeout
59+
60+
def acquire(self, blocking=True):
61+
return (lock.acquire(blocking=blocking) for lock in self.locks)
62+
63+
def __enter__(self):
64+
result = (lock.acquire(blocking=True) for lock in self.locks)
65+
if all(result):
66+
return result
67+
else:
68+
raise LockError("ACQUIRE_ERROR", self)
69+
70+
def __exit__(self, *args):
71+
for lock in self.locks:
72+
lock.release()
73+
74+
def release(self):
75+
for lock in self.locks:
76+
lock.release()

labthings/core/tasks/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
__all__ = [
2+
"taskify",
3+
"tasks",
4+
"dict",
5+
"states",
6+
"current_task",
7+
"update_task_progress",
8+
"cleanup_tasks",
9+
"remove_task",
10+
"update_task_data",
11+
"ThreadTerminationError",
12+
]
13+
14+
from .pool import (
15+
tasks,
16+
dict,
17+
states,
18+
current_task,
19+
update_task_progress,
20+
cleanup_tasks,
21+
remove_task,
22+
update_task_data,
23+
taskify,
24+
)
25+
from .thread import ThreadTerminationError

labthings/core/tasks/pool.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import threading
2+
import logging
3+
from functools import wraps
4+
5+
from .thread import TaskThread
6+
7+
from flask import copy_current_request_context
8+
9+
10+
class TaskMaster:
11+
def __init__(self, *args, **kwargs):
12+
self._tasks = []
13+
14+
@property
15+
def tasks(self):
16+
"""
17+
Returns:
18+
list: List of TaskThread objects.
19+
"""
20+
return self._tasks
21+
22+
@property
23+
def dict(self):
24+
"""
25+
Returns:
26+
dict: Dictionary of TaskThread objects. Key is TaskThread ID.
27+
"""
28+
return {str(t.id): t for t in self._tasks}
29+
30+
@property
31+
def states(self):
32+
"""
33+
Returns:
34+
dict: Dictionary of TaskThread.state dictionaries. Key is TaskThread ID.
35+
"""
36+
return {str(t.id): t.state for t in self._tasks}
37+
38+
def new(self, f, *args, **kwargs):
39+
# copy_current_request_context allows threads to access flask current_app
40+
task = TaskThread(
41+
target=copy_current_request_context(f), args=args, kwargs=kwargs
42+
)
43+
self._tasks.append(task)
44+
return task
45+
46+
def remove(self, task_id):
47+
for task in self._tasks:
48+
if (task.id == task_id) and not task.isAlive():
49+
del task
50+
51+
def cleanup(self):
52+
for task in self._tasks:
53+
if not task.isAlive():
54+
del task
55+
56+
57+
# Task management
58+
59+
60+
def tasks():
61+
"""
62+
List of tasks in default taskmaster
63+
Returns:
64+
list: List of tasks in default taskmaster
65+
"""
66+
global _default_task_master
67+
return _default_task_master.tasks
68+
69+
70+
def dict():
71+
"""
72+
Dictionary of tasks in default taskmaster
73+
Returns:
74+
dict: Dictionary of tasks in default taskmaster
75+
"""
76+
global _default_task_master
77+
return _default_task_master.dict
78+
79+
80+
def states():
81+
"""
82+
Dictionary of TaskThread.state dictionaries. Key is TaskThread ID.
83+
Returns:
84+
dict: Dictionary of task states in default taskmaster
85+
"""
86+
global _default_task_master
87+
return _default_task_master.states
88+
89+
90+
def cleanup_tasks():
91+
global _default_task_master
92+
return _default_task_master.cleanup()
93+
94+
95+
def remove_task(task_id: str):
96+
global _default_task_master
97+
return _default_task_master.remove(task_id)
98+
99+
100+
# Operations on the current task
101+
102+
103+
def current_task():
104+
current_task_thread = threading.current_thread()
105+
if not isinstance(current_task_thread, TaskThread):
106+
return None
107+
return current_task_thread
108+
109+
110+
def update_task_progress(progress: int):
111+
if current_task():
112+
current_task().update_progress(progress)
113+
else:
114+
logging.info("Cannot update task progress of __main__ thread. Skipping.")
115+
116+
117+
def update_task_data(data: dict):
118+
if current_task():
119+
current_task().update_data(data)
120+
else:
121+
logging.info("Cannot update task data of __main__ thread. Skipping.")
122+
123+
124+
# Main "taskify" functions
125+
126+
127+
def taskify(f):
128+
"""
129+
A decorator that wraps the passed in function
130+
and surpresses exceptions should one occur
131+
"""
132+
133+
@wraps(f)
134+
def wrapped(*args, **kwargs):
135+
task = _default_task_master.new(
136+
f, *args, **kwargs
137+
) # Append to parent object's task list
138+
task.start() # Start the function
139+
return task
140+
141+
return wrapped
142+
143+
144+
# Create our default, protected, module-level task pool
145+
_default_task_master = TaskMaster()

0 commit comments

Comments
 (0)