Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Factor out bluesky-tiled-plugins package #814

Merged
merged 13 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .git_archival.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node: $Format:%H$
node-date: $Format:%cI$
describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
ref-names: $Format:%D$
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
databroker/_version.py export-subst
.git_archival.txt export-subst
62 changes: 38 additions & 24 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
@@ -1,33 +1,47 @@
# This workflows will upload a Python Package using flit when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries

name: Upload Python Package
name: CD

on:
workflow_dispatch:
pull_request:
push:
branches:
- main
release:
types: [created]
types:
- published

jobs:
deploy:
dist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

# Build two packages: databroker and bluesky-tiled-plugins.

- uses: hynek/build-and-inspect-python-package@v2
with:
path: .
upload-name-suffix: "-databroker"

- uses: hynek/build-and-inspect-python-package@v2
with:
path: bluesky-tiled-plugins/
upload-name-suffix: "-bluesky-tiled-plugins"

publish:
needs: [dist]
environment: pypi
permissions:
id-token: write
runs-on: ubuntu-latest
if: github.event_name == 'release' && github.event.action == 'published'

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install wheel twine setuptools
- name: Build and publish
env:
TWINE_USERNAME: __token__
# The PYPI_PASSWORD must be a pypi token with the "pypi-" prefix with sufficient permissions to upload this package
# https://pypi.org/help/#apitoken
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*
- uses: actions/download-artifact@v4
with:
name: Packages
path: dist

- uses: pypa/gh-action-pypi-publish@release/v1
10 changes: 10 additions & 0 deletions bluesky-tiled-plugins/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# bluesky-tiled-plugins

This is a separate Python package, `bluesky-tiled-plugins`, that is
developed in the databroker repository.

For a user wishing to connect to a running Tiled server and access Bluesky data,
this package, along with its dependency `tiled[client]`, is all they need.

The databroker package is only required if the user wants to use the legacy
`databroker.Broker` API.
3 changes: 3 additions & 0 deletions bluesky-tiled-plugins/bluesky_tiled_plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .catalog_of_bluesky_runs import CatalogOfBlueskyRuns # noqa: F401
from .bluesky_event_stream import BlueskyEventStream # noqa: F401
from .bluesky_run import BlueskyRun # noqa: F401
6 changes: 6 additions & 0 deletions bluesky-tiled-plugins/bluesky_tiled_plugins/_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# There are methods that IPython will try to call.
# We special-case them because we want to avoid the getattr
# resulting in an unnecessary network hit just to raise
# AttributeError.

