bluesky-tiled-plugins 2.0.0b54__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.
@@ -0,0 +1,140 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+
5
+ # C extensions
6
+ *.so
7
+
8
+ # Distribution / packaging
9
+ .Python
10
+ env/
11
+ bin/
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+ docs/_build
27
+
28
+ # PyInstaller
29
+
30
+ # Usually these files are written by a python script from a template
31
+
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # dotenv environment variables file
41
+ .env*
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ cover/
47
+ .coverage
48
+ .cache
49
+ nosetests.xml
50
+ coverage.xml
51
+ cover/*
52
+
53
+ # Translations
54
+ *.mo
55
+ *.pot
56
+
57
+ # Mr Developer
58
+ .mr.developer.cfg
59
+ .project
60
+ .pydevproject
61
+
62
+ # Rope
63
+ .ropeproject
64
+
65
+ # Django stuff:
66
+ *.log
67
+
68
+ # Sphinx documentation
69
+ docs/_build/
70
+
71
+ #mac
72
+ .DS_Store
73
+ *~
74
+
75
+ #vim
76
+ *.swp
77
+
78
+ #pycharm
79
+ .idea/*
80
+
81
+ #Dolphin browser files
82
+ .directory/
83
+ .directory
84
+
85
+ #Binary data files
86
+ *.volume
87
+ *.am
88
+ *.tiff
89
+ *.tif
90
+ *.dat
91
+ *.DAT
92
+
93
+ #generated documntation files
94
+ docs/resource/api/generated/
95
+
96
+ # Enaml
97
+ __enamlcache__/
98
+
99
+ # PyBuilder
100
+ target/
101
+
102
+ # sphinx docs
103
+ docs/source/generated/
104
+
105
+ # ctags
106
+ .tags*
107
+
108
+ # spyder ide
109
+ .spyderworkspace*
110
+
111
+ # PyCharm
112
+ .idea/
113
+ *.swo
114
+
115
+ # Ctags
116
+ .tags
117
+
118
+ .pytest_cache
119
+ docs/source/_as_gen/*
120
+
121
+ # For VIM Users
122
+
123
+ #
124
+ dosc/source/_as_gen
125
+
126
+ # generated by IPython example in docs
127
+ docs/data/*
128
+ docs/source/_generated_images/*
129
+
130
+ *.swn
131
+
132
+ .vscode/*
133
+
134
+ docs/output_directory
135
+ docs/data.csv
136
+ docs/data.xlsx
137
+ docs/data.h5
138
+
139
+ # generated by docker-compose
140
+ data/*
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: bluesky-tiled-plugins
3
+ Version: 2.0.0b54
4
+ Summary: Tiled client plugins to provide an customized user experience for Bluesky data in Tiled
5
+ Author-email: Bluesky Project Contributors <dallan@bnl.gov>
6
+ Maintainer-email: Brookhaven National Laboratory <dallan@bnl.gov>
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: License :: OSI Approved :: BSD License
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Python :: 3.7
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering :: Physics
17
+ Requires-Python: >=3.7
18
+ Requires-Dist: dask
19
+ Requires-Dist: mongoquery
20
+ Requires-Dist: pytz
21
+ Requires-Dist: tiled[client]>=0.1.0b4
22
+ Requires-Dist: tzlocal
23
+ Description-Content-Type: text/markdown
24
+
25
+ # bluesky-tiled-plugins
26
+
27
+ This is a separate Python package, `bluesky-tiled-plugins`, that is
28
+ developed in the databroker repository.
29
+
30
+ For a user wishing to connect to a running Tiled server and access Bluesky data,
31
+ this package, along with its dependency `tiled[client]`, is all they need.
32
+
33
+ The databroker package is only required if the user wants to use the legacy
34
+ `databroker.Broker` API.
@@ -0,0 +1,10 @@
1
+ # bluesky-tiled-plugins
2
+
3
+ This is a separate Python package, `bluesky-tiled-plugins`, that is
4
+ developed in the databroker repository.
5
+
6
+ For a user wishing to connect to a running Tiled server and access Bluesky data,
7
+ this package, along with its dependency `tiled[client]`, is all they need.
8
+
9
+ The databroker package is only required if the user wants to use the legacy
10
+ `databroker.Broker` API.
@@ -0,0 +1,3 @@
1
+ from .catalog_of_bluesky_runs import CatalogOfBlueskyRuns # noqa: F401
2
+ from .bluesky_event_stream import BlueskyEventStream # noqa: F401
3
+ from .bluesky_run import BlueskyRun # noqa: F401
@@ -0,0 +1,6 @@
1
+ # There are methods that IPython will try to call.
2
+ # We special-case them because we want to avoid the getattr
3
+ # resulting in an unnecessary network hit just to raise
4
+ # AttributeError.
5
+
6
+ IPYTHON_METHODS = {"_ipython_canary_method_should_not_exist_", "_repr_mimebundle_"}
@@ -0,0 +1,16 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
7
+ else:
8
+ VERSION_TUPLE = object
9
+
10
+ version: str
11
+ __version__: str
12
+ __version_tuple__: VERSION_TUPLE
13
+ version_tuple: VERSION_TUPLE
14
+
15
+ __version__ = version = '2.0.0b54'
16
+ __version_tuple__ = version_tuple = (2, 0, 0)
@@ -0,0 +1,83 @@
1
+ import keyword
2
+ import warnings
3
+
4
+ from tiled.client.container import DEFAULT_STRUCTURE_CLIENT_DISPATCH, Container
5
+
6
+ from ._common import IPYTHON_METHODS
7
+
8
+
9
+ class BlueskyEventStream(Container):
10
+ """
11
+ This encapsulates the data and metadata for one 'stream' in a Bluesky 'run'.
12
+
13
+ This adds for bluesky-specific conveniences to the standard client Container.
14
+ """
15
+
16
+ def __repr__(self):
17
+ return f"<{type(self).__name__} {set(self)!r} stream_name={self.metadata['stream_name']!r}>"
18
+
19
+ @property
20
+ def descriptors(self):
21
+ return self.metadata["descriptors"]
22
+
23
+ @property
24
+ def _descriptors(self):
25
+ # For backward-compatibility.
26
+ # We do not normally worry about backward-compatibility of _ methods, but
27
+ # for a time databroker.v2 *only* have _descriptors and not descriptros,
28
+ # and I know there is useer code that relies on that.
29
+ warnings.warn("Use .descriptors instead of ._descriptors.", stacklevel=2)
30
+ return self.descriptors
31
+
32
+ def __getattr__(self, key):
33
+ """
34
+ Let run.X be a synonym for run['X'] unless run.X already exists.
35
+
36
+ This behavior is the same as with pandas.DataFrame.
37
+ """
38
+ # The wisdom of this kind of "magic" is arguable, but we
39
+ # need to support it for backward-compatibility reasons.
40
+ if key in IPYTHON_METHODS:
41
+ raise AttributeError(key)
42
+ if key in self:
43
+ return self[key]
44
+ raise AttributeError(key)
45
+
46
+ def __dir__(self):
47
+ # Build a list of entries that are valid attribute names
48
+ # and add them to __dir__ so that they tab-complete.
49
+ tab_completable_entries = [
50
+ entry
51
+ for entry in self
52
+ if (entry.isidentifier() and (not keyword.iskeyword(entry)))
53
+ ]
54
+ return super().__dir__() + tab_completable_entries
55
+
56
+ def read(self, *args, **kwargs):
57
+ """
58
+ Shortcut for reading the 'data' (as opposed to timestamps or config).
59
+
60
+ That is:
61
+
62
+ >>> stream.read(...)
63
+
64
+ is equivalent to
65
+
66
+ >>> stream["data"].read(...)
67
+ """
68
+ return self["data"].read(*args, **kwargs)
69
+
70
+ def to_dask(self):
71
+ warnings.warn(
72
+ """Do not use this method.
73
+ Instead, set dask or when first creating the client, as in
74
+
75
+ >>> catalog = from_uri("...", "dask")
76
+
77
+ and then read() will return dask objects.""",
78
+ DeprecationWarning,
79
+ stacklevel=2,
80
+ )
81
+ return self.new_variation(
82
+ structure_clients=DEFAULT_STRUCTURE_CLIENT_DISPATCH["dask"]
83
+ ).read()
@@ -0,0 +1,147 @@
1
+ import json
2
+ import keyword
3
+ import warnings
4
+ from datetime import datetime
5
+
6
+ from tiled.client.container import Container
7
+ from tiled.client.utils import handle_error
8
+
9
+ from ._common import IPYTHON_METHODS
10
+ from .document import Start, Stop, Descriptor, EventPage, DatumPage, Resource
11
+
12
+
13
+ _document_types = {
14
+ "start": Start,
15
+ "stop": Stop,
16
+ "descriptor": Descriptor,
17
+ "event_page": EventPage,
18
+ "datum_page": DatumPage,
19
+ "resource": Resource,
20
+ }
21
+
22
+
23
+ class BlueskyRun(Container):
24
+ """
25
+ This encapsulates the data and metadata for one Bluesky 'run'.
26
+
27
+ This adds for bluesky-specific conveniences to the standard client Container.
28
+ """
29
+
30
+ def __repr__(self):
31
+ metadata = self.metadata
32
+ datetime_ = datetime.fromtimestamp(metadata["start"]["time"])
33
+ return (
34
+ f"<{type(self).__name__} "
35
+ f"{set(self)!r} "
36
+ f"scan_id={metadata['start'].get('scan_id', 'UNSET')!s} " # (scan_id is optional in the schema)
37
+ f"uid={metadata['start']['uid'][:8]!r} " # truncated uid
38
+ f"{datetime_.isoformat(sep=' ', timespec='minutes')}"
39
+ ">"
40
+ )
41
+
42
+ @property
43
+ def start(self):
44
+ """
45
+ The Run Start document. A convenience alias:
46
+
47
+ >>> run.start is run.metadata["start"]
48
+ True
49
+ """
50
+ return self.metadata["start"]
51
+
52
+ @property
53
+ def stop(self):
54
+ """
55
+ The Run Stop document. A convenience alias:
56
+
57
+ >>> run.stop is run.metadata["stop"]
58
+ True
59
+ """
60
+ return self.metadata["stop"]
61
+
62
+ @property
63
+ def v2(self):
64
+ return self
65
+
66
+ def documents(self, fill=False):
67
+ # For back-compat with v2:
68
+ if fill == "yes":
69
+ fill = True
70
+ elif fill == "no":
71
+ fill = False
72
+ elif fill == "delayed":
73
+ raise NotImplementedError("fill='delayed' is not supported")
74
+ else:
75
+ fill = bool(fill)
76
+ link = self.item["links"]["self"].replace("/metadata", "/documents", 1)
77
+ with self.context.http_client.stream(
78
+ "GET",
79
+ link,
80
+ params={"fill": fill},
81
+ headers={"Accept": "application/json-seq"},
82
+ ) as response:
83
+ if response.is_error:
84
+ response.read()
85
+ handle_error(response)
86
+ tail = ""
87
+ for chunk in response.iter_bytes():
88
+ for line in chunk.decode().splitlines(keepends=True):
89
+ if line[-1] == "\n":
90
+ item = json.loads(tail + line)
91
+ yield (item["name"], _document_types[item["name"]](item["doc"]))
92
+ tail = ""
93
+ else:
94
+ tail += line
95
+ if tail:
96
+ item = json.loads(tail)
97
+ yield (item["name"], _document_types[item["name"]](item["doc"]))
98
+
99
+ def __getattr__(self, key):
100
+ """
101
+ Let run.X be a synonym for run['X'] unless run.X already exists.
102
+
103
+ This behavior is the same as with pandas.DataFrame.
104
+ """
105
+ # The wisdom of this kind of "magic" is arguable, but we
106
+ # need to support it for backward-compatibility reasons.
107
+ if key in IPYTHON_METHODS:
108
+ raise AttributeError(key)
109
+ if key in self:
110
+ return self[key]
111
+ raise AttributeError(key)
112
+
113
+ def __dir__(self):
114
+ # Build a list of entries that are valid attribute names
115
+ # and add them to __dir__ so that they tab-complete.
116
+ tab_completable_entries = [
117
+ entry
118
+ for entry in self
119
+ if (entry.isidentifier() and (not keyword.iskeyword(entry)))
120
+ ]
121
+ return super().__dir__() + tab_completable_entries
122
+
123
+ def describe(self):
124
+ "For back-compat with intake-based BlueskyRun"
125
+ warnings.warn(
126
+ "This will be removed. Use .metadata directly instead of describe()['metadata'].",
127
+ DeprecationWarning,
128
+ )
129
+ return {"metadata": self.metadata}
130
+
131
+ def __call__(self):
132
+ warnings.warn(
133
+ "Do not call a BlueskyRun. For now this returns self, for "
134
+ "backward-compatibility. but it will be removed in a future "
135
+ "release.",
136
+ DeprecationWarning,
137
+ stacklevel=2,
138
+ )
139
+ return self
140
+
141
+ def read(self):
142
+ raise NotImplementedError(
143
+ "Reading any entire run is not supported. "
144
+ "Access a stream in this run and read that."
145
+ )
146
+
147
+ to_dask = read
@@ -0,0 +1,146 @@
1
+ import collections.abc
2
+ import numbers
3
+ import operator
4
+
5
+ from tiled.adapters.utils import IndexCallable
6
+ from tiled.client.container import Container
7
+ from tiled.client.utils import handle_error
8
+ from tiled.utils import safe_json_dump
9
+
10
+ from .queries import PartialUID, RawMongo, ScanID
11
+
12
+
13
+ class CatalogOfBlueskyRuns(Container):
14
+ """
15
+ This adds some bluesky-specific conveniences to the standard client Container.
16
+
17
+ >>> catalog.scan_id[1234] # scan_id lookup
18
+ >>> catalog.uid["9acjef"] # (partial) uid lookup
19
+ >>> catalog[1234] # automatically do scan_id lookup for positive integer
20
+ >>> catalog["9acjef"] # automatically do (partial) uid lookup for string
21
+ >>> catalog[-5] # automatically do catalog.values()[-N] for negative integer
22
+ """
23
+
24
+ def __init__(self, *args, **kwargs):
25
+ super().__init__(*args, **kwargs)
26
+ self.scan_id = IndexCallable(self._lookup_by_scan_id)
27
+ self.uid = IndexCallable(self._lookup_by_partial_uid)
28
+ self._v1 = None
29
+
30
+ def __repr__(self):
31
+ # This is a copy/paste of the general-purpose implementation
32
+ # tiled.adapters.utils.tree_repr
33
+ # with some modifications to extract scan_id from the metadata.
34
+ sample = self.items()[:10]
35
+ # Use scan_id (int) if defined; otherwise fall back to uid.
36
+ sample_reprs = [
37
+ repr(value.metadata["start"].get("scan_id", key)) for key, value in sample
38
+ ]
39
+ out = "<Catalog {"
40
+ # Always show at least one.
41
+ if sample_reprs:
42
+ out += sample_reprs[0]
43
+ # And then show as many more as we can fit on one line.
44
+ counter = 1
45
+ for sample_repr in sample_reprs[1:]:
46
+ if len(out) + len(sample_repr) > 60: # character count
47
+ break
48
+ out += ", " + sample_repr
49
+ counter += 1
50
+ approx_len = operator.length_hint(self) # cheaper to compute than len(node)
51
+ # Are there more in the node that what we displayed above?
52
+ if approx_len > counter:
53
+ out += f", ...}} ~{approx_len} entries>"
54
+ else:
55
+ out += "}>"
56
+ return out
57
+
58
+ @property
59
+ def v2(self):
60
+ return self
61
+
62
+ def __getitem__(self, key):
63
+ # For convenience and backward-compatiblity reasons, we support
64
+ # some "magic" here that is helpful in an interactive setting.
65
+ if isinstance(key, str):
66
+ # CASE 1: Interpret key as a uid or partial uid.
67
+ if len(key) == 36:
68
+ # This looks like a full uid. Try direct lookup first.
69
+ try:
70
+ return super().__getitem__(key)
71
+ except KeyError:
72
+ # Fall back to partial uid lookup below.
73
+ pass
74
+ return self._lookup_by_partial_uid(key)
75
+ elif isinstance(key, numbers.Integral):
76
+ if key > 0:
77
+ # CASE 2: Interpret key as a scan_id.
78
+ return self._lookup_by_scan_id(key)
79
+ else:
80
+ # CASE 3: Interpret key as a recently lookup, as in
81
+ # `catalog[-1]` is the latest entry.
82
+ key = int(key)
83
+ return self.values()[key]
84
+ elif isinstance(key, slice):
85
+ if (key.start is None) or (key.start >= 0):
86
+ raise ValueError(
87
+ "For backward-compatibility reasons, slicing here "
88
+ "is limited to negative indexes. "
89
+ "Use .values() to slice how you please."
90
+ )
91
+ return self.values()[key]
92
+ elif isinstance(key, collections.abc.Iterable):
93
+ # We know that isn't a str because we check that above.
94
+ # Recurse.
95
+ return [self[item] for item in key]
96
+ else:
97
+ raise ValueError(
98
+ "Indexing expects a string, an integer, or a collection of strings and/or integers."
99
+ )
100
+
101
+ def _lookup_by_scan_id(self, scan_id):
102
+ results = self.search(ScanID(scan_id, duplicates="latest"))
103
+ if not results:
104
+ raise KeyError(f"No match for scan_id={scan_id}")
105
+ else:
106
+ # By construction there must be only one result. Return it.
107
+ return results.values().first()
108
+
109
+ def _lookup_by_partial_uid(self, partial_uid):
110
+ results = self.search(PartialUID(partial_uid))
111
+ if not results:
112
+ raise KeyError(f"No match for partial_uid {partial_uid}")
113
+ else:
114
+ # By construction there must be only one result. Return it.
115
+ return results.values().first()
116
+
117
+ def get_serializer(self):
118
+ from tiled.server.app import get_root_tree
119
+
120
+ if not hasattr(self.context.http_client, "app"):
121
+ raise NotImplementedError("Only works on local application.")
122
+ tree = self.context.http_client.app.dependency_overrides[get_root_tree]()
123
+ return tree.get_serializer()
124
+
125
+ def search(self, query):
126
+ # For backward-compatiblity, accept a dict and interpret it as a Mongo
127
+ # query against the 'start' documents.
128
+ if isinstance(query, dict):
129
+ query = RawMongo(start=query)
130
+ return super().search(query)
131
+
132
+ @property
133
+ def v1(self):
134
+ "Accessor to legacy interface."
135
+ if self._v1 is None:
136
+ from databroker.v1 import Broker
137
+
138
+ self._v1 = Broker(self)
139
+ return self._v1
140
+
141
+ def post_document(self, name, doc):
142
+ link = self.item["links"]["self"].replace("/metadata", "/documents", 1)
143
+ response = self.context.http_client.post(
144
+ link, content=safe_json_dump({"name": name, "doc": doc})
145
+ )
146
+ handle_error(response)
@@ -0,0 +1,165 @@
1
+ import copy
2
+
3
+ from dask.base import normalize_token
4
+
5
+
6
+ class NotMutable(Exception):
7
+ pass
8
+
9
+
10
+ class Document(dict):
11
+ """
12
+ Document is an immutable dict subclass.
13
+
14
+ It is immutable to help consumer code avoid accidentally corrupting data
15
+ that another part of the cosumer code was expected to use unchanged.
16
+
17
+ Subclasses of Document must define __dask_tokenize__. The tokenization
18
+ schemes typically uniquely identify the document based on only a subset of
19
+ its contents, and mutating the contents can thereby create situations where
20
+ two unequal objects have colliding tokens. Immutability helps guard against
21
+ this too.
22
+
23
+ Note that Documents are not *recursively* immutable. Just as it is possible
24
+ create a tuple (immutable) of lists (mutable) and mutate the lists, it is
25
+ possible to mutate the internal contents of a Document, but this should not
26
+ be done. It is safer to use the to_dict() method to create a mutable deep
27
+ copy.
28
+
29
+ This is implemented as a dict subclass in order to satisfy certain
30
+ consumers that expect an object that satisfies isinstance(obj, dict).
31
+ This implementation detail may change in the future.
32
+ """
33
+
34
+ __slots__ = ("__not_a_real_dict",)
35
+
36
+ def __init__(self, *args, **kwargs):
37
+ super().__init__(*args, **kwargs)
38
+ # This lets pickle recognize that this is not a literal dict and that
39
+ # it should respect its custom __setstate__.
40
+ self.__not_a_real_dict = True
41
+
42
+ def __repr__(self):
43
+ # same as dict, but wrapped in the class name so the eval round-trips
44
+ return f"{self.__class__.__name__}({dict(self)})"
45
+
46
+ def _repr_pretty_(self, p, cycle):
47
+ """
48
+ A multi-line but eval-able text repr with readable indentation
49
+
50
+ This hooks into IPython/Jupyter's display mechanism
51
+ This is *not* invoked by print() or repr(), but it is invoked by
52
+ IPython.display.display() which is called in this common scenario::
53
+
54
+ In [1]: doc = Document(...)
55
+ In [2]: doc
56
+ <pretty representation will show here>
57
+ """
58
+ # Note: IPython's pretty-prettying mechanism is custom and complex.
59
+ # The `text` method used below is a direct and blunt way to engage it
60
+ # and seems widely used in the IPython code base. There are other
61
+ # specific mechanisms for displaying collections like dicts, but they
62
+ # can *truncate* which I think we want to avoid and they would require
63
+ # more investment to understand how to use.
64
+ from pprint import pformat
65
+
66
+ return p.text(f"{self.__class__.__name__}({pformat(dict(self))})")
67
+
68
+ def __getstate__(self):
69
+ return dict(self)
70
+
71
+ def __setstate__(self, state):
72
+ dict.update(self, state)
73
+ self.__not_a_real_dict = True
74
+
75
+ def __readonly(self, *args, **kwargs):
76
+ raise NotMutable(
77
+ "Documents are not mutable. Call the method to_dict() to make a "
78
+ "fully independent and mutable deep copy."
79
+ )
80
+
81
+ def __setitem__(self, key, value):
82
+ try:
83
+ self.__not_a_real_dict
84
+ except AttributeError:
85
+ # This path is necessary to support un-pickling.
86
+ return dict.__setitem__(self, key, value)
87
+ else:
88
+ self.__readonly()
89
+
90
+ __delitem__ = __readonly
91
+ pop = __readonly
92
+ popitem = __readonly
93
+ clear = __readonly
94
+ setdefault = __readonly
95
+ update = __readonly
96
+
97
+ def to_dict(self):
98
+ """
99
+ Create a mutable deep copy.
100
+ """
101
+ # Convert to dict and then make a deep copy to ensure that if the user
102
+ # mutates any internally nested dicts there is no spooky action at a
103
+ # distance.
104
+ return copy.deepcopy(dict(self))
105
+
106
+ def __deepcopy__(self, memo):
107
+ # Without this, copy.deepcopy(Document(...)) fails because deepcopy
108
+ # creates a new, empty Document instance and then tries to add items to
109
+ # it.
110
+ return self.__class__({k: copy.deepcopy(v, memo) for k, v in self.items()})
111
+
112
+ def __dask_tokenize__(self):
113
+ raise NotImplementedError
114
+
115
+
116
+ # We must use dask's registration mechanism to tell it to treat Document
117
+ # specially. Dask's tokenization dispatch mechanism discovers that Docuemnt is
118
+ # a dict subclass and treats it as a dict, ignoring its __dask_tokenize__
119
+ # method. To force it to respect our cutsom tokenization, we must explicitly
120
+ # register it.
121
+
122
+
123
+ @normalize_token.register(Document)
124
+ def tokenize_document(instance):
125
+ return instance.__dask_tokenize__()
126
+
127
+
128
+ class Start(Document):
129
+ def __dask_tokenize__(self):
130
+ return ("start", self["uid"])
131
+
132
+
133
+ class Stop(Document):
134
+ def __dask_tokenize__(self):
135
+ return ("stop", self["uid"])
136
+
137
+
138
+ class Resource(Document):
139
+ def __dask_tokenize__(self):
140
+ return ("resource", self["uid"])
141
+
142
+
143
+ class Descriptor(Document):
144
+ def __dask_tokenize__(self):
145
+ return ("descriptor", self["uid"])
146
+
147
+
148
+ class Event(Document):
149
+ def __dask_tokenize__(self):
150
+ return ("event", self["uid"])
151
+
152
+
153
+ class EventPage(Document):
154
+ def __dask_tokenize__(self):
155
+ return ("event_page", self["uid"])
156
+
157
+
158
+ class Datum(Document):
159
+ def __dask_tokenize__(self):
160
+ return ("datum", self["datum_id"])
161
+
162
+
163
+ class DatumPage(Document):
164
+ def __dask_tokenize__(self):
165
+ return ("datum_page", self["uid"])
@@ -0,0 +1,305 @@
1
+ from dataclasses import asdict, dataclass
2
+ import enum
3
+ import warnings
4
+ from typing import List, Optional
5
+
6
+ # Not all of these are used, but import them all
7
+ # for user convenience so everything can be imported from bluesky_tiled_plugins.queries
8
+ from tiled.queries import ( # noqa: F401
9
+ Comparison,
10
+ Contains,
11
+ Eq,
12
+ FullText,
13
+ In,
14
+ Key,
15
+ NotEq,
16
+ NotIn,
17
+ Operator,
18
+ QueryValueError,
19
+ Regex,
20
+ )
21
+ from tiled.query_registration import register
22
+
23
+
24
+ class Duplicates(str, enum.Enum):
25
+ latest = "latest"
26
+ all = "all"
27
+ error = "error"
28
+
29
+
30
+ @register(name="scan_id")
31
+ @dataclass
32
+ class _ScanID:
33
+ """
34
+ Find matches to scan_id(s).
35
+ """
36
+
37
+ scan_ids: List[int]
38
+ duplicates: Duplicates
39
+
40
+ def __init__(self, *, scan_ids, duplicates):
41
+ self.scan_ids = scan_ids
42
+ self.duplicates = Duplicates(duplicates)
43
+
44
+ def encode(self):
45
+ return {
46
+ "scan_ids": ",".join(str(scan_id) for scan_id in self.scan_ids),
47
+ "duplicates": self.duplicates.value,
48
+ }
49
+
50
+ @classmethod
51
+ def decode(cls, *, scan_ids, duplicates):
52
+ return cls(
53
+ scan_ids=[int(scan_id) for scan_id in scan_ids.split(",")],
54
+ duplicates=Duplicates(duplicates),
55
+ )
56
+
57
+
58
+ def ScanID(*scan_ids, duplicates="latest"):
59
+ # Wrap _ScanID to provide a nice usage for *one or more scan_ids*:
60
+ # >>> ScanID(5)
61
+ # >>> ScanID(5, 6, 7)
62
+ # Placing a varargs parameter (*scan_ids) in the dataclass constructor
63
+ # would cause trouble on the server side and generally feels "wrong"
64
+ # so we have this wrapper function instead.
65
+ return _ScanID(scan_ids=scan_ids, duplicates=duplicates)
66
+
67
+
68
+ @register(name="scan_id_range")
69
+ @dataclass
70
+ class ScanIDRange:
71
+ """
72
+ Find scans in the range.
73
+ """
74
+
75
+ start_id: int
76
+ end_id: int
77
+ duplicates: Duplicates
78
+
79
+ def __init__(self, start_id, end_id, duplicates="latest"):
80
+ self.start_id = start_id
81
+ self.end_id = end_id
82
+ self.duplicates = Duplicates(duplicates)
83
+
84
+ def encode(self):
85
+ return {
86
+ "start_id": self.start_id,
87
+ "end_id": self.end_id,
88
+ "duplicates": self.duplicates.value,
89
+ }
90
+
91
+ @classmethod
92
+ def decode(cls, *, start_id, end_id, duplicates="latest"):
93
+ return cls(
94
+ start_id=int(start_id),
95
+ end_id=int(end_id),
96
+ duplicates=Duplicates(duplicates),
97
+ )
98
+
99
+
100
+ @register(name="partial_uid")
101
+ @dataclass
102
+ class _PartialUID:
103
+ """
104
+ Find matches to (partial) uid(s).
105
+ """
106
+
107
+ partial_uids: List[str]
108
+
109
+ def encode(self):
110
+ return {"partial_uids": ",".join(str(uid) for uid in self.partial_uids)}
111
+
112
+ @classmethod
113
+ def decode(cls, *, partial_uids):
114
+ return cls(partial_uids=partial_uids.split(","))
115
+
116
+
117
+ def PartialUID(*partial_uids):
118
+ # See comment above with ScanID and _ScanID. Same thinking here.
119
+ return _PartialUID(partial_uids)
120
+
121
+
122
+ @register(name="duration")
123
+ @dataclass
124
+ class Duration:
125
+ """
126
+ Run a MongoDB query against a given collection.
127
+ """
128
+
129
+ less_than: float
130
+ greater_than: float
131
+
132
+
133
+ def RawMongo(start):
134
+ """
135
+ DEPRECATED
136
+
137
+ Raw MongoDB queries are no longer supported. If it is possible to express
138
+ the import as a supported query, we transform it and warn. If not, we raise
139
+ an error.
140
+ """
141
+
142
+ if len(start) == 1:
143
+ ((key, value),) = start.items()
144
+ if not isinstance(value, dict):
145
+ # We can transform this into a simple query.
146
+ warnings.warn(
147
+ """RawMongo will not be supported
148
+ in a future release of databroker, and its functionality has been limited.
149
+ Instead, use:
150
+
151
+ Key("{key}") == {value!r}
152
+ """
153
+ )
154
+ return Key(key) == value
155
+ raise ValueError(
156
+ """Arbitrary MongoDB queries no longer supported.
157
+
158
+ If this is critical to you, please open an issue at
159
+
160
+ https://github.com/bluesky/databroker
161
+
162
+ describing your use case and we will see what we can work out."""
163
+ )
164
+
165
+
166
+ # human friendly timestamp formats we'll parse
167
+ _TS_FORMATS = [
168
+ "%Y-%m-%d %H:%M:%S",
169
+ "%Y-%m-%d %H:%M", # these 2 are not as originally doc'd,
170
+ "%Y-%m-%d %H", # but match previous pandas behavior
171
+ "%Y-%m-%d",
172
+ "%Y-%m",
173
+ "%Y",
174
+ ]
175
+
176
+
177
+ def _normalize_human_friendly_time(val, tz):
178
+ """Given one of :
179
+ - string (in one of the formats below)
180
+ - datetime (eg. datetime.now()), with or without tzinfo)
181
+ - timestamp (eg. time.time())
182
+ return a timestamp (seconds since jan 1 1970 UTC).
183
+
184
+ Non string/datetime values are returned unaltered.
185
+ Leading/trailing whitespace is stripped.
186
+ Supported formats:
187
+ {}
188
+ """
189
+ # {} is placeholder for formats; filled in after def...
190
+
191
+ import pytz
192
+ from datetime import datetime
193
+
194
+ zone = pytz.timezone(tz) # tz as datetime.tzinfo object
195
+ epoch = pytz.UTC.localize(datetime(1970, 1, 1))
196
+ check = True
197
+
198
+ if isinstance(val, str):
199
+ # unix 'date' cmd format '%a %b %d %H:%M:%S %Z %Y' works but
200
+ # doesn't get TZ?
201
+
202
+ # Could cleanup input a bit? remove leading/trailing [ :,-]?
203
+ # Yes, leading/trailing whitespace to match pandas behavior...
204
+ # Actually, pandas doesn't ignore trailing space, it assumes
205
+ # the *current* month/day if they're missing and there's
206
+ # trailing space, or the month is a single, non zero-padded digit.?!
207
+ val = val.strip()
208
+
209
+ for fmt in _TS_FORMATS:
210
+ try:
211
+ ts = datetime.strptime(val, fmt)
212
+ break
213
+ except ValueError:
214
+ pass
215
+
216
+ try:
217
+ if isinstance(ts, datetime):
218
+ val = ts
219
+ check = False
220
+ else:
221
+ # what else could the type be here?
222
+ raise TypeError("expected datetime," " got {:r}".format(ts))
223
+
224
+ except NameError:
225
+ raise ValueError("failed to parse time: " + repr(val))
226
+
227
+ if check and not isinstance(val, datetime):
228
+ return val
229
+
230
+ if val.tzinfo is None:
231
+ # is_dst=None raises NonExistent and Ambiguous TimeErrors
232
+ # when appropriate, same as pandas
233
+ val = zone.localize(val, is_dst=None)
234
+
235
+ return (val - epoch).total_seconds()
236
+
237
+
238
+ @register(name="time_range")
239
+ @dataclass
240
+ class TimeRange:
241
+ """
242
+ A search query representing a time range.
243
+
244
+ Parameters
245
+ ----------
246
+ since, until: dates gives as timestamp, datetime, or human-friendly string, optional
247
+ timezone : string
248
+ As in, 'US/Eastern'. If None is given, tzlocal is used.
249
+
250
+ Examples
251
+ --------
252
+ Any granularity (year, month, date, hour, minute, second) is accepted.
253
+
254
+ >>> TimeRange(since='2014')
255
+
256
+ >>> TimeRange(until='2019-07')
257
+
258
+ >>> TimeRange(since='2014-07-04', until='2020-07-04')
259
+
260
+ >>> TimeRange(since='2014-07-04 05:00')
261
+
262
+ """
263
+
264
+ timezone: str
265
+ since: Optional[float] = None
266
+ until: Optional[float] = None
267
+
268
+ def __init__(self, *, timezone=None, since=None, until=None):
269
+ # Stash the raw values just for use in the repr.
270
+ self._raw_since = since
271
+ self._raw_until = until
272
+
273
+ if timezone is None:
274
+ import tzlocal
275
+
276
+ lz = tzlocal.get_localzone()
277
+ try:
278
+ timezone = lz.key
279
+ except AttributeError:
280
+ timezone = lz.zone
281
+ self.timezone = timezone
282
+ if since is None:
283
+ self.since = None
284
+ else:
285
+ self.since = _normalize_human_friendly_time(since, tz=self.timezone)
286
+ if until is None:
287
+ self.until = None
288
+ else:
289
+ self.until = _normalize_human_friendly_time(until, tz=self.timezone)
290
+ if since is not None and until is not None:
291
+ if self.since > self.until:
292
+ raise ValueError("since must not be greater than until.")
293
+
294
+ def __repr__(self):
295
+ return (
296
+ f"{type(self).__name__!s}("
297
+ f"timezone={self.timezone!r}, since={self._raw_since!r}, until={self._raw_until!r})"
298
+ )
299
+
300
+ def encode(self):
301
+ return asdict(self)
302
+
303
+ @classmethod
304
+ def decode(cls, *, timezone, since=None, until=None):
305
+ return cls(timezone=timezone, since=since, until=until)
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-vcs"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bluesky-tiled-plugins"
7
+ description = "Tiled client plugins to provide an customized user experience for Bluesky data in Tiled"
8
+ readme = { file = "README.md", content-type = "text/markdown" }
9
+ authors = [
10
+ { name = "Bluesky Project Contributors", email = "dallan@bnl.gov" },
11
+ ]
12
+ maintainers = [
13
+ { name = "Brookhaven National Laboratory", email = "dallan@bnl.gov" },
14
+ ]
15
+ requires-python = ">=3.7"
16
+
17
+ dependencies = [
18
+ "dask",
19
+ "mongoquery",
20
+ "pytz",
21
+ "tiled[client] >=0.1.0b4",
22
+ "tzlocal",
23
+ ]
24
+
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "License :: OSI Approved :: BSD License",
28
+ "Programming Language :: Python :: 3 :: Only",
29
+ "Programming Language :: Python :: 3.7",
30
+ "Programming Language :: Python :: 3.8",
31
+ "Programming Language :: Python :: 3.9",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Topic :: Scientific/Engineering :: Physics",
36
+ ]
37
+
38
+ dynamic = ["version"]
39
+
40
+ [project.entry-points."tiled.special_client"]
41
+ CatalogOfBlueskyRuns = "bluesky_tiled_plugins.catalog_of_bluesky_runs:CatalogOfBlueskyRuns"
42
+ BlueskyRun = "bluesky_tiled_plugins.bluesky_run:BlueskyRun"
43
+ BlueskyEventStream = "bluesky_tiled_plugins.bluesky_event_stream:BlueskyEventStream"
44
+
45
+ [tool.hatch]
46
+ version.source = "vcs"
47
+ version.raw-options = { root = ".." }
48
+ version.fallback-version = "0.0.0"
49
+ build.hooks.vcs.version-file = "bluesky_tiled_plugins/_version.py"
50
+