lima2.client 1.0.9__tar.gz → 2.0rc2__tar.gz

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.
Files changed (55) hide show
  1. {lima2_client-1.0.9/src/lima2.client.egg-info → lima2_client-2.0rc2}/PKG-INFO +37 -7
  2. lima2_client-2.0rc2/README.md +43 -0
  3. {lima2_client-1.0.9 → lima2_client-2.0rc2}/setup.cfg +21 -4
  4. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2/client/__init__.py +6 -3
  5. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2/client/channel.py +1 -1
  6. lima2_client-2.0rc2/src/lima2/client/client.py +276 -0
  7. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2/client/convert.py +1 -1
  8. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2/client/detector.py +31 -199
  9. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2/client/devencoded/__init__.py +1 -1
  10. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2/client/devencoded/dense_frame.py +9 -4
  11. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2/client/devencoded/exception.py +1 -1
  12. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2/client/devencoded/sparse_frame.py +8 -7
  13. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2/client/devencoded/sparse_frame_with_background.py +8 -6
  14. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2/client/devencoded/structured_array.py +2 -2
  15. lima2_client-2.0rc2/src/lima2/client/pipeline.py +555 -0
  16. lima2_client-2.0rc2/src/lima2/client/pipelines/__init__.py +46 -0
  17. lima2_client-2.0rc2/src/lima2/client/pipelines/legacy.py +56 -0
  18. lima2_client-2.0rc2/src/lima2/client/pipelines/smx.py +120 -0
  19. lima2_client-2.0rc2/src/lima2/client/pipelines/xpcs.py +127 -0
  20. lima2_client-2.0rc2/src/lima2/client/progress_counter.py +69 -0
  21. lima2_client-2.0rc2/src/lima2/client/py.typed +0 -0
  22. lima2_client-2.0rc2/src/lima2/client/shell.py +150 -0
  23. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2/client/state_machine.py +33 -25
  24. lima2_client-2.0rc2/src/lima2/client/topology.py +162 -0
  25. lima2_client-2.0rc2/src/lima2/client/utils.py +49 -0
  26. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2/server/__init__.py +1 -1
  27. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2/server/control.py +28 -12
  28. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2/server/receiver.py +56 -12
  29. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2/server/timer.py +1 -1
  30. {lima2_client-1.0.9 → lima2_client-2.0rc2/src/lima2.client.egg-info}/PKG-INFO +37 -7
  31. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2.client.egg-info/SOURCES.txt +15 -4
  32. lima2_client-2.0rc2/src/lima2.client.egg-info/entry_points.txt +2 -0
  33. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2.client.egg-info/requires.txt +6 -0
  34. lima2_client-2.0rc2/tests/test_client.py +162 -0
  35. {lima2_client-1.0.9 → lima2_client-2.0rc2}/tests/test_detector.py +3 -30
  36. {lima2_client-1.0.9 → lima2_client-2.0rc2}/tests/test_import.py +1 -1
  37. lima2_client-2.0rc2/tests/test_pipeline.py +486 -0
  38. lima2_client-2.0rc2/tests/test_pipelines.py +20 -0
  39. lima2_client-2.0rc2/tests/test_progress_counter.py +35 -0
  40. {lima2_client-1.0.9 → lima2_client-2.0rc2}/tests/test_state_machine.py +3 -3
  41. {lima2_client-1.0.9 → lima2_client-2.0rc2}/tests/test_timer.py +1 -1
  42. lima2_client-2.0rc2/tests/test_topology.py +145 -0
  43. lima2_client-2.0rc2/tests/test_utils.py +30 -0
  44. lima2_client-1.0.9/README.md +0 -18
  45. lima2_client-1.0.9/src/lima2/client/cli.py +0 -321
  46. lima2_client-1.0.9/src/lima2/client/pipelines/__init__.py +0 -6
  47. lima2_client-1.0.9/src/lima2/client/pipelines/legacy.py +0 -111
  48. lima2_client-1.0.9/src/lima2/client/pipelines/smx.py +0 -103
  49. lima2_client-1.0.9/src/lima2/client/pipelines/xpcs.py +0 -137
  50. lima2_client-1.0.9/src/lima2/client/processing.py +0 -180
  51. lima2_client-1.0.9/src/lima2.client.egg-info/entry_points.txt +0 -2
  52. /lima2_client-1.0.9/LICENCE → /lima2_client-2.0rc2/LICENSE +0 -0
  53. {lima2_client-1.0.9 → lima2_client-2.0rc2}/pyproject.toml +0 -0
  54. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2.client.egg-info/dependency_links.txt +0 -0
  55. {lima2_client-1.0.9 → lima2_client-2.0rc2}/src/lima2.client.egg-info/top_level.txt +0 -0