IPYTHON_METHODS = {"_ipython_canary_method_should_not_exist_", "_repr_mimebundle_"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import keyword
import warnings

from tiled.client.container import DEFAULT_STRUCTURE_CLIENT_DISPATCH, Container

from ._common import IPYTHON_METHODS


class BlueskyEventStream(Container):
"""
This encapsulates the data and metadata for one 'stream' in a Bluesky 'run'.

This adds for bluesky-specific conveniences to the standard client Container.
"""

def __repr__(self):
return f"<{type(self).__name__} {set(self)!r} stream_name={self.metadata['stream_name']!r}>"

@property
def descriptors(self):
return self.metadata["descriptors"]

@property
def _descriptors(self):
# For backward-compatibility.
# We do not normally worry about backward-compatibility of _ methods, but
# for a time databroker.v2 *only* have _descriptors and not descriptros,
# and I know there is useer code that relies on that.
warnings.warn("Use .descriptors instead of ._descriptors.", stacklevel=2)
return self.descriptors

def __getattr__(self, key):
"""
Let run.X be a synonym for run['X'] unless run.X already exists.

This behavior is the same as with pandas.DataFrame.
"""
# The wisdom of this kind of "magic" is arguable, but we
# need to support it for backward-compatibility reasons.
if key in IPYTHON_METHODS:
raise AttributeError(key)
if key in self:
return self[key]
raise AttributeError(key)

def __dir__(self):
# Build a list of entries that are valid attribute names
# and add them to __dir__ so that they tab-complete.
tab_completable_entries = [
entry
for entry in self
if (entry.isidentifier() and (not keyword.iskeyword(entry)))
]
return super().__dir__() + tab_completable_entries

def read(self, *args, **kwargs):
"""
Shortcut for reading the 'data' (as opposed to timestamps or config).

That is:

>>> stream.read(...)

is equivalent to

>>> stream["data"].read(...)
"""
return self["data"].read(*args, **kwargs)

def to_dask(self):
warnings.warn(
"""Do not use this method.
Instead, set dask or when first creating the client, as in

>>> catalog = from_uri("...", "dask")

and then read() will return dask objects.""",
DeprecationWarning,
stacklevel=2,
)
return self.new_variation(
structure_clients=DEFAULT_STRUCTURE_CLIENT_DISPATCH["dask"]
).read()
147 changes: 147 additions & 0 deletions bluesky-tiled-plugins/bluesky_tiled_plugins/bluesky_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import json
import keyword
import warnings
from datetime import datetime

from tiled.client.container import Container
from tiled.client.utils import handle_error

from ._common import IPYTHON_METHODS
from .document import Start, Stop, Descriptor, EventPage, DatumPage, Resource


_document_types = {
"start": Start,
"stop": Stop,
"descriptor": Descriptor,
"event_page": EventPage,
"datum_page": DatumPage,
"resource": Resource,
}


class BlueskyRun(Container):
"""
This encapsulates the data and metadata for one Bluesky 'run'.

This adds for bluesky-specific conveniences to the standard client Container.
"""

def __repr__(self):
metadata = self.metadata
datetime_ = datetime.fromtimestamp(metadata["start"]["time"])
return (
f"<{type(self).__name__} "
f"{set(self)!r} "
f"scan_id={metadata['start'].get('scan_id', 'UNSET')!s} " # (scan_id is optional in the schema)
f"uid={metadata['start']['uid'][:8]!r} " # truncated uid
f"{datetime_.isoformat(sep=' ', timespec='minutes')}"
">"
)

@property
def start(self):
"""
The Run Start document. A convenience alias:

>>> run.start is run.metadata["start"]
True
"""
return self.metadata["start"]

@property
def stop(self):
"""
The Run Stop document. A convenience alias:

>>> run.stop is run.metadata["stop"]
True
"""
return self.metadata["stop"]

@property
def v2(self):
return self

def documents(self, fill=False):
# For back-compat with v2:
if fill == "yes":
fill = True
elif fill == "no":
fill = False
elif fill == "delayed":
raise NotImplementedError("fill='delayed' is not supported")
else:
fill = bool(fill)
link = self.item["links"]["self"].replace("/metadata", "/documents", 1)
with self.context.http_client.stream(
"GET",
link,
params={"fill": fill},
headers={"Accept": "application/json-seq"},
) as response:
if response.is_error:
response.read()
handle_error(response)
tail = ""
for chunk in response.iter_bytes():
for line in chunk.decode().splitlines(keepends=True):
if line[-1] == "\n":
item = json.loads(tail + line)
yield (item["name"], _document_types[item["name"]](item["doc"]))
tail = ""
else:
tail += line
if tail:
item = json.loads(tail)
yield (item["name"], _document_types[item["name"]](item["doc"]))

def __getattr__(self, key):
"""
Let run.X be a synonym for run['X'] unless run.X already exists.

This behavior is the same as with pandas.DataFrame.
"""
# The wisdom of this kind of "magic" is arguable, but we
# need to support it for backward-compatibility reasons.
if key in IPYTHON_METHODS:
raise AttributeError(key)
if key in self:
return self[key]
raise AttributeError(key)

def __dir__(self):
# Build a list of entries that are valid attribute names
# and add them to __dir__ so that they tab-complete.
tab_completable_entries = [
entry
for entry in self
if (entry.isidentifier() and (not keyword.iskeyword(entry)))
]
return super().__dir__() + tab_completable_entries

def describe(self):
"For back-compat with intake-based BlueskyRun"
warnings.warn(
"This will be removed. Use .metadata directly instead of describe()['metadata'].",
DeprecationWarning,
)
return {"metadata": self.metadata}

def __call__(self):
warnings.warn(
"Do not call a BlueskyRun. For now this returns self, for "
"backward-compatibility. but it will be removed in a future "
"release.",
DeprecationWarning,
stacklevel=2,
)
return self

def read(self):
raise NotImplementedError(
"Reading any entire run is not supported. "
"Access a stream in this run and read that."
)

to_dask = read
Loading
Loading