bapsf-motion 0.2.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bapsf_motion/__init__.py +59 -0
- bapsf_motion/actors/__init__.py +18 -0
- bapsf_motion/actors/axis_.py +328 -0
- bapsf_motion/actors/base.py +281 -0
- bapsf_motion/actors/demos/__init__.py +0 -0
- bapsf_motion/actors/demos/drive_timed_run_1D.py +80 -0
- bapsf_motion/actors/demos/drive_timed_run_2D.py +98 -0
- bapsf_motion/actors/demos/mgroup_timed_run_2D.py +103 -0
- bapsf_motion/actors/drive_.py +360 -0
- bapsf_motion/actors/manager_.py +420 -0
- bapsf_motion/actors/motion_group_.py +1059 -0
- bapsf_motion/actors/motor_.py +1813 -0
- bapsf_motion/actors/tests/__init__.py +0 -0
- bapsf_motion/examples/benchtop_motion_group.toml +27 -0
- bapsf_motion/examples/benchtop_run.toml +30 -0
- bapsf_motion/examples/mg_1d.toml +24 -0
- bapsf_motion/examples/mg_2d_strb_test_setup.toml +37 -0
- bapsf_motion/examples/motion_group.toml +18 -0
- bapsf_motion/gui/__init__.py +4 -0
- bapsf_motion/gui/configure.py +3628 -0
- bapsf_motion/gui/motor.py +313 -0
- bapsf_motion/gui/widgets/__init__.py +24 -0
- bapsf_motion/gui/widgets/buttons.py +233 -0
- bapsf_motion/gui/widgets/logging.py +305 -0
- bapsf_motion/gui/widgets/misc.py +101 -0
- bapsf_motion/motion_builder/__init__.py +12 -0
- bapsf_motion/motion_builder/core.py +434 -0
- bapsf_motion/motion_builder/exclusions/__init__.py +37 -0
- bapsf_motion/motion_builder/exclusions/base.py +201 -0
- bapsf_motion/motion_builder/exclusions/circular.py +169 -0
- bapsf_motion/motion_builder/exclusions/divider.py +197 -0
- bapsf_motion/motion_builder/exclusions/helpers.py +176 -0
- bapsf_motion/motion_builder/exclusions/lapd.py +330 -0
- bapsf_motion/motion_builder/item.py +170 -0
- bapsf_motion/motion_builder/layers/__init__.py +27 -0
- bapsf_motion/motion_builder/layers/base.py +169 -0
- bapsf_motion/motion_builder/layers/helpers.py +178 -0
- bapsf_motion/motion_builder/layers/regular_grid.py +210 -0
- bapsf_motion/transform/__init__.py +12 -0
- bapsf_motion/transform/base.py +451 -0
- bapsf_motion/transform/helpers.py +153 -0
- bapsf_motion/transform/identity.py +80 -0
- bapsf_motion/transform/lapd.py +483 -0
- bapsf_motion/utils/__init__.py +112 -0
- bapsf_motion/utils/exceptions.py +13 -0
- bapsf_motion/utils/toml.py +55 -0
- bapsf_motion-0.2.0b1.dist-info/METADATA +140 -0
- bapsf_motion-0.2.0b1.dist-info/RECORD +51 -0
- bapsf_motion-0.2.0b1.dist-info/WHEEL +5 -0
- bapsf_motion-0.2.0b1.dist-info/dependency_links.txt +1 -0
- bapsf_motion-0.2.0b1.dist-info/top_level.txt +1 -0
bapsf_motion/__init__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""`bapsf_motion`"""
|
|
2
|
+
__all__ = ["__version__"]
|
|
3
|
+
|
|
4
|
+
# Enforce Python version check during package import.
|
|
5
|
+
# This is the same check as the one at the top of setup.py
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
if sys.version_info < (3, 7): # coverage: ignore
|
|
9
|
+
raise ImportError("bapsf_motion does not support Python < 3.7")
|
|
10
|
+
|
|
11
|
+
if sys.version_info >= (3, 8):
|
|
12
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
13
|
+
else:
|
|
14
|
+
from importlib_metadata import version, PackageNotFoundError
|
|
15
|
+
|
|
16
|
+
from bapsf_motion import actors, motion_builder, transform, utils
|
|
17
|
+
|
|
18
|
+
# define version
|
|
19
|
+
try:
|
|
20
|
+
# this places a runtime dependency on setuptools
|
|
21
|
+
#
|
|
22
|
+
# note: if there's any distribution metadata in your source files, then this
|
|
23
|
+
# will find a version based on those files. Keep distribution metadata
|
|
24
|
+
# out of your repository unless you've intentionally installed the package
|
|
25
|
+
# as editable (e.g. `pip install -e {bapsf_motion_directory_root}`),
|
|
26
|
+
# but then __version__ will not be updated with each commit, it is
|
|
27
|
+
# frozen to the version at time of install.
|
|
28
|
+
#
|
|
29
|
+
#: bapsf_motion version string
|
|
30
|
+
__version__ = version("bapsf_motion")
|
|
31
|
+
except PackageNotFoundError:
|
|
32
|
+
# package is not installed
|
|
33
|
+
fallback_version = "unknown"
|
|
34
|
+
try:
|
|
35
|
+
# code most likely being used from source
|
|
36
|
+
# if setuptools_scm is installed then generate a version
|
|
37
|
+
from setuptools_scm import get_version
|
|
38
|
+
|
|
39
|
+
__version__ = get_version(
|
|
40
|
+
root="../", relative_to=__file__, fallback_version=fallback_version
|
|
41
|
+
)
|
|
42
|
+
del get_version
|
|
43
|
+
warn_add = "setuptools_scm failed to detect the version"
|
|
44
|
+
except ModuleNotFoundError:
|
|
45
|
+
# setuptools_scm is not installed
|
|
46
|
+
__version__ = fallback_version
|
|
47
|
+
warn_add = "setuptools_scm is not installed"
|
|
48
|
+
|
|
49
|
+
if __version__ == fallback_version:
|
|
50
|
+
from warnings import warn
|
|
51
|
+
|
|
52
|
+
warn(
|
|
53
|
+
f"bapsf_motion.__version__ not generated (set to 'unknown'), "
|
|
54
|
+
f"bapsf_motion is not an installed package and {warn_add}.",
|
|
55
|
+
RuntimeWarning,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
del warn
|
|
59
|
+
del fallback_version, warn_add
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
__all__ = []
|
|
2
|
+
__actors__ = [
|
|
3
|
+
"Axis",
|
|
4
|
+
"BaseActor",
|
|
5
|
+
"Drive",
|
|
6
|
+
"EventActor",
|
|
7
|
+
"RunManager",
|
|
8
|
+
"MotionGroup",
|
|
9
|
+
"Motor",
|
|
10
|
+
]
|
|
11
|
+
__all__ += __actors__
|
|
12
|
+
|
|
13
|
+
from bapsf_motion.actors.axis_ import Axis
|
|
14
|
+
from bapsf_motion.actors.base import BaseActor, EventActor
|
|
15
|
+
from bapsf_motion.actors.drive_ import Drive
|
|
16
|
+
from bapsf_motion.actors.manager_ import RunManager, RunManagerConfig
|
|
17
|
+
from bapsf_motion.actors.motion_group_ import MotionGroup, MotionGroupConfig
|
|
18
|
+
from bapsf_motion.actors.motor_ import Motor
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for functionality focused around the
|
|
3
|
+
`~bapsf_motion.actors.axis_.Axis` actor class.
|
|
4
|
+
"""
|
|
5
|
+
__all__ = ["Axis"]
|
|
6
|
+
__actors__ = ["Axis"]
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from typing import Any, Dict, Union
|
|
12
|
+
|
|
13
|
+
from bapsf_motion.actors.base import EventActor
|
|
14
|
+
from bapsf_motion.actors.motor_ import Motor
|
|
15
|
+
from bapsf_motion.utils import units as u
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Axis(EventActor):
|
|
19
|
+
"""
|
|
20
|
+
The `Axis` actor is the next level actor above the |Motor| actor.
|
|
21
|
+
This actor is ignorant of how it is situated in a probe drive, but
|
|
22
|
+
is fully aware of the entire physical axis that defines it and the
|
|
23
|
+
motor that moves the axis. This actor operates in physical units
|
|
24
|
+
and will handle all the necessary unit converstion to communicate
|
|
25
|
+
with the |Motor| actor.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
ip: str
|
|
30
|
+
IPv4 address for the motor driving the axis
|
|
31
|
+
|
|
32
|
+
units: str
|
|
33
|
+
Physical units the axis operates in (e.g. ``'cm'``)
|
|
34
|
+
|
|
35
|
+
units_per_rev: float
|
|
36
|
+
The number of ``units`` traversed per motor revolution.
|
|
37
|
+
|
|
38
|
+
name: str
|
|
39
|
+
Name the axis. (DEFAULT: ``'Axis'``)
|
|
40
|
+
|
|
41
|
+
logger: `~logging.Logger`, optional
|
|
42
|
+
An instance of `~logging.Logger` that the Actor will record
|
|
43
|
+
events and status updates to. If `None`, then a logger will
|
|
44
|
+
automatically be generated. (DEFUALT: `None`)
|
|
45
|
+
|
|
46
|
+
loop: `asyncio.AbstractEventLoop`, optional
|
|
47
|
+
Instance of an `asyncio` `event loop`_. Communication with the
|
|
48
|
+
motor will happen primaritly through the evenet loop. If
|
|
49
|
+
`None`, then an `event loop`_ will be auto-generated.
|
|
50
|
+
(DEFAULT: `None`)
|
|
51
|
+
|
|
52
|
+
auto_run: bool, optional
|
|
53
|
+
If `True`, then the `event loop`_ will be placed in a separate
|
|
54
|
+
thread and started. This is all done via the :meth:`run`
|
|
55
|
+
method. (DEFAULT: `False`)
|
|
56
|
+
|
|
57
|
+
Examples
|
|
58
|
+
--------
|
|
59
|
+
|
|
60
|
+
>>> from bapsf_motion.actors import Axis
|
|
61
|
+
>>> import logging
|
|
62
|
+
>>> import sys
|
|
63
|
+
>>> logging.basicConfig(stream=sys.stdout, level=logging.NOTSET)
|
|
64
|
+
>>> ax = Axis(
|
|
65
|
+
... ip="192.168.6.104",
|
|
66
|
+
... units="cm",
|
|
67
|
+
... units_per_rev=0.1*2.54, # acme rod with .1 in pitch
|
|
68
|
+
... name="WALL-E",
|
|
69
|
+
... auto_run=True,
|
|
70
|
+
... )
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
# TODO: better handle naming of the Axis and child Motor
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
*,
|
|
78
|
+
ip: str,
|
|
79
|
+
units: str,
|
|
80
|
+
units_per_rev: float,
|
|
81
|
+
name: str = "Axis",
|
|
82
|
+
logger: logging.Logger = None,
|
|
83
|
+
loop: asyncio.AbstractEventLoop = None,
|
|
84
|
+
auto_run: bool = False,
|
|
85
|
+
):
|
|
86
|
+
# TODO: update units so inches can be used
|
|
87
|
+
self._motor = None
|
|
88
|
+
self._units = u.Unit(units)
|
|
89
|
+
self._units_per_rev = units_per_rev * self._units / u.rev
|
|
90
|
+
|
|
91
|
+
super().__init__(
|
|
92
|
+
name=name,
|
|
93
|
+
logger=logger,
|
|
94
|
+
loop=loop,
|
|
95
|
+
auto_run=False,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self._motor = Motor(
|
|
99
|
+
ip=ip,
|
|
100
|
+
name="motor",
|
|
101
|
+
logger=self.logger,
|
|
102
|
+
loop=self.loop,
|
|
103
|
+
auto_run=False,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
self.run(auto_run=auto_run)
|
|
107
|
+
|
|
108
|
+
def _configure_before_run(self):
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
def _initialize_tasks(self):
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
def run(self, auto_run=True):
|
|
115
|
+
super().run(auto_run=auto_run)
|
|
116
|
+
|
|
117
|
+
if self.motor is None:
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
self.motor.run(auto_run=auto_run)
|
|
121
|
+
|
|
122
|
+
def terminate(self, delay_loop_stop=False):
|
|
123
|
+
self.motor.terminate(delay_loop_stop=True)
|
|
124
|
+
super().terminate(delay_loop_stop=delay_loop_stop)
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def config(self) -> Dict[str, Any]:
|
|
128
|
+
"""Dictionary of the axis configuration parameters."""
|
|
129
|
+
return {
|
|
130
|
+
"name": self.name,
|
|
131
|
+
"ip": self.motor.ip,
|
|
132
|
+
"units": str(self.units),
|
|
133
|
+
"units_per_rev": self.units_per_rev.value.item()
|
|
134
|
+
}
|
|
135
|
+
config.__doc__ = EventActor.config.__doc__
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def motor(self) -> Motor:
|
|
139
|
+
"""Instance of the |Motor| object that belongs to |Axis|."""
|
|
140
|
+
return self._motor
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def ip(self):
|
|
144
|
+
"""IPv4 address for the Axis' motor"""
|
|
145
|
+
return self.motor.ip
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def is_moving(self) -> bool:
|
|
149
|
+
"""
|
|
150
|
+
`True` or `False` indicating if the axis is currently moving.
|
|
151
|
+
"""
|
|
152
|
+
return self.motor.is_moving
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def position(self):
|
|
156
|
+
"""
|
|
157
|
+
Current axis position in units defined by the :attr:`units`
|
|
158
|
+
attribute.
|
|
159
|
+
"""
|
|
160
|
+
pos = self.motor.position
|
|
161
|
+
return pos.to(self.units, equivalencies=self.equivalencies)
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def steps_per_rev(self):
|
|
165
|
+
"""Number of motor steps for a full revolution."""
|
|
166
|
+
return self.motor.steps_per_rev
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def units(self) -> u.Unit:
|
|
170
|
+
"""
|
|
171
|
+
The unit of measure for the `Axis` physical parameters like
|
|
172
|
+
position, speed, etc.
|
|
173
|
+
"""
|
|
174
|
+
return self._units
|
|
175
|
+
|
|
176
|
+
@units.setter
|
|
177
|
+
def units(self, new_units: u.Unit):
|
|
178
|
+
"""Set the units of measure."""
|
|
179
|
+
if self.units.physical_type != new_units.physical_type:
|
|
180
|
+
raise ValueError
|
|
181
|
+
|
|
182
|
+
self._units_per_rev = self.units_per_rev.to(new_units / u.rev)
|
|
183
|
+
self._units = new_units
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def units_per_rev(self) -> u.Quantity:
|
|
187
|
+
"""
|
|
188
|
+
The number of units (:attr:`units`) translated per full
|
|
189
|
+
revolution of the motor (:attr:`motor`).
|
|
190
|
+
"""
|
|
191
|
+
return self._units_per_rev
|
|
192
|
+
|
|
193
|
+
@units_per_rev.setter
|
|
194
|
+
def units_per_rev(self, value: Union[float, u.Quantity]):
|
|
195
|
+
"""
|
|
196
|
+
Update the number of units translated per full revolution of the
|
|
197
|
+
motor.
|
|
198
|
+
"""
|
|
199
|
+
if isinstance(value, float) and value > 0.0:
|
|
200
|
+
self._units_per_rev = value * self.units / u.rev
|
|
201
|
+
elif (
|
|
202
|
+
isinstance(value, u.Quantity)
|
|
203
|
+
and value.unit == self.units / u.rev
|
|
204
|
+
and value > 0.0
|
|
205
|
+
):
|
|
206
|
+
self._units_per_rev = value
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def equivalencies(self):
|
|
210
|
+
"""
|
|
211
|
+
List of unit equivalencies to convert back-and-forth between
|
|
212
|
+
the axis physical units and the motor units.
|
|
213
|
+
"""
|
|
214
|
+
steps_per_rev = self.steps_per_rev.value
|
|
215
|
+
units_per_rev = self.units_per_rev.value
|
|
216
|
+
|
|
217
|
+
equivs = [
|
|
218
|
+
(
|
|
219
|
+
u.rev,
|
|
220
|
+
u.steps,
|
|
221
|
+
lambda x: int(x * steps_per_rev),
|
|
222
|
+
lambda x: x / steps_per_rev,
|
|
223
|
+
),
|
|
224
|
+
(
|
|
225
|
+
u.rev,
|
|
226
|
+
self.units,
|
|
227
|
+
lambda x: x * units_per_rev,
|
|
228
|
+
lambda x: x / units_per_rev,
|
|
229
|
+
),
|
|
230
|
+
(
|
|
231
|
+
u.steps,
|
|
232
|
+
self.units,
|
|
233
|
+
lambda x: x * units_per_rev / steps_per_rev,
|
|
234
|
+
lambda x: int(x * steps_per_rev / units_per_rev),
|
|
235
|
+
),
|
|
236
|
+
]
|
|
237
|
+
for equiv in equivs.copy():
|
|
238
|
+
equivs.extend(
|
|
239
|
+
[
|
|
240
|
+
(equiv[0] / u.s, equiv[1] / u.s, equiv[2], equiv[3]),
|
|
241
|
+
(equiv[0] / u.s / u.s, equiv[1] / u.s / u.s, equiv[2], equiv[3]),
|
|
242
|
+
]
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return equivs
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def conversion_pairs(self):
|
|
249
|
+
"""
|
|
250
|
+
List of conversion pairs between motor units and physical
|
|
251
|
+
units. For example, ``[(u.steps, self.units), ...]``.
|
|
252
|
+
"""
|
|
253
|
+
return [
|
|
254
|
+
(u.steps, self.units),
|
|
255
|
+
(u.steps / u.s, self.units / u.s),
|
|
256
|
+
(u.steps / u.s / u.s, self.units / u.s / u.s),
|
|
257
|
+
(u.rev / u.s, self.units / u.s),
|
|
258
|
+
(u.rev / u.s / u.s, self.units / u.s / u.s),
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
def send_command(self, command, *args):
|
|
262
|
+
"""
|
|
263
|
+
Send ``command`` to the motor, and receive its response. If the
|
|
264
|
+
`event loop`_ is running, then the command will be sent as
|
|
265
|
+
a threadsafe coroutine_ in the loop. Otherwise, the command
|
|
266
|
+
will be sent directly to the motor.
|
|
267
|
+
|
|
268
|
+
Parameters
|
|
269
|
+
----------
|
|
270
|
+
command: str
|
|
271
|
+
The desired command to be sent to the motor.
|
|
272
|
+
*args:
|
|
273
|
+
Any arguments to the ``command`` that will be sent with the
|
|
274
|
+
motor command.
|
|
275
|
+
"""
|
|
276
|
+
cmd_entry = self.motor._commands[command]
|
|
277
|
+
motor_unit = cmd_entry["units"] # type: u.Unit
|
|
278
|
+
|
|
279
|
+
# TODO: put this into a separate convert() method that can handle both
|
|
280
|
+
# the send and recv unit conversion
|
|
281
|
+
if motor_unit is not None and len(args):
|
|
282
|
+
axis_unit = None
|
|
283
|
+
for motor_u, axis_u in self.conversion_pairs:
|
|
284
|
+
if motor_unit == motor_u:
|
|
285
|
+
axis_unit = axis_u
|
|
286
|
+
break
|
|
287
|
+
|
|
288
|
+
if axis_unit is not None:
|
|
289
|
+
args = list(args)
|
|
290
|
+
args[0] = args[0] * axis_unit.to(
|
|
291
|
+
motor_unit, equivalencies=self.equivalencies
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# TODO: There should be a cleaner way of enforcing this
|
|
295
|
+
# int conversion...maybe add it to the Motor class,
|
|
296
|
+
# but I [Erik] currently feel the conversion should
|
|
297
|
+
# happen outside the Motor class
|
|
298
|
+
if motor_unit is u.steps:
|
|
299
|
+
args[0] = int(args[0])
|
|
300
|
+
|
|
301
|
+
rtn = self.motor.send_command(command, *args)
|
|
302
|
+
|
|
303
|
+
# TODO: see detailing todo above
|
|
304
|
+
if hasattr(rtn, "unit"):
|
|
305
|
+
axis_unit = None
|
|
306
|
+
for motor_u, axis_u in self.conversion_pairs:
|
|
307
|
+
if rtn.unit == motor_u:
|
|
308
|
+
axis_unit = axis_u
|
|
309
|
+
break
|
|
310
|
+
|
|
311
|
+
if axis_unit is not None:
|
|
312
|
+
rtn = rtn.to(axis_unit, equivalencies=self.equivalencies)
|
|
313
|
+
|
|
314
|
+
return rtn
|
|
315
|
+
|
|
316
|
+
def move_to(self, *args):
|
|
317
|
+
"""
|
|
318
|
+
Quick access command for ``send_command("move_to", *args)``.
|
|
319
|
+
"""
|
|
320
|
+
return self.send_command("move_to", *args)
|
|
321
|
+
|
|
322
|
+
def stop(self):
|
|
323
|
+
"""
|
|
324
|
+
Quick access command for ``send_command("stop")``.
|
|
325
|
+
"""
|
|
326
|
+
# not sending STOP command through send_command() since using
|
|
327
|
+
# motor.stop() should result in faster execution
|
|
328
|
+
return self.motor.stop()
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for functionality focused around the [Abstract] base actors.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__all__ = ["BaseActor", "EventActor"]
|
|
6
|
+
__actors__ = ["BaseActor", "EventActor"]
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import threading
|
|
11
|
+
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from typing import Any, Dict, List, Optional, Union
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# TODO: create an EventActor for an actor that utilizes asyncio event loops
|
|
17
|
+
# - EventActor should inherit from BaseActor and ABC
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseActor(ABC):
|
|
21
|
+
"""
|
|
22
|
+
Low-level base class for any Actor class.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
name : str, optional
|
|
27
|
+
A unique :attr:`name` for the Actor instance.
|
|
28
|
+
logger : `~logging.Logger`, optional
|
|
29
|
+
The instance of `~logging.Logger` that the Actor should record
|
|
30
|
+
events and status updates.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self, *, name: str = None, logger: logging.Logger = None,
|
|
35
|
+
):
|
|
36
|
+
# setup logger to track events
|
|
37
|
+
log_name = "Actor" if logger is None else logger.name
|
|
38
|
+
if name is not None:
|
|
39
|
+
log_name += f".{name}"
|
|
40
|
+
|
|
41
|
+
self.name = name if name is not None else ""
|
|
42
|
+
self.logger = logging.getLogger(log_name)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def name(self) -> str:
|
|
46
|
+
"""
|
|
47
|
+
(`str`) A unique name given for the instance of the actor. This
|
|
48
|
+
name is used as an identifier in the actor logger (see
|
|
49
|
+
:attr:`logger`).
|
|
50
|
+
|
|
51
|
+
If the user does not specify a name, then the Actor should
|
|
52
|
+
auto-generate a name.
|
|
53
|
+
"""
|
|
54
|
+
return self._name
|
|
55
|
+
|
|
56
|
+
@name.setter
|
|
57
|
+
def name(self, value):
|
|
58
|
+
self._name = value
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def logger(self) -> logging.Logger:
|
|
62
|
+
"""The `~logger.Logger` instance being used for the actor."""
|
|
63
|
+
return self._logger
|
|
64
|
+
|
|
65
|
+
@logger.setter
|
|
66
|
+
def logger(self, value):
|
|
67
|
+
self._logger = value
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def config(self) -> Dict[str, Any]:
|
|
72
|
+
"""
|
|
73
|
+
Configuration dictionary of the actor.
|
|
74
|
+
|
|
75
|
+
.. warning::
|
|
76
|
+
|
|
77
|
+
This dictionary should never be written to from outside the
|
|
78
|
+
owning actor.
|
|
79
|
+
"""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# TODO: Create an EventActor
|
|
84
|
+
# - must setup the asyncio event loop
|
|
85
|
+
# - must handle running th loop in a separate thread
|
|
86
|
+
# - How should I incorporate the heartbeat?
|
|
87
|
+
# - Must have the option to auto_run the event loop
|
|
88
|
+
# - must inherit from BaseActor
|
|
89
|
+
# - will likely need abstract methods _actor_setup_pre_loop() and
|
|
90
|
+
# _actor_setup_post_loop() for setup actions before and after
|
|
91
|
+
# the loop creation, respectively.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class EventActor(BaseActor, ABC):
|
|
95
|
+
r"""
|
|
96
|
+
A base class for any Actor that will be interacting with an `asncio`
|
|
97
|
+
event loop.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
name : str, optional
|
|
102
|
+
A unique :attr:`name` for the Actor instance.
|
|
103
|
+
|
|
104
|
+
logger : `~logging.Logger`, optional
|
|
105
|
+
The instance of `~logging.Logger` that the Actor should record
|
|
106
|
+
events and status updates.
|
|
107
|
+
|
|
108
|
+
loop: `asyncio.AbstractEventLoop`, optional
|
|
109
|
+
Instance of an `asyncio` `event loop`_\ . If `None`, then an
|
|
110
|
+
`event loop`_ will be auto-generated. (DEFAULT: `None`)
|
|
111
|
+
|
|
112
|
+
auto_run: bool, optional
|
|
113
|
+
If `True`, then the `event loop`_ will be placed in a separate
|
|
114
|
+
thread and started. This is all done via the :meth:`run`
|
|
115
|
+
method. (DEFAULT: `False`)
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
*,
|
|
121
|
+
name: str = None,
|
|
122
|
+
logger: logging.Logger = None,
|
|
123
|
+
loop: asyncio.AbstractEventLoop = None,
|
|
124
|
+
auto_run: bool = False,
|
|
125
|
+
):
|
|
126
|
+
|
|
127
|
+
super().__init__(name=name, logger=logger)
|
|
128
|
+
|
|
129
|
+
self._thread = None
|
|
130
|
+
self._loop = self.setup_event_loop(loop)
|
|
131
|
+
self._tasks = None
|
|
132
|
+
|
|
133
|
+
self._configure_before_run()
|
|
134
|
+
self._initialize_tasks()
|
|
135
|
+
|
|
136
|
+
self.run(auto_run)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def tasks(self) -> List[asyncio.Task]:
|
|
140
|
+
r"""
|
|
141
|
+
List of `asyncio.Task`\ s this actor has in its `event loop`_.
|
|
142
|
+
"""
|
|
143
|
+
if self._tasks is None:
|
|
144
|
+
self._tasks = []
|
|
145
|
+
|
|
146
|
+
return self._tasks
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def loop(self) -> asyncio.AbstractEventLoop:
|
|
150
|
+
"""The `asyncio` :term:`event loop` for the actor."""
|
|
151
|
+
return self._loop
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def thread(self) -> threading.Thread:
|
|
155
|
+
"""
|
|
156
|
+
The `~threading.Thread` the `event loop`_ is running in.
|
|
157
|
+
|
|
158
|
+
If :attr:`loop` was given during instantiation, then there is
|
|
159
|
+
no way of obtaining the thread object the event loop is
|
|
160
|
+
running in. In this case :attr:`thread` will be `None`.
|
|
161
|
+
|
|
162
|
+
The thread id can always be retrieved using :attr:`_thread_id`.
|
|
163
|
+
"""
|
|
164
|
+
return self._thread
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def _thread_id(self) -> Union[int, None]:
|
|
168
|
+
"""
|
|
169
|
+
Unique ID for the thread the loop is running in.
|
|
170
|
+
|
|
171
|
+
`None` if the :attr:`loop` does not exit or is not running.
|
|
172
|
+
"""
|
|
173
|
+
if self.loop is None or not self.loop.is_running():
|
|
174
|
+
# no loop has been created or loop is not running
|
|
175
|
+
return None
|
|
176
|
+
elif self._thread is not None:
|
|
177
|
+
return self._thread.ident
|
|
178
|
+
|
|
179
|
+
# get thread id from inside the event loop
|
|
180
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
181
|
+
self._thread_id_async(),
|
|
182
|
+
self.loop
|
|
183
|
+
)
|
|
184
|
+
return future.result(5)
|
|
185
|
+
|
|
186
|
+
async def _thread_id_async(self):
|
|
187
|
+
"""
|
|
188
|
+
Asyncio coroutine for retrieving the id of the thread the event
|
|
189
|
+
loop is running in.
|
|
190
|
+
"""
|
|
191
|
+
return threading.current_thread().ident
|
|
192
|
+
|
|
193
|
+
@abstractmethod
|
|
194
|
+
def _configure_before_run(self):
|
|
195
|
+
# A set of functionality for the subclass to run before the
|
|
196
|
+
# asyncio tasks are created and the event loop is started.
|
|
197
|
+
#
|
|
198
|
+
# This method is executed by __init__ before the event loop is
|
|
199
|
+
# started.
|
|
200
|
+
...
|
|
201
|
+
|
|
202
|
+
@abstractmethod
|
|
203
|
+
def _initialize_tasks(self):
|
|
204
|
+
# Used by the subclass to initialize a list of tasks to be
|
|
205
|
+
# executed in the event loop after the loop is started.
|
|
206
|
+
#
|
|
207
|
+
# This method is executed by __init__ after
|
|
208
|
+
# _configure_before_run() but before the event loop is started.
|
|
209
|
+
...
|
|
210
|
+
|
|
211
|
+
def setup_event_loop(
|
|
212
|
+
self, loop: Optional[asyncio.AbstractEventLoop] = None
|
|
213
|
+
):
|
|
214
|
+
"""
|
|
215
|
+
Set up the `asyncio` `event loop`_. If the given loop is not an
|
|
216
|
+
instance of `~asyncio.AbstractEventLoop`, then a new loop will
|
|
217
|
+
be created.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
loop: `asyncio.AbstractEventLoop`
|
|
222
|
+
`asyncio` `event loop`_ for the actor's tasks
|
|
223
|
+
|
|
224
|
+
"""
|
|
225
|
+
# get a valid event loop
|
|
226
|
+
if loop is None:
|
|
227
|
+
loop = asyncio.new_event_loop()
|
|
228
|
+
elif not isinstance(loop, asyncio.AbstractEventLoop):
|
|
229
|
+
self.logger.warning(
|
|
230
|
+
"Given asyncio event is not valid. Creating a new event loop to use."
|
|
231
|
+
)
|
|
232
|
+
loop = asyncio.new_event_loop()
|
|
233
|
+
return loop
|
|
234
|
+
|
|
235
|
+
def run(self, auto_run=True):
|
|
236
|
+
r"""
|
|
237
|
+
Activate the `asyncio` `event loop`_\ . If the event loop is
|
|
238
|
+
running, then nothing happens. Otherwise, the event loop is
|
|
239
|
+
placed in a separate thread and set to
|
|
240
|
+
`~asyncio.loop.run_forever`.
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
auto_run: `bool`, optional
|
|
245
|
+
If `False`, then do NOT start the event loop. This keyword
|
|
246
|
+
is only made available to help with subclassing.
|
|
247
|
+
(DEFAULT: `True`)
|
|
248
|
+
"""
|
|
249
|
+
if self.loop is None or self.loop.is_running() or not auto_run:
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
self._thread = threading.Thread(target=self._loop.run_forever)
|
|
253
|
+
self._thread.start()
|
|
254
|
+
|
|
255
|
+
def terminate(self, delay_loop_stop=False):
|
|
256
|
+
r"""
|
|
257
|
+
Stop the actor's `event loop`_\ . All actor tasks will be
|
|
258
|
+
cancelled, the connection to the motor will be shutdown, and
|
|
259
|
+
the event loop will be stopped.
|
|
260
|
+
|
|
261
|
+
Parameters
|
|
262
|
+
----------
|
|
263
|
+
delay_loop_stop: bool
|
|
264
|
+
If `True`, then do NOT stop the `event loop`_\ . In this
|
|
265
|
+
case it is assumed the calling functionality is managing
|
|
266
|
+
additional tasks in the event loop, and it is up to that
|
|
267
|
+
functionality to stop the loop. (DEFAULT: `False`)
|
|
268
|
+
"""
|
|
269
|
+
for task in list(self.tasks):
|
|
270
|
+
self.loop.call_soon_threadsafe(task.cancel)
|
|
271
|
+
self.tasks.remove(task)
|
|
272
|
+
|
|
273
|
+
if delay_loop_stop:
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
# if we're stopping the loop, then all tasks need to be cancelled
|
|
277
|
+
for task in asyncio.all_tasks(self.loop):
|
|
278
|
+
if not task.done() or not task.cancelled():
|
|
279
|
+
self.loop.call_soon_threadsafe(task.cancel)
|
|
280
|
+
|
|
281
|
+
self.loop.call_soon_threadsafe(self.loop.stop)
|
|
File without changes
|