@@ -1,21 +1,26 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: lima2.client
3
- Version: 1.0.9
3
+ Version: 2.0rc2
4
4
  Summary: Python client library for Lima2 detector
5
- Home-page: https://gitlab.esrf.fr/limagroup/Lima2-client
5
+ Home-page: https://gitlab.esrf.fr/limagroup/lima2-client
6
6
  Author: BCU (ESRF)
7
- License: LGPLv3
7
+ License: MIT
8
8
  Classifier: Intended Audience :: Science/Research
9
- Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
9
+ Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Description-Content-Type: text/markdown
12
- License-File: LICENCE
12
+ License-File: LICENSE
13
13
  Requires-Dist: pytango
14
14
  Requires-Dist: gevent
15
15
  Requires-Dist: jsonschema
16
16
  Requires-Dist: jsonschema-default
17
17
  Requires-Dist: finite-state-machine
18
18
  Requires-Dist: beartype
19
+ Requires-Dist: numpy
20
+ Requires-Dist: pyyaml
21
+ Requires-Dist: typing-extensions
22
+ Provides-Extra: shell
23
+ Requires-Dist: ipython; extra == "shell"
19
24
 
20
25
  # LImA 2 Client library
21
26
 
@@ -23,6 +28,31 @@ This project provides a Python interface to the LImA 2 distributed system.
23
28
 
