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.
- bluesky_tiled_plugins-2.0.0b54/.gitignore +140 -0
- bluesky_tiled_plugins-2.0.0b54/PKG-INFO +34 -0
- bluesky_tiled_plugins-2.0.0b54/README.md +10 -0
- bluesky_tiled_plugins-2.0.0b54/bluesky_tiled_plugins/__init__.py +3 -0
- bluesky_tiled_plugins-2.0.0b54/bluesky_tiled_plugins/_common.py +6 -0
- bluesky_tiled_plugins-2.0.0b54/bluesky_tiled_plugins/_version.py +16 -0
- bluesky_tiled_plugins-2.0.0b54/bluesky_tiled_plugins/bluesky_event_stream.py +83 -0
- bluesky_tiled_plugins-2.0.0b54/bluesky_tiled_plugins/bluesky_run.py +147 -0
- bluesky_tiled_plugins-2.0.0b54/bluesky_tiled_plugins/catalog_of_bluesky_runs.py +146 -0
- bluesky_tiled_plugins-2.0.0b54/bluesky_tiled_plugins/document.py +165 -0
- bluesky_tiled_plugins-2.0.0b54/bluesky_tiled_plugins/queries.py +305 -0
- bluesky_tiled_plugins-2.0.0b54/pyproject.toml +50 -0
|
@@ -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,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
|
+
|