24
29
  Checkout the [documentation here](https://limagroup.gitlab-pages.esrf.fr/lima2-client/).
25
30
 
31
+ ### Development setup
32
+
33
+ In order to install lima2-client in a new conda environment:
34
+ ```sh
35
+ conda create -n l2c -c esrf-bcu python pytango finite-state-machine
36
+ pip install .
37
+ ```
38
+ and, if you need the lima2-client shell:
39
+ ```sh
40
+ pip install .[shell]
41
+ ```
42
+
43
+ ### Quickstart with blissdemo
44
+
45
+ With `bliss-demo-servers` running, you can run an interactive Lima2 client shell to manipulate the
46
+ Lima2 simulator devices and test the client API:
47
+
48
+ ```sh
49
+ export TANGO_HOST="localhost:10000" # Assuming you are running bliss-demo-servers locally
50
+ lima2_shell
51
+ ```
52
+
53
+ The `lima2_shell` utility instantiates a `Client` object using config from `l2c_config.yaml`,
54
+ creates a set of default control, acquisition and processing parameters, and starts an ipython session.
55
+
26
56
  ### Bootstrapping the documentation
27
57
 
28
58
  The source for the documentation is in the `docs` folder. Here are the instructions to built and read it locally. The documentation is built with [Doxygen](http://www.doxygen.org/) and [Sphinx](http://www.sphinx-doc.org). The sphinx template is from [Sphinx Material](https://bashtage.github.io/sphinx-material/).
@@ -30,7 +60,7 @@ The source for the documentation is in the `docs` folder. Here are the instructi
30
60
  ```
31
61
  conda create -n doc --file docs/requirements.txt -c conda-forge
32
62
  conda activate doc
33
- cd docs
63
+ cd docs
34
64
  make html
35
65
  ```
36
66
 
@@ -0,0 +1,43 @@
1
+ # LImA 2 Client library
2
+
3
+ This project provides a Python interface to the LImA 2 distributed system.
4
+
5
+ Checkout the [documentation here](https://limagroup.gitlab-pages.esrf.fr/lima2-client/).
6
+
7
+ ### Development setup
8
+
9
+ In order to install lima2-client in a new conda environment:
10
+ ```sh
11
+ conda create -n l2c -c esrf-bcu python pytango finite-state-machine
12
+ pip install .
13
+ ```
14
+ and, if you need the lima2-client shell:
15
+ ```sh
16
+ pip install .[shell]
17
+ ```
18
+
19
+ ### Quickstart with blissdemo
20
+
21
+ With `bliss-demo-servers` running, you can run an interactive Lima2 client shell to manipulate the
22
+ Lima2 simulator devices and test the client API:
23
+
24
+ ```sh
25
+ export TANGO_HOST="localhost:10000" # Assuming you are running bliss-demo-servers locally
26
+ lima2_shell
27
+ ```
28
+
29
+ The `lima2_shell` utility instantiates a `Client` object using config from `l2c_config.yaml`,
30
+ creates a set of default control, acquisition and processing parameters, and starts an ipython session.
31
+
32
+ ### Bootstrapping the documentation
33
+
34
+ The source for the documentation is in the `docs` folder. Here are the instructions to built and read it locally. The documentation is built with [Doxygen](http://www.doxygen.org/) and [Sphinx](http://www.sphinx-doc.org). The sphinx template is from [Sphinx Material](https://bashtage.github.io/sphinx-material/).
35
+
36
+ ```
37
+ conda create -n doc --file docs/requirements.txt -c conda-forge
38
+ conda activate doc
39
+ cd docs
40
+ make html
41
+ ```
42
+
43
+ The html documentation is generated in `docs/.build/html`.
@@ -4,11 +4,11 @@ author = BCU (ESRF)
4
4
  description = Python client library for Lima2 detector
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown
7
- license = LGPLv3
8
- url = https://gitlab.esrf.fr/limagroup/Lima2-client
7
+ license = MIT
8
+ url = https://gitlab.esrf.fr/limagroup/lima2-client
9
9
  classifiers =
10
10
  Intended Audience :: Science/Research
11
- License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
11
+ License :: OSI Approved :: MIT License
12
12
  Programming Language :: Python :: 3
13
13
 
14
14
  [options]
@@ -19,16 +19,33 @@ install_requires =
19
19
  jsonschema-default
20
20
  finite-state-machine
21
21
  beartype
22
+ numpy
23
+ pyyaml
24
+ typing-extensions
22
25
 
23
26
  [options.entry_points]
24
27
  console_scripts =
25
- lima2_cli = lima2.client.cli:main
28
+ lima2_shell = lima2.client.shell:main [shell]
29
+
30
+ [options.extras_require]
31
+ shell =
32
+ ipython
26
33
 
27
34
  [flake8]
28
35
  ignore = E501,W503,E203,W504,E251,E262,E265,E266,W291,W293
36
+ per-file-ignores =
37
+ src/lima2/client/shell.py:E402
29
38
 
30
39
  [tool:pytest]
40
+ required_plugins =
41
+ pytest-cov>=6.0.0
42
+ pytest-forked>=1.6.0
31
43
  addopts = -v --forked
44
+ pythonpath = src
45
+
46
+ [coverage:run]
47
+ omit =
48
+ src/lima2/client/shell.py
32
49
 
33
50
  [egg_info]
34
51
  tag_build =
@@ -3,7 +3,7 @@
3
3
  # This file is part of the Lima2 project
4
4
  #
5
5
  # Copyright (c) 2020-2024 Beamline Control Unit, ESRF
6
- # Distributed under the MIT licence. See LICENSE for more info.
6
+ # Distributed under the MIT license. See LICENSE for more info.
7
7
 
8
8
  """Lima2.Client package
9
9
 
@@ -13,6 +13,9 @@
13
13
  state_machine.State
14
14
  """
15
15
 
16
- from lima2.client.detector import Detector, CommError
16
+ from lima2.client.client import Client
17
+ from lima2.client.detector import CommError
17
18
  from lima2.client.state_machine import State
18
- from lima2.client.processing import Processing
19
+ from lima2.client.pipeline import Pipeline
20
+
21
+ __all__ = ["Client", "CommError", "State", "Pipeline"]
@@ -3,7 +3,7 @@
3
3
  # This file is part of the Lima2 project
4
4
  #
5
5
  # Copyright (c) 2020-2024 Beamline Control Unit, ESRF
6
- # Distributed under the MIT licence. See LICENSE for more info.
6
+ # Distributed under the MIT license. See LICENSE for more info.
7
7
 
8
8
  import numpy as np
9
9
  from abc import ABC, abstractmethod
@@ -0,0 +1,276 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # This file is part of the Lima2 project
4
+ #
5
+ # Copyright (c) 2020-2024 Beamline Control Unit, ESRF
6
+ # Distributed under the MIT license. See LICENSE for more info.
7
+
8
+ """User-facing interface exposing Lima2's control and acquisition API.
9
+
10
+ The role of the client is to connect the user to the Lima2 devices via Tango.
11
+
12
+ The Client object instantiates a `Detector` object, allowing the user to control the acquisition
13
+ via the standard prepare/start/stop sequence.
14
+
15
+ The client hides away the topology by providing aggregated stats and counters in the form of
16
+ `ProgressCounter` objects. If needed, these can be explored to trace the information back to
17
+ individual receivers, for instance if one of them is stuck on a frame while the rest are still
18
+ processing data.
19
+ """
20
+
21
+ import os
22
+ from uuid import UUID
23
+
24
+ import tango as tg
25
+ import yaml
26
+ from beartype import beartype
27
+ from typing_extensions import Any, Optional
28
+
29
+ from lima2.client import pipelines, progress_counter, state_machine, topology
30
+ from lima2.client.detector import Detector
31
+ from lima2.client.pipeline import Pipeline
32
+ from lima2.client.progress_counter import ProgressCounter, SingleCounter
33
+ from lima2.client.topology import TopologyKind
34
+ from lima2.client.utils import docstring_from
35
+
36
+
37
+ @beartype
38
+ class Client:
39
+ """Lima2 user-facing client."""
40
+
41
+ def __init__(
42
+ self,
43
+ ctl_dev: tg.DeviceProxy,
44
+ rcv_devs: list[tg.DeviceProxy],
45
+ ):
46
+ """Construct a Client given tango device proxies.
47
+
48
+ The Client can also be built from a yaml config file (see `from_yaml`).
49
+ The topology is queried from the control device properties.
50
+ """
51
+
52
+ self._detector = Detector(ctl_dev, *rcv_devs)
53
+
54
+ try:
55
+ # Get the topology property from control
56
+ topology_prop = ctl_dev.get_property("receiver_topology")
57
+ topology_name = topology_prop["receiver_topology"][0]
58
+ self.topology_kind = TopologyKind(topology_name)
59
+ except IndexError:
60
+ # Property does not exist
61
+ raise RuntimeError(
62
+ "Could not find 'receiver_topology' property in control device"
63
+ ) from None
64
+ except ValueError as e:
65
+ # Invalid topology (no matching name in Topology enum)
66
+ raise ValueError(
67
+ f"No matching topology type for {repr(topology_name)}"
68
+ ) from e
69
+
70
+ self.recv_devs = rcv_devs
71
+
72
+ # Cache of processing pipelines by uuid (see method `pipeline`)
73
+ self.__pipelines: dict[UUID, Pipeline] = {}
74
+
75
+ self.tango_db = tg.Database()
76
+
77
+ @classmethod
78
+ def from_yaml(
79
+ cls,
80
+ config_filename: str = "l2c_config.yaml",
81
+ ) -> "Client":
82
+ with open(os.path.abspath(config_filename)) as config_f:
83
+ # Raises on io/parsing error
84
+ config = yaml.safe_load(config_f)
85
+
86
+ ctl_url = config["ctl_url"]
87
+ rcv_urls = config["rcv_urls"]
88
+
89
+ ctl_dev = tg.DeviceProxy(ctl_url)
90
+ rcv_devs = [tg.DeviceProxy(url) for url in rcv_urls]
91
+
92
+ return Client(ctl_dev=ctl_dev, rcv_devs=rcv_devs)
93
+
94
+ @property
95
+ def detector(self) -> Detector:
96
+ return self._detector
97
+
98
+ ####################################################################################
99
+ # Detector properties
100
+ ####################################################################################
101
+
102
+ @property
103
+ @docstring_from(Detector.det_info)
104
+ def det_info(self) -> dict[str, Any]:
105
+ return self.detector.det_info
106
+
107
+ @property
108
+ @docstring_from(Detector.det_status)
109
+ def det_status(self) -> dict[str, Any]:
110
+ return self.detector.det_status
111
+
112
+ @property
113
+ @docstring_from(Detector.det_capabilities)
114
+ def det_capabilities(self) -> dict[str, Any]:
115
+ return self.detector.det_capabilities
116
+
117
+ @property
118
+ @docstring_from(Detector.state)
119
+ def state(self) -> state_machine.State:
120
+ return self.detector.state
121
+
122
+ @property
123
+ @docstring_from(Detector.params_default)
124
+ def params_default(self) -> dict[str, Any]:
125
+ return self.detector.params_default
126
+
127
+ ####################################################################################
128
+ # Control
129
+ ####################################################################################
130
+
131
+ def prepare_acq(
132
+ self, uuid: UUID, ctrl_params: dict, acq_params: dict, proc_params: dict
133
+ ) -> None:
134
+ """Prepare acquisition with a given uuid and set of params."""
135
+
136
+ # Adjust params for distributed acquisition
137
+ ctl, acq, proc = topology.distribute_acq(
138
+ ctl_params=ctrl_params,
139
+ acq_params=acq_params,
140
+ proc_params=proc_params,
141
+ num_receivers=len(self.recv_devs),
142
+ )
143
+
144
+ self.detector.prepare_acq(uuid, ctl, acq, proc)
145
+
146
+ @docstring_from(Detector.start_acq)
147
+ def start_acq(self) -> None:
148
+ self.detector.start_acq()
149
+
150
+ @docstring_from(Detector.trigger)
151
+ def trigger(self) -> None:
152
+ self.detector.trigger()
153
+
154
+ @docstring_from(Detector.stop_acq)
155
+ def stop_acq(self) -> None:
156
+ self.detector.stop_acq()
157
+
158
+ @docstring_from(Detector.reset_acq)
159
+ def reset_acq(self) -> None:
160
+ self.detector.reset_acq()
161
+
162
+ ####################################################################################
163
+ # Progress counters
164
+ ####################################################################################
165
+
166
+ @property
167
+ def nb_frames_acquired(self) -> ProgressCounter:
168
+ """Number of frames acquired"""
169
+ return ProgressCounter.from_single(
170
+ SingleCounter(
171
+ name="nb_frames_acquired",
172
+ value=self.detector.nb_frames_acquired,
173
+ source=self.detector.ctrl.name(),
174
+ )
175
+ )
176
+
177
+ @property
178
+ def nb_frames_xferred(self) -> ProgressCounter:
179
+ """Aggregated number of frames transferred"""
180
+ return progress_counter.aggregate(
181
+ [
182
+ SingleCounter(
183
+ name="nb_frames_xferred",
184
+ value=recv.nb_frames_xferred,
185
+ source=recv.name(),
186
+ )
187
+ for recv in self.detector.recvs
188
+ ]
189
+ )
190
+
191
+ ####################################################################################
192
+ # Pipelines
193
+ ####################################################################################
194
+
195
+ @property
196
+ def pipelines(self) -> list[UUID]:
197
+ """Get list of available pipelines UUID"""
198
+ uuids = [recv.pipelines for recv in self.recv_devs]
199
+ if all(uuid is None for uuid in uuids):
200
+ return []
201
+ return [UUID(u) for u in {y for x in uuids for y in x}]
202
+
203
+ def pipeline(self, uuid: Optional[UUID] = None) -> Pipeline:
204
+ """Get a specific pipeline by uuid. Return the current one by default (uuid=None)"""
205
+ if uuid is None:
206
+ return self.current_pipeline
207
+
208
+ if uuid in self.__pipelines:
209
+ return self.__pipelines[uuid]
210
+
211
+ # uuid isn't in the local cache of pipelines: get it from Tango
212
+ instances = self.tango_db.get_device_exported(f"*/limaprocessing/{uuid}*")
213
+ if not instances:
214
+ raise ValueError(f"Pipeline not found in tango database: {uuid=}")
215
+
216
+ class_names = [
217
+ self.tango_db.get_device_info(instance).class_name for instance in instances
218
+ ]
219
+
220
+ # Select the pipeline according to the Tango class (Homogeneous processing for now)
221
+ pipeline_class = pipelines.get_class(tango_class_name=class_names[0])
222
+
223
+ # Note: sort list of pipeline instances by name such that pipelines are ordered
224
+ # by receiver index. This is necessary e.g. in a strict round-robin topology to
225
+ # be able to find a frame by index.
226
+ proc_devs = [tg.DeviceProxy(url) for url in sorted(list(instances))]
227
+
228
+ # Instantiate pipeline
229
+ self.__pipelines[uuid] = pipeline_class(uuid, proc_devs, self.topology_kind)
230
+
231
+ return self.__pipelines[uuid]
232
+
233
+ @property
234
+ def current_pipeline(self) -> Pipeline:
235
+ """Return the current pipeline as a Pipeline object.
236
+
237
+ Raises if no pipeline has been created, or if receivers disagree on the
238
+ current pipeline's uuid.
239
+ """
240
+ uuids: list[str] = [recv.current_pipeline for recv in self.recv_devs]
241
+
242
+ if all([uuid == "" for uuid in uuids]):
243
+ raise ValueError("No pipeline present in the tango database")
244
+
245
+ uuids: list[UUID] = [UUID(uuid) for uuid in uuids]
246
+
247
+ if not all([uuid == uuids[0] for uuid in uuids]):
248
+ raise ValueError(f"Inconsistent pipeline uuids on all receivers: {uuids=}")
249
+
250
+ return self.pipeline(uuids[0])
251
+
252
+ def erase_pipeline(self, uuid: UUID) -> None:
253
+ """Erase a pipeline instance"""
254
+ self.__pipelines.pop(uuid, None)
255
+ for recv in self.recv_devs:
256
+ recv.erasePipeline(str(uuid))
257
+
258
+ ####################################################################################
259
+ # Utility
260
+ ####################################################################################
261
+
262
+ def __repr__(self) -> str:
263
+ return "\n".join(
264
+ [
265
+ "Lima2 client",
266
+ f"State: {self.detector.state}",
267
+ "Controller:",
268
+ f" {self.detector.ctrl}",
269
+ f"Receivers ({self.topology_kind}):",
270
+ *[f" - {recv}" for recv in self.detector.recvs],
271
+ "Progress counters:",
272
+ f" - {self.nb_frames_acquired}",
273
+ f" - {self.nb_frames_xferred}",
274
+ str(self.current_pipeline) if self.__pipelines else "",
275
+ ]
276
+ )
@@ -3,7 +3,7 @@
3
3
  # This file is part of the Lima2 project
4
4
  #
5
5
  # Copyright (c) 2020-2024 Beamline Control Unit, ESRF
6
- # Distributed under the MIT licence. See LICENSE for more info.
6
+ # Distributed under the MIT license. See LICENSE for more info.
7
7
 
8
8
  """Convertion routines.
9
9