From 8267c8365353433cc9516477e919ffe211663ce7 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Sun, 8 Nov 2026 22:06:38 +0000 Subject: [PATCH 1/4] Add documentation for using Tiled at Diamond Cover getting started, searching with Tiled queries and Glazed, live streaming, NeXus file creation, the current and central authorization policies, and the per-beamline deployment architecture. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 + docs/README.md | 108 ++++++++++ docs/architecture.md | 109 ++++++++++ docs/authorization.md | 191 +++++++++++++++++ docs/images/deployment-architecture.png | Bin 0 -> 126236 bytes docs/nexus.md | 216 +++++++++++++++++++ docs/searching.md | 271 ++++++++++++++++++++++++ docs/streaming.md | 154 ++++++++++++++ 8 files changed, 1052 insertions(+) create mode 100644 docs/README.md create mode 100644 docs/architecture.md create mode 100644 docs/authorization.md create mode 100644 docs/images/deployment-architecture.png create mode 100644 docs/nexus.md create mode 100644 docs/searching.md create mode 100644 docs/streaming.md diff --git a/README.md b/README.md index 3e1ea09..4ca1c85 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ Source | Docker | `docker run ghcr.io/diamondlightsource/dls-tiled:latest` Releases | +For how to use Tiled at Diamond (searching, streaming, NeXus files, access +control and the deployment architecture), see the [documentation](docs/README.md). + ## Plugins Plugins are FastAPI routers. Enable one by listing it under `routers:` in the diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..7f7dd72 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,108 @@ +# Using Tiled at Diamond + +[Tiled](https://blueskyproject.io/tiled/) is the data access service for scans +run with Bluesky at Diamond. It stores each scan's metadata and tabular data, knows +where its detector files are, and serves all of it over HTTP, with access +checked against your Diamond login. + +| I want to... | Read | +| -------------------------------------------------- | --------------------------------------------------------------------- | +| Find a scan and read its data from Python | [Getting started](#getting-started), [Searching](searching.md) | +| Query Tiled from a web app or with GraphQL | [Glazed](searching.md#graphql-with-glazed) | +| Follow a scan live while it runs | [Streaming](streaming.md) | +| Produce a NeXus file for a scan | [NeXus files](nexus.md) | +| Understand who can see what | [Authorization](authorization.md) | +| Understand how the service is deployed | [Architecture](architecture.md) | +| Report that data can't be read | [Getting help](#getting-help) | + +## Getting started + +Install the client. `bluesky-tiled-plugins` gives you Bluesky-aware objects for +runs and event streams, and the live streaming API: + +```sh +pip install "tiled[client]" bluesky-tiled-plugins +``` + +Connect. The first time, Tiled opens a browser so you can log in with your +Diamond account. Tokens are then cached in `~/.cache/tiled/tokens`. + +```python +from tiled.client import from_uri + +client = from_uri("https://tiled.diamond.ac.uk") +``` + +### How the data is organised + +Tiled is a tree. Runs are written by blueapi under the visit they belong to, so +you normally index straight to your visit: + +``` +///// +p51/cm44254/cm44254-1/9a812aec-b092-4fb4-9c8e-c58beac77fdc/primary/panda1-counter-1-out_dataset +``` + +```python +visit = client["p51/cm44254/cm44254-1"] +``` + +> **Changing soon.** The [central authorization work](authorization.md#new-central-authorization) +> removes the proposal level and adds folders per visit, so the same visit will +> be at `p51/cm44254-1/raw`. Code that builds these paths will need updating +> when that is released. + +### Reading a run + +```python +run = visit["9a812aec-b092-4fb4-9c8e-c58beac77fdc"] + +run.metadata["start"] # the Bluesky start document +run.metadata.get("stop") # the stop document; missing while the scan runs +list(run) # stream names, e.g. ["primary", "baseline"] + +ds = run["primary"].read() # the whole stream as an xarray.Dataset +values = run["primary"]["panda1-counter-1-out_dataset"].read() # one signal +``` + +Detector data can be many gigabytes. Slice arrays to read only what you need: + +```python +images = run["primary"]["detector"] # nothing is downloaded yet +first_frame = images[0] +``` + +### Common use cases + +- **Find scans in a visit** by plan, scan number, outcome or time: + [Searching](searching.md). +- **Analysis scripts** read arrays and tables as NumPy, pandas or xarray objects, + without needing to know file paths. +- **Live displays** get each point as it is taken: [Streaming](streaming.md). +- **Web applications** use the GraphQL interface, [Glazed](searching.md#graphql-with-glazed), + or the REST API. Tiled's interactive API docs are at `/docs` on the server. +- **ViSR imaging** has a server-side binning endpoint, described in the + [project README](../README.md#dls_tiledvisrvisr_router). +- **NeXus files** can be generated from a run: [NeXus files](nexus.md). + +## Getting help + +### If you can't read data for a beamline + +Create a ticket in the +[ACQP Jira project](https://jira.diamond.ac.uk/projects/ACQP/issues/). Include: + +- the beamline and visit, for example `p51` and `cm44254-1` +- the full node path, or the run's uid +- what you ran, the full error, and roughly when it happened +- your FedID, so access can be checked + +Before raising a ticket, these symptoms can help you tell what is wrong: + +| Symptom | Likely cause | +| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `401 Unauthorized` | You are not logged in, or your token has expired. Connect again. | +| The node isn't there (`KeyError`, `404`) | You don't have access to that visit, or the path is wrong. If you were only just added to the visit, wait a minute: Tiled remembers your permissions for up to 60 seconds. | +| The metadata reads, but reading the data gives a `500` | Usually a problem with the file, not your permissions: Tiled can't open it, or a dataset is missing from it. The beamline's pod may also lack filesystem access to it. Quote the error's correlation ID in the ticket if you have it. | +| `503` with `Retry-After` | The file is still catching up with the catalog during a scan. Try again shortly. | +| A whole beamline's data is unreadable | The beamline may not have its own Tiled deployment or data ACL yet ([why](architecture.md#adding-a-beamline)). Raise a ticket. | diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..e6f55dc --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,109 @@ +# Architecture + +Tiled at Diamond runs as one Kubernetes Deployment **per beamline**, all sharing +a single database. Each beamline's pod can read only that beamline's data on +disk. + +![Requests to tiled.diamond.ac.uk are routed by beamline to a dedicated Service, Deployment and pod. Every pod shares tiled-database, and each beamline pod has its own data ACL on GPFS.](images/deployment-architecture.png) + +## Why one deployment per beamline + +Reading a detector file is a filesystem operation, so it runs as whichever POSIX +user the pod runs as. Ideally that would be the user making the request, and the +POSIX UID is available from the access token when the request includes the +`posix-uid` scope. But a pod's POSIX user is fixed when the pod starts, so +acting as each user would need a separate pod per user, which is excessive for a +REST API. + +Instead, there is a persistent deployment for each beamline: + +- It runs as the POSIX user `k8s-ixx-beamline`. +- It has read-only access to `/dls/ixx/data` through the `ixx_data` ACL. + +**The benefit is defence in depth.** If the authorization layer ever failed to +enforce access correctly, a request would still be limited by the filesystem +ACLs of the pod it reached. A request routed to `p99` can't read `p51`'s files, +whatever the access policy decides. + +**The cost is complexity.** There are many deployments to run and upgrade, and +routing has to keep requests and data on the matching beamline. + +## Request routing + +A request is routed on the **beamline, the first segment of the Tiled path**: + +``` +https://tiled.diamond.ac.uk/api/v1/metadata/p99/cm12345/cm12345-1/... + │ + ▼ + k8s-p99-beamline +``` + +The diagram shows this as the ingress routing directly. In the chart there is an +extra hop, because cluster policy (Kyverno) forbids regex paths on Ingress +resources: + +``` +Ingress (a single "/" path) → Envoy → the beamline's Service → the beamline's pod +``` + +Envoy forwards the request unmodified. It only matches the Tiled endpoints that +address a node by path, listed in `envoy.pathPrefixes` in +[`values.yaml`](../Charts/tiled/values.yaml): + +| Endpoints | +| --------------------------------------------------------------------------------------------- | +| `api/v1/search`, `distinct`, `metadata`, `register`, `data_source`, `revisions` | +| `api/v1/array`, `ragged` (`block` and `full`) | +| `api/v1/table/partition`, `table/full`, `container/full`, `node/full` | +| `api/v1/awkward/...`, `asset/bytes`, `asset/manifest` | +| `api/v1/stream/single`, `stream/close` (WebSockets) | +| `api/v1/binned`, `custom/binned`, `zarr/v2`, `zarr/v3` | + +Anything else (the web UI at `/`, `/api/v1/auth/*`, a search of the root node) +goes to **`tiled-default`**. A new plugin router that addresses nodes by path has +to be added to `pathPrefixes`, or its requests will reach `tiled-default`, which +has no beamline's data ACL. + +## Shared services + +| Service | Purpose | +| --------------------- | ----------------------------------------------------------------------------------------------------------- | +| PostgreSQL | The `tiled` catalog (the tree and all metadata), `auth`, and `data` (tabular event data). Shared by all pods. | +| GPFS | Detector files under `/dls//data`, mounted per beamline and read-only. | +| Open Policy Agent | Makes the access decisions. See [Authorization](authorization.md). | +| Redis | Tiled's streaming cache, which live [streaming](streaming.md) requires. | +| Glazed | The [GraphQL interface](searching.md#graphql-with-glazed), a separate service in front of Tiled. | +| oauth2-proxy | Sits in front of Tiled and Glazed, and sends anonymous browsers to log in. | + +Because the catalog is shared, any pod can answer metadata queries for any +node. Only opening data files depends on reaching the right pod. + +## Adding a beamline + +Add an entry to `beamlines` in the Helm values with the POSIX group and data +mount. The chart creates its Deployment, its Service and its Envoy route: + +```yaml +beamlines: + - name: default + - name: p51 + securityContext: + supplementalGroups: [12345] # the p51_data group + volumes: + - name: data + hostPath: {path: /dls/p51/data} + volumeMounts: + - name: data + mountPath: /dls/p51/data + readOnly: true +``` + +`name` must be the exact first segment of the beamline's Tiled path. Until a +beamline has an entry, its requests go to `tiled-default`: metadata still works, +but reading its files fails. To get a beamline added, raise a ticket in +[ACQP](https://jira.diamond.ac.uk/projects/ACQP/issues/). + +Tokens issued by one pod must verify on the others, because all authentication +traffic goes to `tiled-default`. If Tiled issues its own tokens, share +`secret_keys` between the pods, as described in the comments in `values.yaml`. diff --git a/docs/authorization.md b/docs/authorization.md new file mode 100644 index 0000000..e45812e --- /dev/null +++ b/docs/authorization.md @@ -0,0 +1,191 @@ +# Authorization + +Three separate layers control access to data: + +1. **Authentication.** You log in with your Diamond account through Keycloak. + Services use a Keycloak service account (client credentials). +2. **Access policy.** Tiled asks [Open Policy Agent](https://www.openpolicyagent.org/) + (OPA) what your token may see and do, using + [`dls_tiled.policy`](../src/dls_tiled/policy.py). +3. **Filesystem ACLs.** Each beamline's pod can only read that beamline's files. + See [Architecture](architecture.md#why-one-deployment-per-beamline). + +This page describes the policy that is [running now](#current-policy) and the +[central authorization](#new-central-authorization) work that will replace it. + +## How the policy works + +Tiled stores **access tags** on every node. When you list or read nodes, the +policy asks OPA for the tags your token grants, and Tiled returns only nodes +carrying one of them. OPA makes every decision. `dls-tiled` translates between +Tiled and OPA. + +| Tiled asks | OPA answers | +| -------------------------- | ------------------------------------------- | +| What can this user see? | A list of tags, or `["*"]` for everything | +| What may they do here? | A set of scopes, such as `read:data` | +| May they create this node? | The tags to store on it, or a refusal | +| May they retag this node? | See the rules for each policy below | + +Both policies share this behaviour: + +- **A user principal is required.** Anonymous requests get a `401`, and so do + Tiled's own `service` principals. A Keycloak service account is unaffected: to + Tiled its token is an ordinary user login, and OPA recognises it by the + missing `fedid` claim. +- **Answers are cached per token**: for 60 seconds for users and 7.5 minutes for + service accounts. A change of permissions can take that long to reach Tiled. +- **If OPA can't be reached**, the request gets no access. That result isn't + cached, so access returns as soon as OPA does. + +## Current policy + +The rules are in the [`diamond.policy.tiled`](https://github.com/DiamondLightSource/authz/blob/main/policy/diamond/policy/tiled/tiled.rego) +package of the `authz` repository. The tree has three container levels: + +``` +/// +``` + +### Tags + +A node carries its own tag, the grant tags of its ancestors (so one grant covers +a whole subtree), and on the two upper levels a `visible:` tag that lets people +with access further down list the node without gaining access to its contents. + +| Level | Tags stored on the node | +| -------- | -------------------------------------------------------------------------------------------------------- | +| Beamline | `beamline:i22`, `visible:beamline:i22` | +| Proposal | `beamline:i22,proposal:12345`, `beamline:i22`, `visible:beamline:i22,proposal:12345` | +| Visit | `beamline:i22,proposal:12345,session:12345-1`, `beamline:i22,proposal:12345`, `beamline:i22` | + +### Who can read what + +| Caller | Granted tags | Result | +| ------------------------------------ | --------------------------------------------------------- | -------------------------------------------- | +| Admin | `*` | Everything | +| Beamline admin, or service account | The beamline tag | Everything on the beamline | +| Proposal member | The proposal tag, plus the beamline's `visible:` tag | All visits in the proposal | +| Visit member | The visit tag, plus `visible:` tags for the levels above | That visit, and can list the path down to it | + +### Who can write + +Everyone who passes the checks above gets `read:metadata` and `read:data`. A +token whose audience includes `tiled-writer` also gets `write:metadata`, +`write:data`, `create:node` and `register`. + +Creating a node needs one of the following, and the beamline must match the +proposal or visit: + +- a named user with access to that beamline, proposal or visit +- a service account whose token's `beamline` claim matches, which is how blueapi + writes runs + +The client asks for a node by sending one access tag containing JSON, and OPA +replaces it with the real tags. blueapi sends this for each run: + +```json +{"beamline": "i22", "proposal": "cm12345", "visit": 1} +``` + +A node created with no access tags at all is allowed without asking OPA +(`empty_access_blob_public`, on by default). + +### Configuration + +```yaml +access_control: + access_policy: dls_tiled.policy:DiamondOpenPolicyAgentAuthorizationPolicy + args: + authorization_provider: http://opa:8181/v1/data/diamond/policy/ + token_audience: account +``` + +The OPA rule paths default to `tiled/user_sessions`, `tiled/user_session`, +`tiled/modify_session` and `tiled/scopes`. They are appended to +`authorization_provider`, so it needs its trailing `/`. The +[project README](../README.md#dls-access-policy) lists every argument. + +## New central authorization + +> **In development.** This is on the +> [`move-to-central-authz`](https://github.com/DiamondLightSource/dls-tiled/tree/move-to-central-authz) +> branch and hasn't been released. Details may change before it is. + +The rules move to `diamond.tiled` in Diamond's central authorisation service +([`auth-auth/authorisation`](https://gitlab.diamond.ac.uk/auth-auth/authorisation) +on GitLab). The tree loses the proposal level, and each visit gains three +folders with different write permissions: + +``` +/ i22 lets callers see the instrument node only + / i22,cm12345-1 + raw/ i22,cm12345-1 written by the instrument's tiled-writer + processed/ i22,cm12345-1,p written by the processing service account + processing/ i22,cm12345-1,t written by anyone on the visit +``` + +Anyone on the visit can read all three folders, and can see the instrument node +above them. So there is now somewhere for visit members to write their own +results: `processing/`. + +### What changes + +| | Current | Central | +| --------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- | +| Rules | `diamond.policy.tiled`, in `authz` on GitHub | `diamond.tiled`, in `authorisation` on GitLab | +| Tree | `beamline/proposal/visit/run` | `instrument/visit/{raw,processed,processing}/run` | +| Tags on a node | Several per node (`beamline:i22,proposal:...`) | Exactly one (`i22,cm12345-1,t`) | +| Create request | `{"beamline", "proposal", "visit"}` | `{"instrument", "instrument_session", "folder"}` | +| Node tag decided by | The client's request | The node's **path**, which requests must agree with | +| Untagged create | Allowed, and left untagged | Given the tag its path needs, if OPA allows it | +| Changing tags | Yes or no from OPA | Can only restore the tag the path needs | +| Server command | `tiled serve` | `dls-tiled serve` (required) | +| Tiled version | 0.2.18 (`access_blob`) | 0.2.21 (`access_tags`) | +| OPA rules | `user_session`, `user_sessions`, `modify_session`, `scopes` | `node_tags` (create and modify), `user_tags`, `scopes` | + +### A node's tag follows its path + +Envoy routes on the instrument in the path, so a node's tag must always match +where the node is. The policy works out the instrument, visit and folder from the +path being written to, and asks OPA about that place. It refuses with a `403`, +without calling OPA: + +- tags that name a different instrument, visit or folder from the path +- a node anywhere else: at the root, as a fourth folder of a visit, with an + upper-case visit key, or with no key +- changing a node's tags to anything other than the tag its path needs, so a node + can't be moved to another folder by retagging + +It also rejects any OPA answer other than the single tag that the path needs. + +Tiled doesn't tell an access policy where a node is being created. `dls-tiled +serve` is `tiled serve` plus a middleware that records this, and without it the +policy refuses to create nodes or change their tags. The image runs it by +default. Tiled's GraphQL entities and links aren't in the tree, so they are still +tagged from what the caller sends. + +### Creating a node + +Send one access tag holding JSON. `folder` defaults to `raw`, and leaving out +`instrument_session` means the instrument node: + +```json +{"instrument": "i22", "instrument_session": "cm12345-1", "folder": "processing"} +``` + +### Configuration + +```yaml +access_control: + access_policy: dls_tiled.policy:DiamondOpenPolicyAgentAuthorizationPolicy + args: + authorization_provider: http://opa:8181/v1/data/diamond/ + token_audience: account +``` + +### Migrating + +Existing runs have to move into the new tree and be retagged. The scripts and a +runbook, including how to back up the database first, are in `database_migration/` +on the branch (`migrate_to_central_authz.py` and its `README.md`). diff --git a/docs/images/deployment-architecture.png b/docs/images/deployment-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..86235d336c348a19ea0fdd5b3554de22b3b8227f GIT binary patch literal 126236 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�C`UD?9Eu4|9d@phcn;GGNVfJ^w&dPlisW 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%0DJqw8=kv2#% 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 z zx^5!K(iEo%t#(_tOH54X+EKS%Gy%`%(_q~|C=>vcR`=0SguD=&Bz*wL7+%`gymsMA zDno?qq;V{hE4hZIQ+c0sFh%!$3#}GbhvlvoWs6x$s*WAO)2uj;95Rf`A913fyRALL_-K+VLycu|LopkKU5 z9vX>jM+?b!e2lLb7ad*tKR?8k{IhluTxj6YJ-GO=Hq9TPi5^r>Z@>uSTlBU}n#D_6 zcH@}+bt-NxC}%Iu2tWTps2~0>QcS=M{DEGxbyEf0uIBGX1iXWlgAPaQdjB%xi+F$N@YlWPE*i1Cj#b z*#2boilrBWDkiHh=aWqfa24(p3INXt@1H-_HYn`_*a`{lI_4t@D5IZ;hl@iVTL-4e z4$T;5V138|>F4G36u6}PQfiu%;nq_Rz(~aJE_7e~Hl;lyXPKyYUSOagR?kcqZGo2_ zozTV4ZfNDP>AZsNc4XqqCR|`YIDsHhrfaYQXl8&kyxk=gmN%a?8 zMz*0TSB)@g=$Jv08KcWh_v6AQfzF{T<5SB6^cLW}NH^UG)cII2T?w_z~v!U}m;MCDGVhS;!Pb9L| z-eV{vNOEZr^BQc)#A|*Nrtbn3@y; zj&=NbPgz-+PUcIx_YBERl{G85B=)byM;7wVznCJ^gNuI5&sAI?fd5L_*KM7JrT4hH zduY#mKuDpV%>Y}CCs53t$wo)1M~$mx-*aZ3a9a4{P0N<*3Q2RPyU!>4ZiWEncQE8? zA7G{X_lvpfU6kdfM2}2d#j<-LL~Y^jYDqp${oY0zw~sh!p|dw2k9)3#{O1%J=G)8u zS$pm1QQ*p|Wp01N!)Bb_X}}^7el?hu3M^i>BF_4ETWP3~!JQ}Xu^4@kC~(&UszeUm z0LeytM4^%9LS}OD2Vop>co^H!P?#k3^&^1l;2;HB7yS3(fBE_q+ZnV8B4T#7kW?Ki z-wXc}5y225*TA=+TwJ?=a_Q2?)W?;Up2R*S!F&gHK1+G$)8WFX@w?=r$j>>XP+7k? zw93sRJWIJJdYj4J#d`*xZVOm30CDG-mLFX))KoC5;=_0G^U?nevHQ@|EnD;SQ~eTOJQ zZo|XYSj}f~NB^cAK(xqVLe6*G!E;P2jtY-~9pZgVqv()eFx6?keQv_Y2iyJl{>WU3s@X^Bz_c{j?tI&!R`E{=~VDqT3Fo8=4*qh`PA z%$|S*+nZ%A`svXH zFdn4oem(T92iU(4my?bUO|c2Tj(LHcR_~|yQ2oaK#zrfLfi9aQ6i$bWbW^H&1YGkJ zqZ>g9k7gaK>&yKaqdiF(hl6VFe(7)6?ymQ?XQUi|2FG-U zGbXY+1_PS<^S;=PL`>u!-Epye{M#ultxz*_a~0faC?!lmi(ymyfp+ynjSO&*pj)`D zo`}8Gs&gT3j~4D;`Wh^ONtkF7tcVPY1VFBYEeZOgMOj3vGD$W4=JPD|dkrnml4z^; zrKsB5za+~XVjo$MQ4nyZ3vFy!Ouc?+@AxP4mr1)+-SnpiV+W4dqabER-S$F6O}Kry z_Ud%kCGu}gi4U_-HzRRxU6HBOuNu^vb{Vmf!nnrr z5VBKKzwuPn1bgy}T|UH!%}Q%;oZa?wP1z@_v0Im{UQPLoXmK^&x{X^|)~%qVL{t6qVnc@zk literal 0 HcmV?d00001 diff --git a/docs/nexus.md b/docs/nexus.md new file mode 100644 index 0000000..ce10925 --- /dev/null +++ b/docs/nexus.md @@ -0,0 +1,216 @@ +# NeXus files + +[`bluesky-nexus`](https://github.com/bluesky/bluesky-nexus) turns a Bluesky run into +a [NeXus](https://www.nexusformat.org/) file. It is an early-stage project and isn't +on PyPI, so install it from GitHub: + +```sh +pip install git+https://github.com/bluesky/bluesky-nexus "tiled[client]" bluesky-tiled-plugins +``` + +There are three ways to get a file, depending on when you need it: + +| When | How | Status | +| ---------------------------- | ------------------------------------------------------------------- | ------------------------- | +| [After the scan](#after-the-scan) | Rebuild the run's documents from Tiled | Works today | +| [As the scan ends](#as-the-scan-ends) | Subscribe a callback to the scan's documents | Works, if you can subscribe | +| [During the scan](#experimental-during-the-scan-with-streams) | Rewrite a snapshot on each Tiled stream update | Experimental | + +In all of them, the file holds **only the values recorded in events**: motor +positions, scalar signals and configuration. Detector data that Bluesky stores in +external files is listed in the NeXus file with **empty datasets**, and isn't +linked. `bluesky-nexus` doesn't read those files. Fetch such data from Tiled +separately. + +Run the code on a machine where you have write access to the output directory. +The Tiled pods themselves can't write to `/dls`. + +## After the scan + +Build the Bluesky documents from what Tiled stores and pass them to the callback. +This helper needs no server support. It is also what the experimental recipe below +uses. + +```python +import time +from pathlib import Path + +from bluesky_nexus.callbacks import HdfWriterCallback +from tiled.client import from_uri + + +def run_documents(run): + """Yield (name, document) pairs for the event data Tiled holds for a run.""" + start = run.metadata["start"] + yield "start", start + for stream_name in run: + stream = run[stream_name] + metadata = dict(stream.metadata) + descriptor_uid = metadata["uid"] + object_keys = {} + for key, info in metadata["data_keys"].items(): + object_keys.setdefault(info["object_name"], []).append(key) + yield "descriptor", { + **metadata, + "name": stream_name, + "run_start": start["uid"], + "object_keys": object_keys, + } + table = stream.base["internal"].read() + keys = [key for key in metadata["data_keys"] if key in table.columns] + for row in table.to_dict("records"): + yield "event", { + "descriptor": descriptor_uid, + "uid": f"{descriptor_uid}-{row['seq_num']}", + "seq_num": int(row["seq_num"]), + "time": row["time"], + "data": {key: row[key] for key in keys}, + "timestamps": {key: row[f"ts_{key}"] for key in keys}, + } + # A running scan has no stop document, but the callback needs one to write. + yield "stop", run.metadata.get("stop") or { + "uid": "in-progress", + "run_start": start["uid"], + "time": time.time(), + "exit_status": "unknown", + "reason": "scan still running", + "num_events": {}, + } + + +client = from_uri("https://tiled.diamond.ac.uk") +run = client["p51/cm44254/cm44254-1/9a812aec-b092-4fb4-9c8e-c58beac77fdc"] + +writer = HdfWriterCallback(Path("/path/to/output")) +for name, document in run_documents(run): + writer(name, document) +``` + +The file is named from the run's start time and plan, such as +`2026-07-02-10-22-15-scan.nxs`. `HdfWriterCallback` won't overwrite an existing +file. + +### Using `run.documents()` + +Tiled can reconstruct the documents on the server, which also includes the +detector `stream_resource` documents. Where the server supports it, you can replace +`run_documents(run)` with `run.documents()`. + +**Diamond's server doesn't support this yet**, so the call fails with +`406: None of the media types requested by the client are supported`. It needs +`bluesky-tiled-plugins` added to the image, and this in the server config: + +```yaml +media_types: + BlueskyRun: + application/json-seq: bluesky_tiled_plugins.exporters:json_seq_exporter +``` + +## As the scan ends + +`HdfWriterCallback` is a normal Bluesky callback. It collects a run's documents and +writes one file when the stop document arrives. + +If you control the `RunEngine`: + +```python +RE.subscribe(HdfWriterCallback(Path("/path/to/output"))) +``` + +Or run it as a separate service that receives documents over ZeroMQ or Kafka: + +```sh +pip install "bluesky-nexus[zmq] @ git+https://github.com/bluesky/bluesky-nexus" +python -m bluesky_nexus.dispatchers.zmq + +pip install "bluesky-nexus[kafka] @ git+https://github.com/bluesky/bluesky-nexus" +python -m bluesky_nexus.dispatchers.kafka +``` + +The file only appears once the scan has finished. blueapi publishes its documents +over STOMP, which neither service reads, and you can't subscribe to its `RunEngine` +from outside, so for scans run through blueapi use [After the scan](#after-the-scan). + +To do something other than write HDF5, subclass `BaseCallback` and implement +`process_nexus_structure(self, generated_structure)`. + +## Experimental: during the scan with streams + +> **Experimental.** This is a stopgap until +> [bluesky-nexus#1](https://github.com/bluesky/bluesky-nexus/issues/1) adds a proper +> Tiled interface, which will let a callback take a Tiled container directly. +> Expect this recipe to be replaced. Don't use it for anything that depends on the +> file being complete. + +This watches a visit with [streaming](streaming.md). While a scan runs, it rebuilds +the NeXus file from Tiled every few seconds. When the stop document arrives, it +writes the file a final time. It uses `run_documents` from +[After the scan](#after-the-scan). + +```python +import os +import threading + +from bluesky_nexus.callbacks import BaseCallback +from bluesky_tiled_plugins import subscribe_to_streams + +OUTPUT = Path("/path/to/output") +SECONDS_BETWEEN_SNAPSHOTS = 5 + +visit = client["p51/cm44254/cm44254-1"] +lock = threading.Lock() +last_written = {} + + +class SnapshotWriter(BaseCallback): + def process_nexus_structure(self, generated_structure): + start = self._run_data.run_start + final = OUTPUT / f"{start['scan_id']}-{start['uid'][:8]}.nxs" + # Readers only ever see a complete file. + partial = final.with_suffix(".partial.nxs") + generated_structure.save(partial, mode="w") + os.replace(partial, final) + + +def write_snapshot(run_uid, final=False): + with lock: # callbacks can run concurrently + now = time.monotonic() + if not final and now - last_written.get(run_uid, 0) < SECONDS_BETWEEN_SNAPSHOTS: + return + last_written[run_uid] = now + writer = SnapshotWriter() + for name, document in run_documents(visit[run_uid]): + writer(name, document) + + +def on_update(update): + write_snapshot(update.run_uid) + + +def on_metadata(update): + if "stop" in update.metadata: + write_snapshot(update.key, final=True) + + +finished_runs = visit.subscribe() +finished_runs.child_metadata_updated.add_callback(on_metadata) +finished_runs.start_in_thread() + +with subscribe_to_streams(visit, on_update, streams={"primary": None}): + threading.Event().wait() # until interrupted +``` + +Limitations: + +- Each snapshot re-reads the whole run and rewrites the whole file, so the cost + grows as the scan does. Increase `SECONDS_BETWEEN_SNAPSHOTS` for long scans. +- Until the scan ends, `entry/run_info/run_stop` holds a placeholder with + `exit_status` of `unknown`. +- It relies on the private `_run_data` attribute for the file name. +- Only the `primary` stream triggers a snapshot, though each snapshot includes every + stream that exists by then. +- Runs that fail before recording an event get only the final file. +- On startup, updates still in the streaming cache are replayed, so you may also + get files for scans that ran shortly before. +- A snapshot may lag the newest points slightly, because streaming doesn't guarantee + that a read made at the same moment sees the latest writes. diff --git a/docs/searching.md b/docs/searching.md new file mode 100644 index 0000000..e1c4413 --- /dev/null +++ b/docs/searching.md @@ -0,0 +1,271 @@ +# Searching and querying + +There are two ways to query Tiled: the [Python client](#searching-from-python), +which has the full range of queries, and [Glazed](#graphql-with-glazed), a +GraphQL interface suited to web applications. + +## Searching from Python + +`search` filters the **direct children** of a node. Runs are stored under their +visit, so search the visit. Searching the root finds nothing, because the root's +children are beamlines. + +```python +from tiled.client import from_uri +from tiled.queries import Key + +client = from_uri("https://tiled.diamond.ac.uk") +visit = client["p51/cm44254/cm44254-1"] + +results = visit.search(Key("start.plan_name") == "scan") +len(results) +run = results.values().first() +``` + +`Key` addresses metadata with dots. A run's metadata is its Bluesky start and stop +documents, so keys begin with `start.` or `stop.`. Always include that prefix: +leaving it out only works on catalogs with Bluesky's custom client, and Diamond's +visit nodes aren't those. + +Each `search` returns another searchable node, so chain them to combine +conditions: + +```python +failed_scans = ( + visit.search(Key("start.plan_name") == "scan") + .search(Key("stop.exit_status") != "success") +) +``` + +### Useful metadata + +| Key | Meaning | +| -------------------------------------------------------- | --------------------------------------------- | +| `start.uid` | The run's unique ID, also its key in Tiled | +| `start.scan_id` | The scan number | +| `start.time` | Start time, as a Unix timestamp | +| `start.plan_name`, `start.plan_type` | The plan that ran | +| `start.instrument`, `start.instrument_session` | Beamline and visit | +| `start.detectors`, `start.motors` | Lists of device names | +| `start.num_points`, `start.shape` | Size of the scan | +| `start.blueapi_task_id` | The blueapi task that produced the run | +| `stop.exit_status` | `success`, `abort` or `fail` | +| `stop.reason`, `stop.num_events.primary` | Why it stopped; events recorded in `primary` | + +To see what else a run has, look at `run.metadata`. A run that is still going has +no `stop` key. + +### Query types + +All from `tiled.queries`: + +| Query | Example | Finds runs where... | +| --------------------- | ----------------------------------------------------------- | --------------------------------------- | +| Equals | `Key("start.scan_id") == 42` | the value matches | +| Not equal | `Key("stop.exit_status") != "success"` | the value differs | +| Comparison | `Key("start.num_points") > 100` (also `>=`, `<`, `<=`) | the number compares | +| `In` | `In("start.plan_name", ["scan", "grid_scan"])` | the value is one of these | +| `NotIn` | `NotIn("start.plan_name", ["count"])` | the value is none of these | +| `Contains` | `Contains("start.detectors", "panda1")` | the list includes this value | +| `KeyPresent` | `KeyPresent("stop")` | the key exists (here: the run finished) | +| `Like` | `Like("start.sample", "sil%")` | SQL-style match, `%` is a wildcard | +| `FullText` | `FullText("lysozyme")` | any metadata contains the whole word | +| `SpecsQuery` | `SpecsQuery(["BlueskyRun"])` | the node has these specs | +| `StructureFamilyQuery`| `StructureFamilyQuery("container")` | the node is of this type | + +`Regex` exists in `tiled.queries`, but the catalog doesn't support it. Use `Like`. + +### Recipes + +```python +from datetime import datetime + +from tiled.queries import Contains, Key, KeyPresent + +# A scan by number +visit.search(Key("start.scan_id") == 42).values().first() + +# The most recent run +visit.values().last() + +# The run from a blueapi task +visit.search(Key("start.blueapi_task_id") == task_id).values().first() + +# Everything since a given time +visit.search(Key("start.time") > datetime(2026, 7, 2, 9, 0).timestamp()) + +# Finished scans using a detector, newest first +results = ( + visit.search(KeyPresent("stop")) + .search(Contains("start.detectors", "panda1")) + .sort(("start.time", -1)) +) + +# Which plans have been run here, and how often +visit.distinct("start.plan_name", counts=True) +``` + +`.values().first()` raises `IndexError` if nothing matched. + +Sort with `1` for ascending and `-1` for descending. The `DESCENDING` constant +raises a `TypeError` in the 0.2.21 client. + +Two things from Bluesky tutorials don't work on Diamond's server: + +- `visit[-1]` or `visit[42]`, which need Bluesky's catalog client. Use the recipes + above. +- `ScanID`, `PartialUID`, `ScanIDRange` and `TimeRange` from + `bluesky_tiled_plugins.queries`, which need the plugin on the server, and the + `dls-tiled` image doesn't include it. Use `Key("start.scan_id")` and + `Key("start.time")` instead. + +### Searching over HTTP + +Queries are URL parameters on the search endpoint, in the form +`filter[][condition][]`. Values are JSON, so strings keep their +quotes: + +``` +GET /api/v1/search/p51/cm44254/cm44254-1 + ?filter[eq][condition][key]=start.plan_name + &filter[eq][condition][value]="scan" +``` + +## GraphQL with Glazed + +[Glazed](https://github.com/DiamondLightSource/glazed) is a read-only GraphQL +service in front of Tiled. It passes your bearer token through, so you see exactly +what you would see in Tiled. It is served at `https://tiled.diamond.ac.uk/glazed/`, +and the paths below are relative to that address. To try queries in your browser, +open the playground at . + +| Path | Purpose | +| ------------------------------------ | ----------------------------------------------------------------- | +| `POST /graphql` | Queries | +| `GET /graphiql` | Interactive playground, with the schema browsable in your browser | +| `/subscribe` | WebSocket subscriptions, using the `graphql-transport-ws` protocol | +| `GET /asset/{run}/{stream}/{det}/{id}` | Download a file | +| `GET /status` | Glazed's version | + +> **Limitation: only runs at the root of Tiled are found.** Glazed searches the +> root node's children, but blueapi now writes runs under +> `beamline/proposal/visit`, so `instrumentSession` and `run` don't find them. +> They work only for older runs that are still stored at the root. +> `appMetadata` and the subscriptions aren't affected. + +You can run these queries in the playground. From Python, the Tiled client's +HTTP session already holds your login: + +```python +GLAZED = "https://tiled.diamond.ac.uk/glazed/" + +def gql(query, **variables): + response = client.context.http_client.post( + f"{GLAZED}graphql", json={"query": query, "variables": variables} + ) + return response.json()["data"] +``` + +### Runs in a visit + +```graphql +query ($session: String!) { + instrumentSession(name: $session) { + name + runs { + id + scanNumber + } + } +} +``` + +Variables: `{"session": "cm00000-1"}`. + +### One run and its data + +`data` is a list of two possible types, so pick fields for each. An unknown ID +returns `null`. + +```graphql +query ($id: String!) { + run(id: $id) { + id + scanNumber + data { + __typename + ... on ArrayData { + name + files { + file + download + } + } + ... on TableData { + name + columns + } + } + } +} +``` + +`file` is the path on disk. `download` is a Glazed URL to fetch it with your +token, and is empty when the file can't be downloaded. + +### Table values + +Leave out `columns` to get every column: + +```graphql +query ($id: String!, $columns: [String!]) { + run(id: $id) { + data { + ... on TableData { + name + data(columns: $columns) + } + } + } +} +``` + +The result maps each column name to a list of values. + +### Server information + +```graphql +{ + appMetadata { + apiVersion + libraryVersion + queries + } +} +``` + +### Live events + +`events` watches Tiled's root, and `nodeEvents(node: ...)` watches one node. The +first message is always the node's schema, and new data follows. + +```graphql +subscription ($node: String!) { + nodeEvents(node: $node) { + __typename + ... on ContainerSchema { version } + ... on ChildCreated { key sequence metadata } + ... on ChildMetadataUpdated { key sequence metadata } + ... on StreamTableData { sequence append arrowSchema } + ... on StreamArrayData { sequence shape mimetype } + } +} +``` + +The other event types are `ArraySchema`, `TableSchema` and `ArrayRef`. To +authenticate, send your token as `{"Authorization": "Bearer "}` in the +`connection_init` payload, and also on the WebSocket handshake so that it passes +the login proxy. + +Glazed isn't the same as Tiled's own GraphQL endpoint at `/api/graphql`, which +serves a separate graph of entities and links, not runs. diff --git a/docs/streaming.md b/docs/streaming.md new file mode 100644 index 0000000..ff308c4 --- /dev/null +++ b/docs/streaming.md @@ -0,0 +1,154 @@ +# Streaming live data + +Tiled can push data to you as a scan runs, over WebSockets. blueapi writes each +event to Tiled as soon as it is taken (`TiledWriter(..., batch_size=1)`), so you +get each point as it arrives and don't need to poll. + +Upstream reference: +[Subscribe to live Bluesky stream data](https://blueskyproject.io/bluesky-tiled-plugins/tutorials/streaming.html#subscribe-to-live-bluesky-stream-data). + +## Subscribe to a visit + +`subscribe_to_streams` watches a container for new runs and delivers the data keys +you choose. The container must be the **direct parent of the runs**, which at +Diamond is the visit. + +It needs a recent client: `bluesky-tiled-plugins` 2.0.13 and `tiled` 0.2.21 are +known to work. Older Tiled clients try to create an API key for the connection, +which fails without the `create:apikeys` scope. + +```python +from bluesky_tiled_plugins import BlueskyStreamUpdate, subscribe_to_streams +from tiled.client import from_uri + +client = from_uri("https://tiled.diamond.ac.uk") +visit = client["p51/cm44254/cm44254-1"] + + +def on_update(update: BlueskyStreamUpdate) -> None: + print(update.run_uid, update.stream_name, update.data_keys, update.sequence) + print(update.data()) + + +with subscribe_to_streams(visit, on_update, streams={"primary": "det"}): + input("Listening. Press Enter to stop.\n") +``` + +Leaving the `with` block disconnects everything. If a context manager doesn't suit +your code, keep the returned object and call `disconnect()` on it. + +The subscription stays active across scans, so it picks up each new run in the +visit as it starts. + +### Choosing what to receive + +`streams` is required. + +| Value | You receive | +| --------------------------------------------- | ------------------------------------------------------ | +| `{"primary": "det"}` | One key | +| `{"primary": ("det", "motor")}` | Several keys | +| `{"primary": None, "baseline": "x"}` | Every key in `primary`, and one key in `baseline` | +| `None` | Every key in every stream of every run (can be a lot) | + +Streams you don't name are ignored. Ragged and bytes data aren't delivered. + +### Choosing which runs + +Filters are checked before anything is connected. A run must pass all of them. + +```python +subscribe_to_streams( + visit, + on_update, + streams={"primary": None}, + metadata_filter=lambda start: start.get("plan_name") == "spec_scan", + required_specs=("BlueskyRun",), +) +``` + +`run_filter` is also available. It receives the raw `LiveChildCreated` update, for +conditions that the other two can't express. + +To get a run's start document in your callback: + +```python +start = visit[update.run_uid].metadata_copy()[0].get("start", {}) +``` + +### Things to design for + +- **Updates aren't ordered across data nodes.** Arrays and table columns in + different nodes arrive as separate callbacks, with no alignment between them. + Scalars stored in the same table arrive together in one batch. +- **Callbacks can run concurrently.** Protect any shared state with a lock. +- **Use `update.sequence` to detect gaps or duplicates** within one node. It + doesn't compare across nodes. +- **Replay is limited.** By default (`start=0`) you first get whatever is still in + the streaming cache, not the whole history. Use [search](searching.md) for + history. +- **The end of a stream doesn't mean the data can be read yet.** A REST read made + at the same moment may not see the last writes. + +## Lower-level subscriptions + +To be told about new runs and their completion, without any data, subscribe to the +container itself. Use the `key` to find the run: + +```python +sub = visit.subscribe() +sub.child_created.add_callback(lambda u: print("new run", u.key)) +sub.child_metadata_updated.add_callback( + lambda u: print("finished", u.key) if "stop" in u.metadata else None +) +sub.start_in_thread() +``` + +## Raw WebSockets + +For clients without the Python library, connect to `stream/single/` under the +API, then send your access token as the first message: + +```python +import json + +from websockets.sync.client import connect + +path = "p51/cm44254/cm44254-1" +url = f"wss://tiled.diamond.ac.uk/api/v1/stream/single/{path}?envelope_format=json" + +with connect(url) as ws: + ws.send(json.dumps({"type": "auth", "access_token": token})) + schema = json.loads(ws.recv()) # bad credentials close the socket with code 4003 + for message in ws: + print(json.loads(message)) +``` + +Add `&start=1` to replay from the first update still in the cache. + +| Message `type` | Meaning | +| ---------------------------------- | ---------------------------------------------------------- | +| `container-schema`, `array-schema`, `table-schema` | The first message, describing the node | +| `container-child-created` | A new child: on a visit, a new run | +| `container-child-metadata-updated` | A child's metadata changed: a run's `stop` document arrives this way | +| `table-data` | New rows | +| `array-data`, `array-ref` | New array data, or a reference to it | + +Watching a single table gives you rows as they arrive: +`/primary/internal` holds a run's scalar values. + +[Glazed subscriptions](searching.md#live-events) offer the same events over +GraphQL. + +## Running a server with streaming + +This matters only if you run your own Tiled. Streaming needs a Redis cache in the +server config: + +```yaml +streaming_cache: + uri: "redis://:password@redis-host:6379" +``` + +Without it, or with Tiled's embedded `SimpleTiledServer`, subscriptions aren't +available. From 51c9203d84ff033d96b3721632e71bf4783ffa3c Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Sun, 8 Nov 2026 22:54:37 +0000 Subject: [PATCH 2/4] Build the documentation with Sphinx and deploy it with GitHub Actions Generate the docs setup from python-copier-template 5.4.0 with docs_type sphinx and move the pages into its tutorials, how-to and explanations layout. Publish through the GitHub Pages actions in place of the template's gh-pages step. Co-Authored-By: Claude Opus 5.5 --- .copier-answers.yml | 2 +- .github/workflows/_docs.yml | 65 ++ .github/workflows/ci.yml | 9 +- .github/workflows/periodic.yml | 13 + README.md | 6 +- docs/README.md | 108 --- docs/_api.rst | 16 + docs/_templates/custom-module-template.rst | 37 + docs/conf.py | 197 ++++++ docs/explanations.md | 10 + docs/{ => explanations}/architecture.md | 8 +- docs/{ => explanations}/authorization.md | 4 +- docs/explanations/decisions.md | 12 + .../0001-record-architecture-decisions.md | 18 + ...0002-switched-to-python-copier-template.md | 28 + docs/explanations/decisions/COPYME | 19 + docs/genindex.md | 3 + docs/how-to.md | 10 + docs/how-to/contribute.md | 2 + .../create-nexus-files.md} | 2 +- docs/how-to/get-help.md | 19 + docs/how-to/run-container.md | 14 + docs/{searching.md => how-to/search.md} | 0 .../stream-live-data.md} | 4 +- docs/images/dls-logo.svg | 11 + docs/index.md | 56 ++ docs/reference.md | 12 + docs/tutorials.md | 10 + docs/tutorials/getting-started.md | 86 +++ docs/tutorials/installation.md | 35 + pyproject.toml | 38 +- uv.lock | 639 +++++++++++++++++- 32 files changed, 1341 insertions(+), 152 deletions(-) create mode 100644 .github/workflows/_docs.yml create mode 100644 .github/workflows/periodic.yml delete mode 100644 docs/README.md create mode 100644 docs/_api.rst create mode 100644 docs/_templates/custom-module-template.rst create mode 100644 docs/conf.py create mode 100644 docs/explanations.md rename docs/{ => explanations}/architecture.md (92%) rename docs/{ => explanations}/authorization.md (97%) create mode 100644 docs/explanations/decisions.md create mode 100644 docs/explanations/decisions/0001-record-architecture-decisions.md create mode 100644 docs/explanations/decisions/0002-switched-to-python-copier-template.md create mode 100644 docs/explanations/decisions/COPYME create mode 100644 docs/genindex.md create mode 100644 docs/how-to.md create mode 100644 docs/how-to/contribute.md rename docs/{nexus.md => how-to/create-nexus-files.md} (98%) create mode 100644 docs/how-to/get-help.md create mode 100644 docs/how-to/run-container.md rename docs/{searching.md => how-to/search.md} (100%) rename docs/{streaming.md => how-to/stream-live-data.md} (98%) create mode 100644 docs/images/dls-logo.svg create mode 100644 docs/index.md create mode 100644 docs/reference.md create mode 100644 docs/tutorials.md create mode 100644 docs/tutorials/getting-started.md create mode 100644 docs/tutorials/installation.md diff --git a/.copier-answers.yml b/.copier-answers.yml index f42e44e..baaf458 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -6,7 +6,7 @@ author_name: Giles Knap description: Tiled server with Diamond Light Source plugins distribution_name: dls-tiled docker: true -docs_type: README +docs_type: sphinx git_platform: github.com github_org: DiamondLightSource package_name: dls_tiled diff --git a/.github/workflows/_docs.yml b/.github/workflows/_docs.yml new file mode 100644 index 0000000..ba0f5ef --- /dev/null +++ b/.github/workflows/_docs.yml @@ -0,0 +1,65 @@ +on: + workflow_call: + + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + # Need this to get version number from last tag + fetch-depth: 0 + + - name: Install system packages + run: sudo apt-get install graphviz + + - name: Install uv + uses: astral-sh/setup-uv@v10.0.0 + + - name: Build docs + run: uv run --locked tox -e docs + + - name: Remove environment.pickle + run: rm build/html/.doctrees/environment.pickle + + - name: Upload built docs artifact + uses: actions/upload-artifact@v7 + with: + name: docs + path: build + + - name: Sanitize ref name for docs version + run: echo "DOCS_VERSION=${GITHUB_REF_NAME//[^A-Za-z0-9._-]/_}" >> $GITHUB_ENV + + - name: Move to versioned directory + run: mv build/html .github/pages/$DOCS_VERSION + + - name: Write switcher.json + run: python .github/pages/make_switcher.py --add $DOCS_VERSION ${{ github.repository }} .github/pages/switcher.json + + - name: Upload GitHub Pages artifact + if: github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v5 + with: + path: .github/pages + + # Each deployment replaces the whole site, so only main is published: a tag + # would replace it with just that version + deploy: + needs: build + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a85871b..5db6915 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,11 +62,18 @@ jobs: contents: read packages: write + docs: + uses: ./.github/workflows/_docs.yml + permissions: + contents: read + pages: write + id-token: write + dist: uses: ./.github/workflows/_dist.yml release: - needs: [dist, test, container, helm] + needs: [dist, test, container, helm, docs] if: github.ref_type == 'tag' uses: ./.github/workflows/_release.yml permissions: diff --git a/.github/workflows/periodic.yml b/.github/workflows/periodic.yml new file mode 100644 index 0000000..18b419c --- /dev/null +++ b/.github/workflows/periodic.yml @@ -0,0 +1,13 @@ +name: Periodic + +on: + workflow_dispatch: + schedule: + # Run weekly to check URL links still resolve + - cron: "0 8 * * WED" + +jobs: + linkcheck: + uses: ./.github/workflows/_tox.yml + with: + tox: docs -- -b linkcheck diff --git a/README.md b/README.md index 4ca1c85..1cedce7 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,14 @@ What | Where :---: | :---: Source | Docker | `docker run ghcr.io/diamondlightsource/dls-tiled:latest` +Documentation | Releases | + + For how to use Tiled at Diamond (searching, streaming, NeXus files, access -control and the deployment architecture), see the [documentation](docs/README.md). +control and the deployment architecture), see the +[documentation](https://diamondlightsource.github.io/dls-tiled). ## Plugins diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 7f7dd72..0000000 --- a/docs/README.md +++ /dev/null @@ -1,108 +0,0 @@ -# Using Tiled at Diamond - -[Tiled](https://blueskyproject.io/tiled/) is the data access service for scans -run with Bluesky at Diamond. It stores each scan's metadata and tabular data, knows -where its detector files are, and serves all of it over HTTP, with access -checked against your Diamond login. - -| I want to... | Read | -| -------------------------------------------------- | --------------------------------------------------------------------- | -| Find a scan and read its data from Python | [Getting started](#getting-started), [Searching](searching.md) | -| Query Tiled from a web app or with GraphQL | [Glazed](searching.md#graphql-with-glazed) | -| Follow a scan live while it runs | [Streaming](streaming.md) | -| Produce a NeXus file for a scan | [NeXus files](nexus.md) | -| Understand who can see what | [Authorization](authorization.md) | -| Understand how the service is deployed | [Architecture](architecture.md) | -| Report that data can't be read | [Getting help](#getting-help) | - -## Getting started - -Install the client. `bluesky-tiled-plugins` gives you Bluesky-aware objects for -runs and event streams, and the live streaming API: - -```sh -pip install "tiled[client]" bluesky-tiled-plugins -``` - -Connect. The first time, Tiled opens a browser so you can log in with your -Diamond account. Tokens are then cached in `~/.cache/tiled/tokens`. - -```python -from tiled.client import from_uri - -client = from_uri("https://tiled.diamond.ac.uk") -``` - -### How the data is organised - -Tiled is a tree. Runs are written by blueapi under the visit they belong to, so -you normally index straight to your visit: - -``` -///// -p51/cm44254/cm44254-1/9a812aec-b092-4fb4-9c8e-c58beac77fdc/primary/panda1-counter-1-out_dataset -``` - -```python -visit = client["p51/cm44254/cm44254-1"] -``` - -> **Changing soon.** The [central authorization work](authorization.md#new-central-authorization) -> removes the proposal level and adds folders per visit, so the same visit will -> be at `p51/cm44254-1/raw`. Code that builds these paths will need updating -> when that is released. - -### Reading a run - -```python -run = visit["9a812aec-b092-4fb4-9c8e-c58beac77fdc"] - -run.metadata["start"] # the Bluesky start document -run.metadata.get("stop") # the stop document; missing while the scan runs -list(run) # stream names, e.g. ["primary", "baseline"] - -ds = run["primary"].read() # the whole stream as an xarray.Dataset -values = run["primary"]["panda1-counter-1-out_dataset"].read() # one signal -``` - -Detector data can be many gigabytes. Slice arrays to read only what you need: - -```python -images = run["primary"]["detector"] # nothing is downloaded yet -first_frame = images[0] -``` - -### Common use cases - -- **Find scans in a visit** by plan, scan number, outcome or time: - [Searching](searching.md). -- **Analysis scripts** read arrays and tables as NumPy, pandas or xarray objects, - without needing to know file paths. -- **Live displays** get each point as it is taken: [Streaming](streaming.md). -- **Web applications** use the GraphQL interface, [Glazed](searching.md#graphql-with-glazed), - or the REST API. Tiled's interactive API docs are at `/docs` on the server. -- **ViSR imaging** has a server-side binning endpoint, described in the - [project README](../README.md#dls_tiledvisrvisr_router). -- **NeXus files** can be generated from a run: [NeXus files](nexus.md). - -## Getting help - -### If you can't read data for a beamline - -Create a ticket in the -[ACQP Jira project](https://jira.diamond.ac.uk/projects/ACQP/issues/). Include: - -- the beamline and visit, for example `p51` and `cm44254-1` -- the full node path, or the run's uid -- what you ran, the full error, and roughly when it happened -- your FedID, so access can be checked - -Before raising a ticket, these symptoms can help you tell what is wrong: - -| Symptom | Likely cause | -| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `401 Unauthorized` | You are not logged in, or your token has expired. Connect again. | -| The node isn't there (`KeyError`, `404`) | You don't have access to that visit, or the path is wrong. If you were only just added to the visit, wait a minute: Tiled remembers your permissions for up to 60 seconds. | -| The metadata reads, but reading the data gives a `500` | Usually a problem with the file, not your permissions: Tiled can't open it, or a dataset is missing from it. The beamline's pod may also lack filesystem access to it. Quote the error's correlation ID in the ticket if you have it. | -| `503` with `Retry-After` | The file is still catching up with the catalog during a scan. Try again shortly. | -| A whole beamline's data is unreadable | The beamline may not have its own Tiled deployment or data ACL yet ([why](architecture.md#adding-a-beamline)). Raise a ticket. | diff --git a/docs/_api.rst b/docs/_api.rst new file mode 100644 index 0000000..ad039f7 --- /dev/null +++ b/docs/_api.rst @@ -0,0 +1,16 @@ +:orphan: + +.. + This page is not included in the TOC tree, but must exist so that the + autosummary pages are generated for dls_tiled and all its + subpackages + +API +=== + +.. autosummary:: + :toctree: _api + :template: custom-module-template.rst + :recursive: + + dls_tiled diff --git a/docs/_templates/custom-module-template.rst b/docs/_templates/custom-module-template.rst new file mode 100644 index 0000000..9aeca54 --- /dev/null +++ b/docs/_templates/custom-module-template.rst @@ -0,0 +1,37 @@ +{{ ('``' + fullname + '``') | underline }} + +{%- set filtered_members = [] %} +{%- for item in members %} + {%- if item in functions + classes + exceptions + attributes %} + {% set _ = filtered_members.append(item) %} + {%- endif %} +{%- endfor %} + +.. automodule:: {{ fullname }} + :members: + + {% block modules %} + {% if modules %} + .. rubric:: Submodules + + .. autosummary:: + :toctree: + :template: custom-module-template.rst + :recursive: + {% for item in modules %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block members %} + {% if filtered_members %} + .. rubric:: Members + + .. autosummary:: + :nosignatures: + {% for item in filtered_members %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..f66972e --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,197 @@ +"""Configuration file for the Sphinx documentation builder. + +This file only contains a selection of the most common options. For a full +list see the documentation: +https://www.sphinx-doc.org/en/master/usage/configuration.html +""" + +import sys +from pathlib import Path +from subprocess import check_output + +import requests + +import dls_tiled + +# -- General configuration ------------------------------------------------ + +# General information about the project. +project = "dls-tiled" + +# The full version, including alpha/beta/rc tags. +release = dls_tiled.__version__ + +# The short X.Y version. +if "+" in release: + # Not on a tag, use branch name + root = Path(__file__).absolute().parent.parent + git_branch = check_output("git branch --show-current".split(), cwd=root) + version = git_branch.decode().strip() +else: + version = release + +extensions = [ + # Use this for generating API docs + "sphinx.ext.autodoc", + # and making summary tables at the top of API docs + "sphinx.ext.autosummary", + # This can parse google style docstrings + "sphinx.ext.napoleon", + # For linking to external sphinx documentation + "sphinx.ext.intersphinx", + # Add links to source code in API docs + "sphinx.ext.viewcode", + # Adds the inheritance-diagram generation directive + "sphinx.ext.inheritance_diagram", + # Add a copy button to each code block + "sphinx_copybutton", + # For the card element + "sphinx_design", + # So we can write markdown files + "myst_parser", +] + +# So we can use the ::: syntax +myst_enable_extensions = ["colon_fence"] + +# So markdown links can target headings, e.g. [text](page.md#a-heading) +myst_heading_anchors = 3 + +# If true, Sphinx will warn about all references where the target cannot +# be found. +nitpicky = True + +# A list of (type, target) tuples (by default empty) that should be ignored when +# generating warnings in "nitpicky mode". Note that type should include the +# domain name if present. Example entries would be ('py:func', 'int') or +# ('envvar', 'LD_LIBRARY_PATH'). +nitpick_ignore = [ + ("py:class", "NoneType"), + ("py:class", "'str'"), + ("py:class", "'float'"), + ("py:class", "'int'"), + ("py:class", "'bool'"), + ("py:class", "'object'"), + ("py:class", "'id'"), + ("py:class", "typing_extensions.Literal"), +] + +# Both the class’ and the __init__ method’s docstring are concatenated and +# inserted into the main body of the autoclass directive +autoclass_content = "both" + +# Order the members by the order they appear in the source code +autodoc_member_order = "bysource" + +# Don't inherit docstrings from baseclasses +autodoc_inherit_docstrings = False + +# Document only what is in __all__ +autosummary_ignore_module_all = False + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# Output graphviz directive produced images in a scalable format +graphviz_output_format = "svg" + +# The name of a reST role (builtin or Sphinx extension) to use as the default +# role, that is, for text marked up `like this` +default_role = "any" + +# The master toctree document. +master_doc = "index" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# These patterns also affect html_static_path and html_extra_path +exclude_patterns = ["_build"] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# This means you can link things like `str` and `asyncio` to the relevant +# docs in the python documentation. +intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} + +# A dictionary of graphviz graph attributes for inheritance diagrams. +inheritance_graph_attrs = {"rankdir": "TB"} + +# Ignore localhost links for periodic check that links in docs are valid +# or Diamond hosts, which the public CI runners cannot reach +linkcheck_ignore = [r"http://localhost:\d+/", r"https://[^/]*\.diamond\.ac\.uk"] + +# GitHub adds the anchors of rendered markdown with javascript +linkcheck_anchors_ignore_for_url = [r"https://github\.com/"] + +# Set copy-button to ignore python and bash prompts +# https://sphinx-copybutton.readthedocs.io/en/latest/use.html#using-regexp-prompt-identifiers +copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " +copybutton_prompt_is_regexp = True + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "pydata_sphinx_theme" +github_repo = "dls-tiled" +github_user = "DiamondLightSource" +switcher_json = f"https://{github_user}.github.io/{github_repo}/switcher.json" +switcher_exists = requests.get(switcher_json).ok +if not switcher_exists: + print( + "*** Can't read version switcher, is GitHub pages enabled? \n" + " Set the Github pages source to be 'GitHub Actions' at:\n" + f" https://github.com/{github_user}/{github_repo}/settings/pages", + file=sys.stderr, + ) + +# Theme options for pydata_sphinx_theme +# We don't check switcher because there are 3 possible states for a repo: +# 1. New project, docs are not published so there is no switcher +# 2. Existing project with latest copier template, switcher exists and works +# 3. Existing project with old copier template that makes broken switcher, +# switcher exists but is broken +# Point 3 makes checking switcher difficult, because the updated copier template +# will fix the switcher at the end of the docs workflow, but never gets a chance +# to complete as the docs build warns and fails. +html_theme_options = { + "logo": { + "text": project, + }, + "use_edit_page_button": True, + "github_url": f"https://github.com/{github_user}/{github_repo}", + "icon_links": [ + { + "name": "PyPI", + "url": f"https://pypi.org/project/{project}", + "icon": "fas fa-cube", + } + ], + "switcher": { + "json_url": switcher_json, + "version_match": version, + }, + "check_switcher": False, + "navbar_end": ["theme-switcher", "icon-links", "version-switcher"], + "navigation_with_keys": False, +} + +# A dictionary of values to pass into the template engine’s context for all pages +html_context = { + "github_user": github_user, + "github_repo": github_repo, + "github_version": version, + "doc_path": "docs", +} + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +html_show_sphinx = False + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +html_show_copyright = False + +# Logo +html_logo = "images/dls-logo.svg" +html_favicon = html_logo diff --git a/docs/explanations.md b/docs/explanations.md new file mode 100644 index 0000000..73ab289 --- /dev/null +++ b/docs/explanations.md @@ -0,0 +1,10 @@ +# Explanations + +Explanations of how it works and why it works that way. + +```{toctree} +:maxdepth: 1 +:glob: + +explanations/* +``` diff --git a/docs/architecture.md b/docs/explanations/architecture.md similarity index 92% rename from docs/architecture.md rename to docs/explanations/architecture.md index e6f55dc..185568c 100644 --- a/docs/architecture.md +++ b/docs/explanations/architecture.md @@ -4,7 +4,7 @@ Tiled at Diamond runs as one Kubernetes Deployment **per beamline**, all sharing a single database. Each beamline's pod can read only that beamline's data on disk. -![Requests to tiled.diamond.ac.uk are routed by beamline to a dedicated Service, Deployment and pod. Every pod shares tiled-database, and each beamline pod has its own data ACL on GPFS.](images/deployment-architecture.png) +![Requests to tiled.diamond.ac.uk are routed by beamline to a dedicated Service, Deployment and pod. Every pod shares tiled-database, and each beamline pod has its own data ACL on GPFS.](../images/deployment-architecture.png) ## Why one deployment per beamline @@ -49,7 +49,7 @@ Ingress (a single "/" path) → Envoy → the beamline's Service → the beamlin Envoy forwards the request unmodified. It only matches the Tiled endpoints that address a node by path, listed in `envoy.pathPrefixes` in -[`values.yaml`](../Charts/tiled/values.yaml): +[`values.yaml`](https://github.com/DiamondLightSource/dls-tiled/blob/main/Charts/tiled/values.yaml): | Endpoints | | --------------------------------------------------------------------------------------------- | @@ -72,8 +72,8 @@ has no beamline's data ACL. | PostgreSQL | The `tiled` catalog (the tree and all metadata), `auth`, and `data` (tabular event data). Shared by all pods. | | GPFS | Detector files under `/dls//data`, mounted per beamline and read-only. | | Open Policy Agent | Makes the access decisions. See [Authorization](authorization.md). | -| Redis | Tiled's streaming cache, which live [streaming](streaming.md) requires. | -| Glazed | The [GraphQL interface](searching.md#graphql-with-glazed), a separate service in front of Tiled. | +| Redis | Tiled's streaming cache, which live [streaming](../how-to/stream-live-data.md) requires. | +| Glazed | The [GraphQL interface](../how-to/search.md#graphql-with-glazed), a separate service in front of Tiled. | | oauth2-proxy | Sits in front of Tiled and Glazed, and sends anonymous browsers to log in. | Because the catalog is shared, any pod can answer metadata queries for any diff --git a/docs/authorization.md b/docs/explanations/authorization.md similarity index 97% rename from docs/authorization.md rename to docs/explanations/authorization.md index e45812e..d147b6d 100644 --- a/docs/authorization.md +++ b/docs/explanations/authorization.md @@ -6,7 +6,7 @@ Three separate layers control access to data: Services use a Keycloak service account (client credentials). 2. **Access policy.** Tiled asks [Open Policy Agent](https://www.openpolicyagent.org/) (OPA) what your token may see and do, using - [`dls_tiled.policy`](../src/dls_tiled/policy.py). + [`dls_tiled.policy`](https://github.com/DiamondLightSource/dls-tiled/blob/main/src/dls_tiled/policy.py). 3. **Filesystem ACLs.** Each beamline's pod can only read that beamline's files. See [Architecture](architecture.md#why-one-deployment-per-beamline). @@ -104,7 +104,7 @@ access_control: The OPA rule paths default to `tiled/user_sessions`, `tiled/user_session`, `tiled/modify_session` and `tiled/scopes`. They are appended to `authorization_provider`, so it needs its trailing `/`. The -[project README](../README.md#dls-access-policy) lists every argument. +[project README](https://github.com/DiamondLightSource/dls-tiled#dls-access-policy) lists every argument. ## New central authorization diff --git a/docs/explanations/decisions.md b/docs/explanations/decisions.md new file mode 100644 index 0000000..0533b98 --- /dev/null +++ b/docs/explanations/decisions.md @@ -0,0 +1,12 @@ +# Architectural Decision Records + +Architectural decisions are made throughout a project's lifetime. As a way of keeping track of these decisions, we record these decisions in Architecture Decision Records (ADRs) listed below. + +```{toctree} +:glob: true +:maxdepth: 1 + +decisions/* +``` + +For more information on ADRs see this [blog by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). diff --git a/docs/explanations/decisions/0001-record-architecture-decisions.md b/docs/explanations/decisions/0001-record-architecture-decisions.md new file mode 100644 index 0000000..44d234e --- /dev/null +++ b/docs/explanations/decisions/0001-record-architecture-decisions.md @@ -0,0 +1,18 @@ +# 1. Record architecture decisions + +## Status + +Accepted + +## Context + +We need to record the architectural decisions made on this project. + +## Decision + +We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). + +## Consequences + +See Michael Nygard's article, linked above. To create new ADRs we will copy and +paste from existing ones. diff --git a/docs/explanations/decisions/0002-switched-to-python-copier-template.md b/docs/explanations/decisions/0002-switched-to-python-copier-template.md new file mode 100644 index 0000000..66fe5d8 --- /dev/null +++ b/docs/explanations/decisions/0002-switched-to-python-copier-template.md @@ -0,0 +1,28 @@ +# 2. Adopt python-copier-template for project structure + +## Status + +Accepted + +## Context + +We should use the following [python-copier-template](https://github.com/DiamondLightSource/python-copier-template). +The template will ensure consistency in developer +environments and package management. + +## Decision + +We have switched to using the template. + +## Consequences + +This module will use a fixed set of tools as developed in `python-copier-template` +and can pull from this template to update the packaging to the latest techniques. + +As such, the developer environment may have changed, the following could be +different: + +- linting +- formatting +- pip venv setup +- CI/CD diff --git a/docs/explanations/decisions/COPYME b/docs/explanations/decisions/COPYME new file mode 100644 index 0000000..b466c79 --- /dev/null +++ b/docs/explanations/decisions/COPYME @@ -0,0 +1,19 @@ +# 3. Short descriptive title + +Date: Today's date + +## Status + +Accepted + +## Context + +Background to allow us to make the decision, to show how we arrived at our conclusions. + +## Decision + +What decision we made. + +## Consequences + +What we will do as a result of this decision. diff --git a/docs/genindex.md b/docs/genindex.md new file mode 100644 index 0000000..73f1191 --- /dev/null +++ b/docs/genindex.md @@ -0,0 +1,3 @@ +# Index + + diff --git a/docs/how-to.md b/docs/how-to.md new file mode 100644 index 0000000..6b16141 --- /dev/null +++ b/docs/how-to.md @@ -0,0 +1,10 @@ +# How-to Guides + +Practical step-by-step guides for the more experienced user. + +```{toctree} +:maxdepth: 1 +:glob: + +how-to/* +``` diff --git a/docs/how-to/contribute.md b/docs/how-to/contribute.md new file mode 100644 index 0000000..6e41979 --- /dev/null +++ b/docs/how-to/contribute.md @@ -0,0 +1,2 @@ +```{include} ../../.github/CONTRIBUTING.md +``` diff --git a/docs/nexus.md b/docs/how-to/create-nexus-files.md similarity index 98% rename from docs/nexus.md rename to docs/how-to/create-nexus-files.md index ce10925..9313577 100644 --- a/docs/nexus.md +++ b/docs/how-to/create-nexus-files.md @@ -142,7 +142,7 @@ To do something other than write HDF5, subclass `BaseCallback` and implement > Expect this recipe to be replaced. Don't use it for anything that depends on the > file being complete. -This watches a visit with [streaming](streaming.md). While a scan runs, it rebuilds +This watches a visit with [streaming](stream-live-data.md). While a scan runs, it rebuilds the NeXus file from Tiled every few seconds. When the stop document arrives, it writes the file a final time. It uses `run_documents` from [After the scan](#after-the-scan). diff --git a/docs/how-to/get-help.md b/docs/how-to/get-help.md new file mode 100644 index 0000000..161b33d --- /dev/null +++ b/docs/how-to/get-help.md @@ -0,0 +1,19 @@ +# Get help when you can't read data + +Create a ticket in the +[ACQP Jira project](https://jira.diamond.ac.uk/projects/ACQP/issues/). Include: + +- the beamline and visit, for example `p51` and `cm44254-1` +- the full node path, or the run's uid +- what you ran, the full error, and roughly when it happened +- your FedID, so access can be checked + +Before raising a ticket, these symptoms can help you tell what is wrong: + +| Symptom | Likely cause | +| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `401 Unauthorized` | You are not logged in, or your token has expired. Connect again. | +| The node isn't there (`KeyError`, `404`) | You don't have access to that visit, or the path is wrong. If you were only just added to the visit, wait a minute: Tiled remembers your permissions for up to 60 seconds. | +| The metadata reads, but reading the data gives a `500` | Usually a problem with the file, not your permissions: Tiled can't open it, or a dataset is missing from it. The beamline's pod may also lack filesystem access to it. Quote the error's correlation ID in the ticket if you have it. | +| `503` with `Retry-After` | The file is still catching up with the catalog during a scan. Try again shortly. | +| A whole beamline's data is unreadable | The beamline may not have its own Tiled deployment or data ACL yet ([why](../explanations/architecture.md#adding-a-beamline)). Raise a ticket. | diff --git a/docs/how-to/run-container.md b/docs/how-to/run-container.md new file mode 100644 index 0000000..e53b523 --- /dev/null +++ b/docs/how-to/run-container.md @@ -0,0 +1,14 @@ +# Run in a container + +Pre-built containers with dls-tiled and its dependencies already +installed are available on [Github Container Registry](https://ghcr.io/DiamondLightSource/dls-tiled). + +## Starting the container + +To pull the container from github container registry and run: + +``` +$ docker run ghcr.io/diamondlightsource/dls-tiled:latest tiled --version +``` + +To get a released version, use a numbered release instead of `latest`. diff --git a/docs/searching.md b/docs/how-to/search.md similarity index 100% rename from docs/searching.md rename to docs/how-to/search.md diff --git a/docs/streaming.md b/docs/how-to/stream-live-data.md similarity index 98% rename from docs/streaming.md rename to docs/how-to/stream-live-data.md index ff308c4..c9f5b01 100644 --- a/docs/streaming.md +++ b/docs/how-to/stream-live-data.md @@ -85,7 +85,7 @@ start = visit[update.run_uid].metadata_copy()[0].get("start", {}) - **Use `update.sequence` to detect gaps or duplicates** within one node. It doesn't compare across nodes. - **Replay is limited.** By default (`start=0`) you first get whatever is still in - the streaming cache, not the whole history. Use [search](searching.md) for + the streaming cache, not the whole history. Use [search](search.md) for history. - **The end of a stream doesn't mean the data can be read yet.** A REST read made at the same moment may not see the last writes. @@ -137,7 +137,7 @@ Add `&start=1` to replay from the first update still in the cache. Watching a single table gives you rows as they arrive: `/primary/internal` holds a run's scalar values. -[Glazed subscriptions](searching.md#live-events) offer the same events over +[Glazed subscriptions](search.md#live-events) offer the same events over GraphQL. ## Running a server with streaming diff --git a/docs/images/dls-logo.svg b/docs/images/dls-logo.svg new file mode 100644 index 0000000..4fcaa86 --- /dev/null +++ b/docs/images/dls-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..730b3fd --- /dev/null +++ b/docs/index.md @@ -0,0 +1,56 @@ +--- +html_theme.sidebar_secondary.remove: true +--- + +```{include} ../README.md +:end-before: + +::::{grid} 2 +:gutter: 4 + +:::{grid-item-card} {material-regular}`directions_walk;2em` +```{toctree} +:maxdepth: 2 +tutorials +``` ++++ +Tutorials for installation and typical usage. New users start here. +::: + +:::{grid-item-card} {material-regular}`directions;2em` +```{toctree} +:maxdepth: 2 +how-to +``` ++++ +Practical step-by-step guides for the more experienced user. +::: + +:::{grid-item-card} {material-regular}`info;2em` +```{toctree} +:maxdepth: 2 +explanations +``` ++++ +Explanations of how it works and why it works that way. +::: + +:::{grid-item-card} {material-regular}`menu_book;2em` +```{toctree} +:maxdepth: 2 +reference +``` ++++ +Technical reference material including APIs and release notes. +::: + +:::: diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 0000000..cb16460 --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,12 @@ +# Reference + +Technical reference material including APIs and release notes. + +```{toctree} +:maxdepth: 1 +:glob: + +API <_api/dls_tiled> +genindex +Release Notes +``` diff --git a/docs/tutorials.md b/docs/tutorials.md new file mode 100644 index 0000000..1fe66c5 --- /dev/null +++ b/docs/tutorials.md @@ -0,0 +1,10 @@ +# Tutorials + +Tutorials for installation and typical usage. New users start here. + +```{toctree} +:maxdepth: 1 +:glob: + +tutorials/* +``` diff --git a/docs/tutorials/getting-started.md b/docs/tutorials/getting-started.md new file mode 100644 index 0000000..ae5de44 --- /dev/null +++ b/docs/tutorials/getting-started.md @@ -0,0 +1,86 @@ +# Getting started with Tiled + +[Tiled](https://blueskyproject.io/tiled/) is the data access service for scans +run with Bluesky at Diamond. It stores each scan's metadata and tabular data, knows +where its detector files are, and serves all of it over HTTP, with access +checked against your Diamond login. + +| I want to... | Read | +| -------------------------------------------------- | --------------------------------------------------------------------- | +| Find a scan and read its data from Python | [Reading a run](#reading-a-run), [Searching](../how-to/search.md) | +| Query Tiled from a web app or with GraphQL | [Glazed](../how-to/search.md#graphql-with-glazed) | +| Follow a scan live while it runs | [Streaming](../how-to/stream-live-data.md) | +| Produce a NeXus file for a scan | [NeXus files](../how-to/create-nexus-files.md) | +| Understand who can see what | [Authorization](../explanations/authorization.md) | +| Understand how the service is deployed | [Architecture](../explanations/architecture.md) | +| Report that data can't be read | [Getting help](../how-to/get-help.md) | + +## Install and connect + +Install the client. `bluesky-tiled-plugins` gives you Bluesky-aware objects for +runs and event streams, and the live streaming API: + +```sh +pip install "tiled[client]" bluesky-tiled-plugins +``` + +Connect. The first time, Tiled opens a browser so you can log in with your +Diamond account. Tokens are then cached in `~/.cache/tiled/tokens`. + +```python +from tiled.client import from_uri + +client = from_uri("https://tiled.diamond.ac.uk") +``` + +## How the data is organised + +Tiled is a tree. Runs are written by blueapi under the visit they belong to, so +you normally index straight to your visit: + +``` +///// +p51/cm44254/cm44254-1/9a812aec-b092-4fb4-9c8e-c58beac77fdc/primary/panda1-counter-1-out_dataset +``` + +```python +visit = client["p51/cm44254/cm44254-1"] +``` + +> **Changing soon.** The [central authorization work](../explanations/authorization.md#new-central-authorization) +> removes the proposal level and adds folders per visit, so the same visit will +> be at `p51/cm44254-1/raw`. Code that builds these paths will need updating +> when that is released. + +## Reading a run + +```python +run = visit["9a812aec-b092-4fb4-9c8e-c58beac77fdc"] + +run.metadata["start"] # the Bluesky start document +run.metadata.get("stop") # the stop document; missing while the scan runs +list(run) # stream names, e.g. ["primary", "baseline"] + +ds = run["primary"].read() # the whole stream as an xarray.Dataset +values = run["primary"]["panda1-counter-1-out_dataset"].read() # one signal +``` + +Detector data can be many gigabytes. Slice arrays to read only what you need: + +```python +images = run["primary"]["detector"] # nothing is downloaded yet +first_frame = images[0] +``` + +## Common use cases + +- **Find scans in a visit** by plan, scan number, outcome or time: + [Searching](../how-to/search.md). +- **Analysis scripts** read arrays and tables as NumPy, pandas or xarray objects, + without needing to know file paths. +- **Live displays** get each point as it is taken: [Streaming](../how-to/stream-live-data.md). +- **Web applications** use the GraphQL interface, [Glazed](../how-to/search.md#graphql-with-glazed), + or the REST API. Tiled's interactive API docs are at `/docs` on the server. +- **ViSR imaging** has a server-side binning endpoint, described in the + [project README](https://github.com/DiamondLightSource/dls-tiled#dls_tiledvisrvisr_router). +- **NeXus files** can be generated from a run: [NeXus files](../how-to/create-nexus-files.md). diff --git a/docs/tutorials/installation.md b/docs/tutorials/installation.md new file mode 100644 index 0000000..1a761fc --- /dev/null +++ b/docs/tutorials/installation.md @@ -0,0 +1,35 @@ +# Installation + +## Check your version of python + +You will need python 3.11 or later. You can check your version of python by +typing into a terminal: + +``` +$ python3 --version +``` + +## Create a virtual environment + +It is recommended that you install into a “virtual environment” so this +installation will not interfere with any existing Python software: + +``` +$ python3 -m venv /path/to/venv +$ source /path/to/venv/bin/activate +``` + +## Installing the library + +You can now use `pip` to install the library and its dependencies from github: + +``` +$ python3 -m pip install git+https://github.com/DiamondLightSource/dls-tiled.git +``` + +The library should now be installed and the commandline interface on your path. +You can check the version that has been installed by typing: + +``` +$ dls-tiled --version +``` diff --git a/pyproject.toml b/pyproject.toml index ed48214..e53139f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,11 +21,16 @@ requires-python = ">=3.11" [dependency-groups] dev = [ + "myst-parser", "pre-commit", + "pydata-sphinx-theme>=0.12", "pyright", "pytest", "pytest-cov", "ruff", + "sphinx-autobuild", + "sphinx-copybutton", + "sphinx-design", "tox-uv", "types-mock", ] @@ -74,7 +79,7 @@ source = ["src", "**/site-packages/"] [tool.tox] skipsdist = true # envs to runs automatically with tox -p -env_list = ["pre-commit", "type-checking", "tests"] +env_list = ["pre-commit", "type-checking", "tests", "docs"] [tool.tox.env_run_base] # Pass all environment variables to the test environments @@ -122,6 +127,37 @@ commands = [ ], ] +[tool.tox.env.docs] +description = "Run docs build with clean environment" +commands = [ + [ + "sphinx-build", + "--fresh-env", + "--show-traceback", + "--fail-on-warning", + "--keep-going", + "docs", + "build/html", + { replace = "posargs", default = [ + ], extend = true }, + ], +] + +[tool.tox.env.docs-autobuild] +description = "Run docs with autobuild on change" +commands = [ + [ + "sphinx-autobuild", + "--show-traceback", + "--watch", + "README.md", + "docs", + "build/html", + { replace = "posargs", default = [ + ], extend = true }, + ], +] + [tool.ruff] src = ["src", "tests"] line-length = 88 diff --git a/uv.lock b/uv.lock index 785eb02..7d7b958 100644 --- a/uv.lock +++ b/uv.lock @@ -1,16 +1,22 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] [manifest] @@ -19,6 +25,18 @@ constraints = [ { name = "starlette", specifier = "<1.7" }, ] +[[package]] +name = "accessible-pygments" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, +] + [[package]] name = "adbc-driver-manager" version = "1.12.0" @@ -107,6 +125,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, ] +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + [[package]] name = "alembic" version = "1.20.0" @@ -383,6 +410,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/54/120c5b07f146c219cd609c5c58835ba41d64982b953eaa02ec19f3ece412/awkward_cpp-57-cp315-cp315t-win_arm64.whl", hash = "sha256:0c11360f090a7bec0516adef0a6c7f82d12bb8393c989afdabb40de1e6d6822f", size = 908079, upload-time = "2026-09-17T19:56:45.527Z" }, ] +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve", version = "2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11.5'" }, + { name = "soupsieve", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11.5'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, +] + [[package]] name = "blosc2" version = "4.14.1" @@ -556,6 +606,153 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz", hash = "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef", size = 172659, upload-time = "2026-09-30T04:39:23.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/67/6a0b94a7960d5e1b5eacd2fb529f3fccc47db4644f7f0a7cfdcfc3be578a/charset_normalizer-3.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d21b8b13c7592db2ac5e544a6d83187b995257472b0c9e8351b6d507ae37ed6", size = 370642, upload-time = "2026-09-30T04:35:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/fb/94/01009e13b94041599004edf32e56e382c24e570f60f79bab8efe45cfe1eb/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d760fe2a4d7c3b226cb9026d6a842868d52a7901bd98420e1baf14e80da85cf5", size = 259280, upload-time = "2026-09-30T04:35:08.448Z" }, + { url = "https://files.pythonhosted.org/packages/66/85/3b5358f60a13210f0b67d3755c168ef758701b021e655d88d4da28554467/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9790464842f85f437dbbb54417eda1e0e6bfc52dd8d22d6fd1c994b73b2dc74", size = 245335, upload-time = "2026-09-30T04:35:10.104Z" }, + { url = "https://files.pythonhosted.org/packages/74/75/77c1c479b09ecd751d1e767b251ea5c14d4d50ff757bf404afab2692f600/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4685902cf26edf013ed7a3da0f426ebba7a00ebb9541386d835afbf002c11cab", size = 291707, upload-time = "2026-09-30T04:35:11.575Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0d/363f78cacb70f58f15f4b083961bbd9d292f335d3f5c66fc4f1cfe69cb90/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4495c5002a7b28557e7e222e77e0b661183e432b7d6d2e788101e3f240e05b8c", size = 286747, upload-time = "2026-09-30T04:35:13.022Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ed/cf505d3011ffceb12c2067a7a5d3cfe92b875d4d44bb0ff0d69375e2c184/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:211d5a3eb6af8f513b8d4ca19a8c1b7accab1b5f0d3175f9826b03c1a920dc1f", size = 269972, upload-time = "2026-09-30T04:35:14.606Z" }, + { url = "https://files.pythonhosted.org/packages/15/d8/f0a93a431d170e7ca681d4f6650fee3de934d18560e474e7267eb4b0f987/charset_normalizer-3.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef4fcbf3327382cd4c9f540babd61248208af7b93eec4de397b4d5f58a09e288", size = 269339, upload-time = "2026-09-30T04:35:16.087Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/9b2bd1c5b7af02462c9752d33994834ff972a96b4c483eefde9e594488e2/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd16aabe4a02a297c23417aa17ac6299dbd8c49f673bcd645b4929b11f5a4400", size = 261683, upload-time = "2026-09-30T04:35:17.488Z" }, + { url = "https://files.pythonhosted.org/packages/76/a5/cac540ab0fd61f3fec88ad3dbb64509e71424593d73cfdfff5ab3e4db279/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fb9e68df06293761f9fe66ade60a9bc6d0f5e42b8acf2939a9158af86ab0e5bd", size = 248143, upload-time = "2026-09-30T04:35:18.849Z" }, + { url = "https://files.pythonhosted.org/packages/71/7a/ff467301deef2089fad87f72df9e000a26a78fec7acbb18e1999371b8369/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:59f63901b0031c3136cf64704dcb21de0bbae62ce2c9529bc39d27665463de37", size = 292358, upload-time = "2026-09-30T04:35:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/22d7e785d1e210afc2e2f58600dd1799d17a35665faf84383f002826c5f8/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:304d5463e65a35d7bb0850550e0780395395f6fcf452f04db7d5ca7cecc425ac", size = 268766, upload-time = "2026-09-30T04:35:21.72Z" }, + { url = "https://files.pythonhosted.org/packages/ae/91/e8e946267f1c2d9e2bd651726e2fbd2addf02c4d36cea5069e32ca9d7bb5/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9cf9b1a857e25c4baceeb3624e92a56df3668f398c4acba74e174d81fb4d1d3a", size = 287432, upload-time = "2026-09-30T04:35:23.273Z" }, + { url = "https://files.pythonhosted.org/packages/4e/88/7561d8a88d555e7df6623abe7c0070b4baf47549b9408783a2ae0a1a6cf7/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:114e4d0c92d618409ed82a99e22b5c5e768fe995f2973f78265f4524f49d4640", size = 272470, upload-time = "2026-09-30T04:35:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/35/7e/578c702301ec036f01455f30744a08d2b42f6ab35b9b2d4bf8cae0ef2a80/charset_normalizer-3.5.2-cp311-cp311-win32.whl", hash = "sha256:2625388c6c754520c37abaf3b41eb34d1cc4a373f457898f08606c8e362b891d", size = 186688, upload-time = "2026-09-30T04:35:26.225Z" }, + { url = "https://files.pythonhosted.org/packages/e8/fc/fdf8cf52ff21cd5bf158f20978991cf985325842f74283eb6df26c8a39d8/charset_normalizer-3.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:87e50a3e7cb90af586b6c5faf23e302a970415ac73bd7bd90a515a04b427ef96", size = 214932, upload-time = "2026-09-30T04:35:27.796Z" }, + { url = "https://files.pythonhosted.org/packages/97/66/3e45a506d8110b632541faf9a9470185aa9878f1ed44020f31346c1c5e5b/charset_normalizer-3.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:254eb48b9fa5ee9898a3c445825a1f340fe53712a098904b39b0bddba8ea3cb1", size = 202828, upload-time = "2026-09-30T04:35:29.259Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/693809898870237d82785a03f3b2b58fe4c9f14669f84a7d4e623c92a59e/charset_normalizer-3.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491", size = 367780, upload-time = "2026-09-30T04:35:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/c9/87/2fea8c13dc24b3ca9c6f803a5b2dfdeae73eb4f9e12c7885ed908ff0433c/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c", size = 246730, upload-time = "2026-09-30T04:35:32.286Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9e/09efac30b937722f46d3110ba30b875b24b2e3a266ed746cc4e376a94d80/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0", size = 237707, upload-time = "2026-09-30T04:35:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/9e/18/70d76670b13686237863a379928d60bd10e021f17d243ab3d7014c4a5f4e/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51", size = 273050, upload-time = "2026-09-30T04:35:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/54/e2/77a8b09d5adc013ed07b95b01b8b8fa5441c4e810e83ee7e4aae2fa4d91a/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5", size = 270345, upload-time = "2026-09-30T04:35:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c5/38806a25ab5e65fc178f39affeda20858efafede2fce1ffc2556cfc9fe73/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649", size = 257601, upload-time = "2026-09-30T04:35:37.919Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8d/213565184708fdb263ae55e2c04ee1ff748129dd65d48ed0e3502da9c85a/charset_normalizer-3.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e", size = 252222, upload-time = "2026-09-30T04:35:39.544Z" }, + { url = "https://files.pythonhosted.org/packages/7e/24/76d2cefc25472531e4c5c7dfff68865eb1c39b78482f0fdc15b46f047830/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346", size = 248482, upload-time = "2026-09-30T04:35:41.088Z" }, + { url = "https://files.pythonhosted.org/packages/7d/dc/65a801b66ab4c197e22c433ab25e7ac24324ac6f45a2269aca42cce309bf/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1", size = 241206, upload-time = "2026-09-30T04:35:42.59Z" }, + { url = "https://files.pythonhosted.org/packages/a7/95/ca9b5eabde673002c6f1e7ada1b223916fe18f6d661da7aabd4d643718f1/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875", size = 273190, upload-time = "2026-09-30T04:35:44.347Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8b/803b4d2a3f6e1740f63f1e87b04d14b42f3d4fdfe6ed7d4db2d34102b14f/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1", size = 253527, upload-time = "2026-09-30T04:35:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/55/93c0e5dbd085ae0471346026abbe7e0db9ea2d6fea74e51f0b5a46f233a7/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413", size = 271285, upload-time = "2026-09-30T04:35:47.49Z" }, + { url = "https://files.pythonhosted.org/packages/95/69/0dbd0e0b9b16cfa816cdfcb3e2e3854a1f680dc07fb1245ea125e7448060/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869", size = 260010, upload-time = "2026-09-30T04:35:48.996Z" }, + { url = "https://files.pythonhosted.org/packages/58/9d/e7b88e7b1bf403590c3b573277b5e1e488c68c7a6fbacca310a2c324e90c/charset_normalizer-3.5.2-cp312-cp312-win32.whl", hash = "sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e", size = 184125, upload-time = "2026-09-30T04:35:50.777Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e6/e6e083884cbcfd49c64865af05027fe7011be7b2d9179524f099a1b611f3/charset_normalizer-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc", size = 207486, upload-time = "2026-09-30T04:35:52.194Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e3/017aea0911ada7405a825c7d937eb3a13009664e2f5b38e8c4bbf2abf894/charset_normalizer-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3", size = 196734, upload-time = "2026-09-30T04:35:53.636Z" }, + { url = "https://files.pythonhosted.org/packages/c5/34/68292d68512768591aaff07c59bb53ee31341c87759433a859c4641a50c2/charset_normalizer-3.5.2-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5", size = 225946, upload-time = "2026-09-30T04:35:55.313Z" }, + { url = "https://files.pythonhosted.org/packages/e3/80/bee0b01b90ccd5322ae1d0abb33fab1bd95b7c2eadaf02aeccf22e04ee83/charset_normalizer-3.5.2-cp313-cp313-android_24_x86_64.whl", hash = "sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e", size = 238619, upload-time = "2026-09-30T04:35:56.863Z" }, + { url = "https://files.pythonhosted.org/packages/78/6e/60ce52a85a7fd631ae8482ae6d74521014ca2f255892679484dc04d7ef56/charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a", size = 206701, upload-time = "2026-09-30T04:35:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/36/8c/71aafad23f971afc84c2b295bc0c560739ce1dac558aad9fec22e39f3639/charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d", size = 210506, upload-time = "2026-09-30T04:36:00.147Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/3c5a7798c046df7d2d68ad653cf5b6c5a8bfee225055a843c6f2f42aac1a/charset_normalizer-3.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055", size = 366626, upload-time = "2026-09-30T04:36:01.77Z" }, + { url = "https://files.pythonhosted.org/packages/e1/16/710ac3de2ee354e2bd1a9c94efe45a2d27b5c6ad39b2d6a905be2c094b6c/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858", size = 244624, upload-time = "2026-09-30T04:36:03.389Z" }, + { url = "https://files.pythonhosted.org/packages/d6/39/45c7439f5b63d24f7d5b2a1d760f34af7628782d7144b4cc8ded45c2d4bc/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234", size = 235731, upload-time = "2026-09-30T04:36:04.987Z" }, + { url = "https://files.pythonhosted.org/packages/4d/34/38f3154785ce92e9f56eb226f4d35bdfae6b008480dd055f58837a89c810/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21", size = 270352, upload-time = "2026-09-30T04:36:06.412Z" }, + { url = "https://files.pythonhosted.org/packages/04/f3/859f74e7babc977705026b30593b3be04049632a522fb7000f83c033d747/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718", size = 267393, upload-time = "2026-09-30T04:36:07.865Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/41d27f234b82e47c167a5f6c0f62501dc0c640585ff4aba79e08a390336a/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4", size = 254660, upload-time = "2026-09-30T04:36:09.248Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/5d1a997587febe5b26d8daffe363b5c1a091cece19828eec6502fd09c5ef/charset_normalizer-3.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3", size = 249996, upload-time = "2026-09-30T04:36:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1f/d1e78246f7ed60c8c8d606b4ac27f66ce49cc3e95f24893ccbeba9f77302/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c", size = 247028, upload-time = "2026-09-30T04:36:12.294Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/eba316edd4f0c4d3a5d945924c4eeeae59abac4056aa815d8a4268f863a2/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429", size = 240562, upload-time = "2026-09-30T04:36:13.887Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8e/aaa037d40ca9ef045977f1a661048b1aa33f223adfce3452fe9be9f79d14/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f", size = 271699, upload-time = "2026-09-30T04:36:15.41Z" }, + { url = "https://files.pythonhosted.org/packages/26/19/1c1c9f75974adf523b87f34b8a2adc5a435cd65916812bcbd0dfa45f9a29/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a", size = 252030, upload-time = "2026-09-30T04:36:16.839Z" }, + { url = "https://files.pythonhosted.org/packages/bc/90/0660ef18e18df0a4d2a1a0edff7dfbba42d4e50ef2425557a5bb7051f77b/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00", size = 268565, upload-time = "2026-09-30T04:36:18.468Z" }, + { url = "https://files.pythonhosted.org/packages/79/ba/57adc269824e8658f1a0f97a9e514c247445a9632b3419b97e0ba37f16dc/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d", size = 257627, upload-time = "2026-09-30T04:36:19.938Z" }, + { url = "https://files.pythonhosted.org/packages/9a/85/33abd4315c052d3d4f54c92b1ee49bfbc0dc7115a981e462a793b6d2ab87/charset_normalizer-3.5.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3", size = 143407, upload-time = "2026-09-30T04:36:21.376Z" }, + { url = "https://files.pythonhosted.org/packages/4f/de/6435e18d1aaa5d910b896d551411c96af1f42a0c56c29afc2016c61ccc2e/charset_normalizer-3.5.2-cp313-cp313-win32.whl", hash = "sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd", size = 183207, upload-time = "2026-09-30T04:36:22.776Z" }, + { url = "https://files.pythonhosted.org/packages/9c/76/b8ec57f4e9ee3253541abf95e4a462c0175fe8032dcd070f1f2421240942/charset_normalizer-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639", size = 206489, upload-time = "2026-09-30T04:36:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/c647c6ae47480221e875ea5d743ff94946f7416e3c69415ab772928e8d32/charset_normalizer-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3", size = 195501, upload-time = "2026-09-30T04:36:25.846Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/7aa91362a2f77ac8e9e28a9b902a74f7d0e11a851ef0d27a74308da8cd90/charset_normalizer-3.5.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187", size = 226010, upload-time = "2026-09-30T04:36:27.669Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/ac8878d0322cf88a1aad4c7b147db32ca0bd806eb0060957b2e31486dbe6/charset_normalizer-3.5.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad", size = 239266, upload-time = "2026-09-30T04:36:29.434Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6d/9a08d7e0b29b7208e2c6c01dc56c8e0520e7c7beadbbfb024b58fd69c8a5/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf", size = 206783, upload-time = "2026-09-30T04:36:30.872Z" }, + { url = "https://files.pythonhosted.org/packages/82/44/b0aa350280e6ff5a5492d17cf10460dd39d5ee848f872f7ba2df10607f60/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995", size = 210449, upload-time = "2026-09-30T04:36:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/40db9aa9f5907bb0e6f8b6d64064bf8852fb33d4b813ff9414911df7647c/charset_normalizer-3.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424", size = 367815, upload-time = "2026-09-30T04:36:34.197Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/9c5e7707b57c8ddfa9ddf7b0b1d009d7fbab9e9e887d5b721060f37e307d/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13", size = 247095, upload-time = "2026-09-30T04:36:35.803Z" }, + { url = "https://files.pythonhosted.org/packages/83/09/71e453691e927de4ddf792770cfaab3f49d494e222f66ea5e404bbd5e39c/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d", size = 233942, upload-time = "2026-09-30T04:36:37.407Z" }, + { url = "https://files.pythonhosted.org/packages/9f/86/85c84e4da8b27dd409577d9437926ff581c5f9d3c66038dc68c1a526de51/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4", size = 273762, upload-time = "2026-09-30T04:36:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/564955a4b5f2ccb410ab480bbe8c6a18063ff27f2d35458731c4a5335df9/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438", size = 269514, upload-time = "2026-09-30T04:36:40.469Z" }, + { url = "https://files.pythonhosted.org/packages/18/24/bad3ac4271589df29cf5ce2f5ae490518a5739358052bd0d61209e6fea54/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a", size = 255056, upload-time = "2026-09-30T04:36:42.02Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3e/350d89ad49916b86554d6f5f2d03ec1152148f87e5ff735106c6a03b1a36/charset_normalizer-3.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56", size = 251311, upload-time = "2026-09-30T04:36:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/56/5b/4970a2d154df502e133402906dd04e3ae7cada7b3011283c88d0479a2585/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd", size = 250190, upload-time = "2026-09-30T04:36:45.185Z" }, + { url = "https://files.pythonhosted.org/packages/88/8c/f1a91bddc8fb47c2889e29ea7ea49a194eb0d9868675d786806519c00d76/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204", size = 237098, upload-time = "2026-09-30T04:36:46.689Z" }, + { url = "https://files.pythonhosted.org/packages/24/0e/bb5dace3cc7e79068425386a6589c19b5a2ab5fefc2a46abea6919683332/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7", size = 274228, upload-time = "2026-09-30T04:36:48.31Z" }, + { url = "https://files.pythonhosted.org/packages/9d/79/b849ad523017ea9f5a45581bbebed91439e0cf42fd2860a6f64e358eb5a6/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd", size = 252401, upload-time = "2026-09-30T04:36:50.091Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/75469d690cf47200bce8f6cad7655724fc23148e147abfc5ce78b5f65863/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc", size = 271846, upload-time = "2026-09-30T04:36:51.719Z" }, + { url = "https://files.pythonhosted.org/packages/26/cd/6d52d3c7437cdcf2e310ce9f28f282e733d4ef60ed19105d1819c356255f/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874", size = 258151, upload-time = "2026-09-30T04:36:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4c/070b38bdb5f49a70199fce923ec0726a49536a63ab262abbfcaaf351110b/charset_normalizer-3.5.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655", size = 143570, upload-time = "2026-09-30T04:36:54.816Z" }, + { url = "https://files.pythonhosted.org/packages/81/84/9ebfc8ed6c8c4fcd8e726ff6bf220cc8deb3966e31dce9be8dd8aa017e64/charset_normalizer-3.5.2-cp314-cp314-win32.whl", hash = "sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0", size = 186412, upload-time = "2026-09-30T04:36:56.643Z" }, + { url = "https://files.pythonhosted.org/packages/d1/78/5ed86f743d4bc350db307e7636419a0a5ee1d91806d30c7f667bd5c80dae/charset_normalizer-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c", size = 210965, upload-time = "2026-09-30T04:36:58.205Z" }, + { url = "https://files.pythonhosted.org/packages/53/94/a3a7698e9b1a395e1eb99ccd9a324be9347973bff4e72db2a06496d7cd27/charset_normalizer-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253", size = 200406, upload-time = "2026-09-30T04:36:59.764Z" }, + { url = "https://files.pythonhosted.org/packages/c1/48/c5dd00d5ef7791f02666de250a5bb6071e29b7e133cf4b835800b6d3bc27/charset_normalizer-3.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709", size = 396871, upload-time = "2026-09-30T04:37:01.543Z" }, + { url = "https://files.pythonhosted.org/packages/12/c8/8379554b42e8368161d898476686947a0fdbd3e8865170d7909dcabfdee8/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084", size = 249711, upload-time = "2026-09-30T04:37:03.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/eb/2ddb1035d17320caa9f41682935123a9a250277b261c3efc86b2d2a21343/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb", size = 231146, upload-time = "2026-09-30T04:37:04.721Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/2ecb4bde104322cd7859d6594fcfa74649f8d90b3221c9feecbef149875b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f", size = 271552, upload-time = "2026-09-30T04:37:06.295Z" }, + { url = "https://files.pythonhosted.org/packages/3f/98/9d5f6ebc3aee9fef5d30b4aff11fb2ab7a1222b4064f8ef2c7c87cde217a/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09", size = 269810, upload-time = "2026-09-30T04:37:07.905Z" }, + { url = "https://files.pythonhosted.org/packages/09/e1/a3b06a10461b1b7628853c934c644e03bc28e42767116afb52f19a56519b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80", size = 259845, upload-time = "2026-09-30T04:37:09.554Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/6f561f74a296cf27d61775a1dc665ad13f3bff6a798810ca05907f37a7c4/charset_normalizer-3.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c", size = 252825, upload-time = "2026-09-30T04:37:11.274Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/69e13ca3b18f43e0eafcd34c04a45b732ae22a43b54a5fc9e119103356eb/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f", size = 252659, upload-time = "2026-09-30T04:37:12.941Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/ace29806a0dae18939919c76ba526472d83214afa101105fabff2cf30625/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03", size = 238819, upload-time = "2026-09-30T04:37:14.659Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c1/6116d52a2e3311ec80f21f5fb5e17b27405f10b9608af8f6e69516841a1b/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604", size = 272973, upload-time = "2026-09-30T04:37:16.346Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/9955c7e93bba10a9c7e8f7a5031b7ced66f3a1883a55c00712b8d5850ff3/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8", size = 253692, upload-time = "2026-09-30T04:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/bb/33/2a6ae7fdc1b10cb581cef91addd8cdfc5f40d50abb5702309369d5834579/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93", size = 268757, upload-time = "2026-09-30T04:37:19.877Z" }, + { url = "https://files.pythonhosted.org/packages/a2/22/80992720a0282cd39bba1db35868e6b9c22f41281160143a836544bc1d8a/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915", size = 262220, upload-time = "2026-09-30T04:37:21.583Z" }, + { url = "https://files.pythonhosted.org/packages/92/9f/181fd07e1bffea1d95cd80c84ac537354f50699c22cfc4d3c02b6fc16208/charset_normalizer-3.5.2-cp314-cp314t-win32.whl", hash = "sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5", size = 197797, upload-time = "2026-09-30T04:37:23.235Z" }, + { url = "https://files.pythonhosted.org/packages/49/1c/25d8415ec1c4f2f41f1680435e4c87cfb378ff2f677d950946f2a45d0632/charset_normalizer-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc", size = 222765, upload-time = "2026-09-30T04:37:24.891Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b4/46b48f013dadfc0d0d33b375438e31bdf5a989dc68389c6bf627054d4df9/charset_normalizer-3.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105", size = 210985, upload-time = "2026-09-30T04:37:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e9/34e597dee616d0b8ee4b34d29399e85c2204ade174157a48505d42baa4ff/charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26", size = 367676, upload-time = "2026-09-30T04:37:28.329Z" }, + { url = "https://files.pythonhosted.org/packages/60/9f/a5d1c91c0263745e2cd344c5a4415d787c575501ab1d449f1148ac6b495d/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364", size = 248414, upload-time = "2026-09-30T04:37:30.167Z" }, + { url = "https://files.pythonhosted.org/packages/26/79/e697f77464748a3ee3cf490c83d592459400d4898380d66c38366b03080c/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253", size = 244775, upload-time = "2026-09-30T04:37:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/3d42a42e18ea066e2513936fd678a00696e77878b5ae04528976abdbcb83/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0", size = 273877, upload-time = "2026-09-30T04:37:33.661Z" }, + { url = "https://files.pythonhosted.org/packages/c3/76/8a28136f3938ba9836f84280ce0c4d61ed1cf15a036b2034900c62634162/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc", size = 269455, upload-time = "2026-09-30T04:37:35.573Z" }, + { url = "https://files.pythonhosted.org/packages/a0/a1/4fbf5d0f0f1b2a080474c1cf9a2f12c4c6531bb0e8ba591055e846d2b4e9/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229", size = 257336, upload-time = "2026-09-30T04:37:37.397Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a2/8b50aa320adb880ad579518e6f718f24944804b42a88b83d267d5d444125/charset_normalizer-3.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5", size = 257584, upload-time = "2026-09-30T04:37:39.522Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/50e3fed84e175f40349bd0da7a4fce94c87f0378f52d74f511d89e0bdc20/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98", size = 251854, upload-time = "2026-09-30T04:37:41.23Z" }, + { url = "https://files.pythonhosted.org/packages/d6/54/f7fbb3493c9f49091213b9c2d6dd65800696f1ce1a3f196a4205f50417b1/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3", size = 248767, upload-time = "2026-09-30T04:37:42.883Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/b3a6385acc5a1e45b39ae9c90bfb9cf838a09b9dd37ef2740ab4c6b4a2eb/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2", size = 274479, upload-time = "2026-09-30T04:37:44.658Z" }, + { url = "https://files.pythonhosted.org/packages/89/44/809913e2cfd279e635a9294fdbbfb1b1dc62a8189d473d561f649fce98d8/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf", size = 258310, upload-time = "2026-09-30T04:37:46.529Z" }, + { url = "https://files.pythonhosted.org/packages/af/a2/f28400ab13359d91bd39179df8e149376b9bf36588e739a3a4f9de2b84b2/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95", size = 271100, upload-time = "2026-09-30T04:37:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/e9/89/9bab37955edf0adb3b66f8a3a6617d9f2f487e0d56f295a6a286cb640aa6/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d", size = 259936, upload-time = "2026-09-30T04:37:50.023Z" }, + { url = "https://files.pythonhosted.org/packages/23/b5/4459e08d45a679f903d50fea08bc52cfa728cca4d7bd02c757b5e5abda2e/charset_normalizer-3.5.2-cp315-cp315-win32.whl", hash = "sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847", size = 186301, upload-time = "2026-09-30T04:37:51.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/e8/55d5fd3935b4bce6da4fe0df61898e8c82653e317e677bd58aceb9c60f13/charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8", size = 210858, upload-time = "2026-09-30T04:37:53.427Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/974423c2fd8e524c7a7f64318c1e02240ef954912fa2b4d70344107b9c68/charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a", size = 200309, upload-time = "2026-09-30T04:37:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f9/00ee0195db1013d8f7c416fd770fbeb560bb46eb2e36b054d05cb56f6cfa/charset_normalizer-3.5.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1", size = 395343, upload-time = "2026-09-30T04:37:56.743Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/c00b50e94c964cf934c7899cd47c97952fc11dad71cc5884b3c61795b09b/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b", size = 250114, upload-time = "2026-09-30T04:37:58.607Z" }, + { url = "https://files.pythonhosted.org/packages/50/27/d102dc880bbcffd0479ab64dfc1fb96777a854355a55e2bda72a71efadcb/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f", size = 239997, upload-time = "2026-09-30T04:38:00.511Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4a/bf7ef45794dd293fab5f98a9309817977fbb845b9998f171b8cc5d8437a3/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3", size = 272313, upload-time = "2026-09-30T04:38:02.509Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ee/008a2837737991474c5754bb3191010007663860979701990982a502cbaf/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e", size = 269986, upload-time = "2026-09-30T04:38:04.435Z" }, + { url = "https://files.pythonhosted.org/packages/93/ad/bd74a283940dc910c5b14f8e4f80a248082bc9c0fcbe1f54530cb6d9cc5e/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9", size = 260202, upload-time = "2026-09-30T04:38:06.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7b/ed341c66f69f688723501fac752be3d63c7159ca0d0d4174fc611e5710bb/charset_normalizer-3.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a", size = 259083, upload-time = "2026-09-30T04:38:08.311Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9d/e41588b777965e5031a43128a1e96173ebb35ac75fc53ec3b517e7c21cd4/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115", size = 253081, upload-time = "2026-09-30T04:38:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/81/35/b761eb6d8c1eb218b9b42b9b4d5ac902afdc399fb6dac6f9a9aac7bda589/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c", size = 248712, upload-time = "2026-09-30T04:38:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2c/147169a041b747759f37405c0a97157e8e92de967968373101ff14915cba/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d", size = 274176, upload-time = "2026-09-30T04:38:14.138Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2d/0ff8db0d373ba8538db686db11cd7e8912031490b9e4f383b41912e8d594/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d", size = 259413, upload-time = "2026-09-30T04:38:15.841Z" }, + { url = "https://files.pythonhosted.org/packages/8a/8e/b4a085fb47c9d3a7e43576a4784fdd8fe23f907514a972de8086edaf7a48/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4", size = 269434, upload-time = "2026-09-30T04:38:17.626Z" }, + { url = "https://files.pythonhosted.org/packages/83/1c/d8d8d7322a7c3eecdf3237a4a419cf41d2eaad8e006ce7dfdd9d4c8fa2eb/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b", size = 262716, upload-time = "2026-09-30T04:38:19.214Z" }, + { url = "https://files.pythonhosted.org/packages/a0/16/0e4c6ba9b44e97a2da150e52d331e8f9c968b21b358fbffa6c856cebcd89/charset_normalizer-3.5.2-cp315-cp315t-win32.whl", hash = "sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800", size = 197159, upload-time = "2026-09-30T04:38:21.037Z" }, + { url = "https://files.pythonhosted.org/packages/be/33/e90bc2b1374f7f36ef106f56620de5a783907e19ca857efe2277e31cac3e/charset_normalizer-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21", size = 221322, upload-time = "2026-09-30T04:38:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/66/89/dfa6dcb08c200b7830ab56439e8c1890f2971d51aafbb3937894a2e7fcfc/charset_normalizer-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58", size = 209192, upload-time = "2026-09-30T04:38:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ab/176fbfd5b64939c55d652366aa5b9ef1d767af207a3aa6ebeb0d226c484d/charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd", size = 331815, upload-time = "2026-09-30T04:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7", size = 253276, upload-time = "2026-09-30T04:38:28.032Z" }, + { url = "https://files.pythonhosted.org/packages/43/6f/c4fbae58febff71709c51bc7e18fdfa55341dc382704740f9f0cbf03817b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f", size = 241239, upload-time = "2026-09-30T04:38:29.732Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/458c3f42164a07d0c5210798e9e704b39e540a6793b05aba67f3a35243a9/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93", size = 231121, upload-time = "2026-09-30T04:38:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/ab9e89367076f6331bb6c65c4bf14a5361fa5191cb6561bf534f18504e1b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade", size = 260350, upload-time = "2026-09-30T04:38:33.239Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/061431ecc688d9d76602502cb57cc01e691e682c18f1beb45f9673b5bbd2/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0", size = 255430, upload-time = "2026-09-30T04:38:34.865Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1f/20c8949f0676f7ab811abdeb7f4d7f1cbc6e61ff20bef08b44edeb092bc8/charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26", size = 250612, upload-time = "2026-09-30T04:38:36.649Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/46f2fa4c431fc98c4ae76a8cb5bdca54e0341e3cfc3fcfd8e82740250818/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011", size = 242083, upload-time = "2026-09-30T04:38:38.26Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/559be29a0c0f086e0bba6922babd38916cc5e0b58ced4de13ee01ea05508/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621", size = 232738, upload-time = "2026-09-30T04:38:39.81Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6c/387b0e4f756a282831c1d9fc6aeb6c51ca4507ca202767c8de15ce9b12e2/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4", size = 260703, upload-time = "2026-09-30T04:38:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/1fdf015f09ef449f50d3ac4b67c90887c9c318b727daa95cc4f866e6521d/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e", size = 247622, upload-time = "2026-09-30T04:38:42.937Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3c/8e7b8a5671ad5d433669fb2a76f1a0164df2d9b1718b0206bc2a16d840cc/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c", size = 257500, upload-time = "2026-09-30T04:38:44.604Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f0/45b579df5cabc1d5d53ea1cc35e8437d3ca768c0acccc7041517cb6fbb32/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0", size = 255100, upload-time = "2026-09-30T04:38:46.289Z" }, + { url = "https://files.pythonhosted.org/packages/31/68/fdec18a343f5fb3f310588dd478b09ac4799e0b187dbade3a8cd776f03ef/charset_normalizer-3.5.2-cp37-abi3-win32.whl", hash = "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf", size = 174499, upload-time = "2026-09-30T04:38:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8a/b618149cc5207943a0242068d7a27897f56a62947b5a039085f2a22029f8/charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036", size = 200092, upload-time = "2026-09-30T04:38:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/03/cf/4c66866fa9e2b1c78e3c911516d1de497a677b7ac60f1eceda74ce777ca3/charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl", hash = "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e", size = 294363, upload-time = "2026-09-30T04:38:51.312Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl", hash = "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685", size = 68872, upload-time = "2026-09-30T04:39:21.828Z" }, +] + [[package]] name = "click" version = "8.5.0" @@ -833,11 +1030,16 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "myst-parser" }, { name = "pre-commit" }, + { name = "pydata-sphinx-theme" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, + { name = "sphinx-autobuild" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-design" }, { name = "tox-uv" }, { name = "types-mock" }, ] @@ -850,15 +1052,29 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "myst-parser" }, { name = "pre-commit" }, + { name = "pydata-sphinx-theme", specifier = ">=0.12" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, + { name = "sphinx-autobuild" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-design" }, { name = "tox-uv" }, { name = "types-mock" }, ] +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + [[package]] name = "donfig" version = "0.8.1.post1" @@ -1300,6 +1516,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, ] +[[package]] +name = "imagesize" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/5e/513ff06670c84e7b9887c1fdf61b2d42b4f574a831f2f1d2222023049d8a/imagesize-2.0.1.tar.gz", hash = "sha256:b2ba6a4dea487a7ebcd53248d3476aca449d30db12a2dde5e0c5ca9624fd77e5", size = 1883774, upload-time = "2026-08-24T12:35:19.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl", hash = "sha256:ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02", size = 14794, upload-time = "2026-08-24T12:35:12.548Z" }, +] + [[package]] name = "importlib-metadata" version = "9.0.1" @@ -1656,6 +1881,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/92/f9/ecbde7149e95b8a0f18e16d5d747f7dc06049d5da2e4f77f6f5e4a1f46a8/markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba", size = 14417, upload-time = "2026-10-02T23:06:56.246Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -1848,6 +2085,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/c1/664578dd98be70cd4ab1a9dcf3a181b1376b83c65ec41ee162130b58c8c0/msgspec-0.22.0-cp315-cp315t-win_arm64.whl", hash = "sha256:268594d0bae5510572599a6ab0364dd9de43c867d24a30856cd9f5edb63d8dc6", size = 202117, upload-time = "2026-09-29T14:14:09.891Z" }, ] +[[package]] +name = "myst-parser" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "sphinx", version = "9.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl", hash = "sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a", size = 85817, upload-time = "2026-05-13T09:38:17.904Z" }, +] + [[package]] name = "ndindex" version = "1.10.1" @@ -1958,9 +2214,12 @@ name = "numcodecs" version = "0.16.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, @@ -1998,9 +2257,12 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" } }, @@ -2092,9 +2354,12 @@ name = "numpy" version = "2.4.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } wheels = [ @@ -2179,9 +2444,12 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" } wheels = [ @@ -2828,6 +3096,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" }, ] +[[package]] +name = "pydata-sphinx-theme" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accessible-pygments" }, + { name = "babel" }, + { name = "beautifulsoup4" }, + { name = "docutils" }, + { name = "jinja2" }, + { name = "pygments" }, + { name = "requests" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "sphinx", version = "9.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/08/ef36c71c85ccd574233bb35e04a9695efecc5d54cb2edae839a5bc58f8ca/pydata_sphinx_theme-0.24.0.tar.gz", hash = "sha256:bc9050daac696ab62c817e324ef29ee43720c2e6068fde809c76a52d4c788ac2", size = 5020159, upload-time = "2026-11-02T13:42:05.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/3b/f99ccbf185b910e7303ddb20849f83eb0995c0ffc983b4b5e10eda1ca7b6/pydata_sphinx_theme-0.24.0-py3-none-any.whl", hash = "sha256:195bc167ab1dcf0b31037e26f145f60461dc9cbb1eb2011283565567ee4fb6e5", size = 3217864, upload-time = "2026-11-02T13:42:03.147Z" }, +] + [[package]] name = "pygments" version = "2.21.0" @@ -3048,6 +3337,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + [[package]] name = "rich" version = "15.0.0" @@ -3061,6 +3365,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + [[package]] name = "rpds-py" version = "2026.9.1" @@ -3266,6 +3579,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "snowballstemmer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/71/c3/1b817965ac12dc002d7c9cd7dfffdd7d4fbf9b45763ef2ffe7b86ee94670/soupsieve-2.10.tar.gz", hash = "sha256:49e9380d7d2905463583bafe285e818c7366a9ed7b3aee221c1ac79c905d8bc0", size = 124337, upload-time = "2026-09-24T02:36:35.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/87/5ed59e1d0290564e2027ed066c52b059892c4637741337fc0967183c8d4d/soupsieve-2.10-py3-none-any.whl", hash = "sha256:8596eb8967d744174820280fa62b4542a2e955bfaccca73ed8a13c6eb8e9b502", size = 38019, upload-time = "2026-09-24T02:36:33.709Z" }, +] + +[[package]] +name = "soupsieve" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/01/7c/a64eedae0b7776cb9602d2da9cb8f8caae8e1b4c1da5e8e87bf1e68121ec/soupsieve-4.0.1.tar.gz", hash = "sha256:7621e3c5f6b371ed2c5eabaac5de300ac3d66d05e6d7f34783e31b63c4f90c75", size = 153302, upload-time = "2026-11-04T16:36:27.749Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/5c/4f68370847b561a389dd9a2d7eb3d19fd8cbb8acbae543a0c6b8d0d9700b/soupsieve-4.0.1-py3-none-any.whl", hash = "sha256:2e68cf7dd5a0f29d81f9d485275ccf294e6475f615e211895d91cb81f8614f04", size = 50688, upload-time = "2026-11-04T16:36:26.158Z" }, +] + [[package]] name = "sparse" version = "0.19.2" @@ -3280,6 +3639,212 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/4e/23e0009b6d5d415c223ea38fcf1e38dc9b0fc6d3ff15b5c5090dcc1f6d88/sparse-0.19.2-py2.py3-none-any.whl", hash = "sha256:2fcad7b83dacb5c70123a7de76db3626cbdaf567fcf12bdbf637aca6c4ee1bbe", size = 155257, upload-time = "2026-08-14T09:37:25.64Z" }, ] +[[package]] +name = "sphinx" +version = "9.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/73/567727fc8c8d85bc3adc88a246e12937cfda8d565cf1d80e85bceaa61fe5/sphinx-9.1.1.tar.gz", hash = "sha256:48b627f02908f22c601b3ba9c7b17265b4f315dd05c8bc64cd4da41183f44fbc", size = 8725638, upload-time = "2026-10-20T00:41:13.96Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/18/3c66ea76da7de47e716b181c35bdbfafbd5e9af0bb247b14578e9de348e9/sphinx-9.1.1-py3-none-any.whl", hash = "sha256:f8b60013ce25017be41a509e5c2d265bc1918410061315c36b809c7f335646c0", size = 3923559, upload-time = "2026-10-20T00:41:11.288Z" }, +] + +[[package]] +name = "sphinx" +version = "9.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/d4/413466f0be8b1cb0e3e43893a148b96d7462d0923d82b4166b21afc5f30c/sphinx-9.2.0.tar.gz", hash = "sha256:90470459f3cab15d9dfed146c71cf77694b0445df17b2e0e69059cb9f7fc9ee4", size = 8782438, upload-time = "2026-10-21T00:05:15.019Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/28/a0c50761cd215b7042f4bc617a6bc5c0cd7be62f75bc908e169e28a4d21e/sphinx-9.2.0-py3-none-any.whl", hash = "sha256:4c3ec13b612712c0d3ce4cbdd9e3eb91e68577e07a8e31f32f21cf178453578c", size = 3925388, upload-time = "2026-10-21T00:05:12.241Z" }, +] + +[[package]] +name = "sphinx-autobuild" +version = "2025.8.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "sphinx", version = "9.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "starlette" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/20/56411b52f917696995f5ad27d2ea7e9492c84a043c5b49a3a3173573cd93/sphinx_autobuild-2025.8.25-py3-none-any.whl", hash = "sha256:b750ac7d5a18603e4665294323fd20f6dcc0a984117026d1986704fa68f0379a", size = 12535, upload-time = "2025-08-25T18:44:54.164Z" }, +] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "sphinx", version = "9.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" }, +] + +[[package]] +name = "sphinx-design" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "sphinx", version = "9.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/7b/804f311da4663a4aecc6cf7abd83443f3d4ded970826d0c958edc77d4527/sphinx_design-0.7.0.tar.gz", hash = "sha256:d2a3f5b19c24b916adb52f97c5f00efab4009ca337812001109084a740ec9b7a", size = 2203582, upload-time = "2026-01-19T13:12:53.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/cf/45dd359f6ca0c3762ce0490f681da242f0530c49c81050c035c016bfdd3a/sphinx_design-0.7.0-py3-none-any.whl", hash = "sha256:f82bf179951d58f55dca78ab3706aeafa496b741a91b1911d371441127d64282", size = 2220350, upload-time = "2026-01-19T13:12:51.077Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/02/4f334c7fc3e54c51d2364575a41cd2fae6847e7f53001949f0be5f32e18b/sphinxcontrib_jsmath-2.0.0.tar.gz", hash = "sha256:551f780cf9687f9c3c74f0cf9f18aca06e0d6958853453f3803fe4d357c5b924", size = 14511, upload-time = "2026-10-20T18:22:05.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/c2/2965c35b39587d4b7a397f65a9c7d4b942b031061e7ac1ce91782f394a0d/sphinxcontrib_jsmath-2.0.0-py3-none-any.whl", hash = "sha256:15ca92fe0eec82f0baf91a7efcc51ffacdb605924be6327683a52ebdacb6744c", size = 78095, upload-time = "2026-10-20T18:22:04.42Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + [[package]] name = "sqlalchemy" version = "2.1.3" @@ -3437,9 +4002,12 @@ name = "tifffile" version = "2026.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, @@ -3457,9 +4025,12 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" } }, @@ -4065,9 +4636,12 @@ name = "zarr" version = "3.1.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.11.5' and sys_platform == 'win32'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.11.5' and sys_platform == 'emscripten'", + "python_full_version >= '3.11.5' and python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.11.5' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "donfig" }, @@ -4090,9 +4664,12 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "donfig" }, From 865533cff3e50ad2f33c9c124b66478552217d76 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Sun, 8 Nov 2026 22:58:51 +0000 Subject: [PATCH 3/4] Document service levels, service accounts and custom endpoints Explain that acquisition comes first and how an outage is handled. Add an example of reading Tiled from UDC, a workaround for creating a processed folder that the central authorization migration keeps, how to request a custom endpoint, and searching with TimeRange. Co-Authored-By: Claude Opus 5.5 --- docs/explanations/service-levels.md | 37 ++++++ docs/how-to/request-a-custom-endpoint.md | 38 ++++++ docs/how-to/search.md | 30 +++-- docs/how-to/use-a-service-account.md | 140 +++++++++++++++++++++++ docs/tutorials/getting-started.md | 7 +- 5 files changed, 242 insertions(+), 10 deletions(-) create mode 100644 docs/explanations/service-levels.md create mode 100644 docs/how-to/request-a-custom-endpoint.md create mode 100644 docs/how-to/use-a-service-account.md diff --git a/docs/explanations/service-levels.md b/docs/explanations/service-levels.md new file mode 100644 index 0000000..129f780 --- /dev/null +++ b/docs/explanations/service-levels.md @@ -0,0 +1,37 @@ +# Service levels and outages + +## What Tiled is for + +For DAQ Core, the main purpose of Tiled is **acquisition**. A beamline that is enabled +for Tiled can't run any plans through blueapi without it, so a Tiled outage stops data +collection. + +We will try to avoid any downtime to acquisition. Search, the UI and analysis matter, +but when we have to choose, keeping data collection running comes first. + +## What happens if Tiled stops working + +If Tiled fails, we restore acquisition first and investigate second: + +1. **Bring Tiled back with a new, empty database.** blueapi can write to it, so data + collection continues. +2. **Find and fix the problem** with Tiled and its production database, while + beamlines keep collecting. +3. **Point Tiled back at the production database**, and move the data collected in the + meantime into it. + +## What works during an outage + +While Tiled is running on the temporary database, it holds only what has been collected +since the switch. + +| Works | Doesn't work until step 3 is finished | +| ---------------------------------- | ------------------------------------------------------ | +| Data collection through blueapi | Analysis that depends on historical data | +| Scans collected since the switch | Searching for earlier scans | +| | The UI, and anything else that needs the complete data | + +Switching to the temporary database doesn't change the production database, so earlier +data is available again once Tiled points back to it. + +To report a problem, see [Get help](../how-to/get-help.md). diff --git a/docs/how-to/request-a-custom-endpoint.md b/docs/how-to/request-a-custom-endpoint.md new file mode 100644 index 0000000..14331c6 --- /dev/null +++ b/docs/how-to/request-a-custom-endpoint.md @@ -0,0 +1,38 @@ +# Request a custom endpoint + +If a beamline needs something Tiled doesn't do, such as server-side processing for a +live display, we can add a custom endpoint for it. Custom endpoints are plugins that +ship in the `dls-tiled` image. + +To ask for one, create a ticket in the +[ACQP Jira project](https://jira.diamond.ac.uk/projects/ACQP/issues/). Include: + +- the beamline that needs it +- what the endpoint should take and return +- what will call it, and how often +- why this can't be done on the client, for example because it would mean downloading + a lot of data for each update + +A beamline's endpoint can be added for **that beamline only**, so it doesn't affect any +other beamline. + +## Example: ViSR on b01-1 + +ViSR (b01-1) has its own endpoint, `dls_tiled.visr:visr_router`. It bins a scan into a +2-D image on the server, so that a display doesn't have to fetch every point on each +update: + +``` +GET /api/v1/binned/ +``` + +Other beamlines can ask to use it. Its parameters are described in the +[project README](https://github.com/DiamondLightSource/dls-tiled#dls_tiledvisrvisr_router). + +## How an endpoint is added + +For developers: an endpoint is a FastAPI router in `src/dls_tiled`, listed under +`routers:` in the Tiled server config. If it addresses nodes by path, it must also be +added to `envoy.pathPrefixes` in the Helm values. Otherwise its requests will +[reach `tiled-default`](../explanations/architecture.md#request-routing), which can't +read any beamline's files. diff --git a/docs/how-to/search.md b/docs/how-to/search.md index e1c4413..e8639dd 100644 --- a/docs/how-to/search.md +++ b/docs/how-to/search.md @@ -78,8 +78,6 @@ All from `tiled.queries`: ### Recipes ```python -from datetime import datetime - from tiled.queries import Contains, Key, KeyPresent # A scan by number @@ -91,9 +89,6 @@ visit.values().last() # The run from a blueapi task visit.search(Key("start.blueapi_task_id") == task_id).values().first() -# Everything since a given time -visit.search(Key("start.time") > datetime(2026, 7, 2, 9, 0).timestamp()) - # Finished scans using a detector, newest first results = ( visit.search(KeyPresent("stop")) @@ -107,6 +102,23 @@ visit.distinct("start.plan_name", counts=True) `.values().first()` raises `IndexError` if nothing matched. +### Searching by time + +`TimeRange` from `bluesky_tiled_plugins` turns readable dates into timestamps. Any +level of detail works, from `"2026"` to `"2026-07-02 09:00"`. Set `timezone`, or it uses +your computer's. Search on the values it gives you: + +```python +from bluesky_tiled_plugins.queries import TimeRange + +window = TimeRange(since="2026-07-02 09:00", until="2026-07-03", timezone="Europe/London") +runs = visit.search(Key("start.time") >= window.since).search( + Key("start.time") < window.until +) +``` + +Use `since` or `until` alone for an open-ended range, with just the matching search. + Sort with `1` for ascending and `-1` for descending. The `DESCENDING` constant raises a `TypeError` in the 0.2.21 client. @@ -114,10 +126,10 @@ Two things from Bluesky tutorials don't work on Diamond's server: - `visit[-1]` or `visit[42]`, which need Bluesky's catalog client. Use the recipes above. -- `ScanID`, `PartialUID`, `ScanIDRange` and `TimeRange` from - `bluesky_tiled_plugins.queries`, which need the plugin on the server, and the - `dls-tiled` image doesn't include it. Use `Key("start.scan_id")` and - `Key("start.time")` instead. +- Passing `ScanID`, `PartialUID`, `ScanIDRange` or `TimeRange` straight to + `visit.search`. Only Bluesky's catalog client knows how to apply them. On Diamond's + visit nodes the filter is ignored, so you get **every run back with no error**. Use + `Key("start.scan_id")`, and [`TimeRange` as shown above](#searching-by-time). ### Searching over HTTP diff --git a/docs/how-to/use-a-service-account.md b/docs/how-to/use-a-service-account.md new file mode 100644 index 0000000..19f7bec --- /dev/null +++ b/docs/how-to/use-a-service-account.md @@ -0,0 +1,140 @@ +# Use a service account + +Scripts and services that run without a person logging in, such as Unattended Data +Collection (UDC), authenticate to Tiled with a Keycloak service account. + +The account's token carries a `beamline` claim, and it can see that beamline's +data. Its audience decides what else it can do: + +| Audience | Access | Typical use | +| -------------- | -------------------------------------- | ------------ | +| `tiled` | Read-only | UDC | +| `tiled-writer` | Read and write on its beamline | blueapi | + +## Read Tiled from UDC + +This example uses a client called `p46UDC`, with `beamline` set to `p46` and the +audience `tiled`. Keep the secret out of your code and pass it in through the +environment, for example from a `.env` file that isn't committed. + +```python +import os + +try: + import httpx2 as httpx # the Tiled client uses httpx2 from version 0.2.21 +except ImportError: + import httpx + +from tiled.client import from_uri +from tiled.queries import Key + +TOKEN_URL = "https://identity.diamond.ac.uk/realms/dls/protocol/openid-connect/token" + + +class ServiceAccountAuth(httpx.Auth): + def __init__(self, client_id, client_secret): + self.form = { + "client_id": client_id, + "client_secret": client_secret, + "grant_type": "client_credentials", + } + + def sync_auth_flow(self, request): + response = httpx.post(TOKEN_URL, data=self.form).raise_for_status() + request.headers["Authorization"] = f"Bearer {response.json()['access_token']}" + yield request + + +client = from_uri( + "https://tiled.diamond.ac.uk", + auth=ServiceAccountAuth("p46UDC", os.environ["TILED_CLIENT_SECRET"]), +) + +visit = client["p46/cm44194/cm44194-1"] +finished = visit.search(Key("stop.exit_status") == "success") +print(len(finished)) +``` + +From here, everything in [Searching](search.md) works as it does for a person. + +## Create a processed folder + +:::{warning} +This is a workaround until the +[central authorization policy](../explanations/authorization.md#new-central-authorization) +is released, when a dedicated processing service account will replace it. It uses +**blueapi's `tiled-writer` secret**, which can write anywhere on the beamline. Only +use it on a machine you trust, read it from the environment, and never commit it or +paste it into a notebook. +::: + +Add this to the script above. It creates the folder and writes one result into it. + +```python +import json + +import numpy as np + + +def access_tags_for(beamline, visit): + proposal, number = visit.split("-") + blob = {"beamline": beamline, "proposal": proposal, "visit": int(number)} + return [json.dumps(blob)] + + +def get_or_create(parent, key, access_tags): + try: + return parent[key] + except KeyError: + return parent.create_container(key=key, access_tags=access_tags) + + +def processed_folder(client, beamline, visit): + access_tags = access_tags_for(beamline, visit) + visit_node = get_or_create(client[beamline], visit, access_tags) + return get_or_create(visit_node, "processed", access_tags) + + +def new_result(folder, name, beamline, visit): + return folder.create_container( + key=name, + metadata={"start": {"instrument": beamline, "instrument_session": visit}}, + access_tags=access_tags_for(beamline, visit), + ) + + +BEAMLINE = "p46" +VISIT = "cm44194-1" + +writer = from_uri( + "https://tiled.diamond.ac.uk", + auth=ServiceAccountAuth( + os.environ["TILED_WRITER_CLIENT_ID"], os.environ["TILED_WRITER_CLIENT_SECRET"] + ), +) + +processed = processed_folder(writer, BEAMLINE, VISIT) +result = new_result(processed, "fit-1", BEAMLINE, VISIT) +result.write_array( + np.linspace(0, 1, 100), key="fit", access_tags=access_tags_for(BEAMLINE, VISIT) +) +``` + +The account's `beamline` claim must match `BEAMLINE`, and the visit must be on that +beamline. + +To let your data move to the new policy without further work, keep to these rules: + +- **Use `//processed`, as the script does.** This is deliberately + not under `//`, because it is the layout that the new + policy uses. The migration leaves it in place. A `processed` folder in the old tree + would not migrate correctly: runs in it would move to `raw`, and anything else would + be left behind where nobody but admins could see it. +- **Put everything in a result container from `new_result`.** The `start` metadata is + what makes the migration treat the result as a run and retag everything inside it. + A node written directly into `processed` isn't retagged. +- **Pass `access_tags` every time you create a node**, including arrays and tables + inside a result. A node created without tags isn't tied to the visit's permissions. + +After the migration, paths stay the same and each result has the tag +`,,p`, so everyone on the visit can read it. diff --git a/docs/tutorials/getting-started.md b/docs/tutorials/getting-started.md index ae5de44..e4ddc95 100644 --- a/docs/tutorials/getting-started.md +++ b/docs/tutorials/getting-started.md @@ -1,7 +1,8 @@ # Getting started with Tiled [Tiled](https://blueskyproject.io/tiled/) is the data access service for scans -run with Bluesky at Diamond. It stores each scan's metadata and tabular data, knows +run with Bluesky at Diamond, and DAQ Core's main purpose for it is +[acquisition](../explanations/service-levels.md). It stores each scan's metadata and tabular data, knows where its detector files are, and serves all of it over HTTP, with access checked against your Diamond login. @@ -13,6 +14,10 @@ checked against your Diamond login. | Produce a NeXus file for a scan | [NeXus files](../how-to/create-nexus-files.md) | | Understand who can see what | [Authorization](../explanations/authorization.md) | | Understand how the service is deployed | [Architecture](../explanations/architecture.md) | +| Know what happens during an outage | [Service levels and outages](../explanations/service-levels.md) | +| Connect from UDC or another script without a login | [Use a service account](../how-to/use-a-service-account.md) | +| Create a processed folder for a visit | [Use a service account](../how-to/use-a-service-account.md#create-a-processed-folder) | +| Ask for a custom endpoint for a beamline | [Request a custom endpoint](../how-to/request-a-custom-endpoint.md) | | Report that data can't be read | [Getting help](../how-to/get-help.md) | ## Install and connect From 4d01eec4accae03ad2f8da911ecdb575fc506fd9 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Sat, 14 Nov 2026 00:56:14 +0000 Subject: [PATCH 4/4] Few changes --- docs/explanations/decisions.md | 18 ++++++------ .../0001-record-architecture-decisions.md | 18 ------------ ...0002-switched-to-python-copier-template.md | 28 ------------------- docs/explanations/decisions/COPYME | 19 ------------- docs/how-to/use-a-service-account.md | 16 +++++++---- 5 files changed, 19 insertions(+), 80 deletions(-) delete mode 100644 docs/explanations/decisions/0001-record-architecture-decisions.md delete mode 100644 docs/explanations/decisions/0002-switched-to-python-copier-template.md delete mode 100644 docs/explanations/decisions/COPYME diff --git a/docs/explanations/decisions.md b/docs/explanations/decisions.md index 0533b98..23d8409 100644 --- a/docs/explanations/decisions.md +++ b/docs/explanations/decisions.md @@ -1,12 +1,10 @@ # Architectural Decision Records -Architectural decisions are made throughout a project's lifetime. As a way of keeping track of these decisions, we record these decisions in Architecture Decision Records (ADRs) listed below. - -```{toctree} -:glob: true -:maxdepth: 1 - -decisions/* -``` - -For more information on ADRs see this [blog by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). +Decisions that affect Tiled at Diamond are recorded in the +[Diamond Developer Guide](https://dev-guide.diamond.ac.uk/adrs/docs/records/0024/): + +| ADR | Title | Status | +| --- | ----- | ------ | +| [0024](https://dev-guide.diamond.ac.uk/adrs/docs/records/0024/) | Tiled Data Retention and DataCat Metadata Schema | Proposed | +| [0027](https://dev-guide.diamond.ac.uk/adrs/docs/records/0027/) | Nexus File Creation | Proposed | +| [0028](https://gitlab.diamond.ac.uk/sscc-docs/architectural-decision-records/-/merge_requests/21/diffs) | "Tiled for everything" Metadata model | In review (merge request) | diff --git a/docs/explanations/decisions/0001-record-architecture-decisions.md b/docs/explanations/decisions/0001-record-architecture-decisions.md deleted file mode 100644 index 44d234e..0000000 --- a/docs/explanations/decisions/0001-record-architecture-decisions.md +++ /dev/null @@ -1,18 +0,0 @@ -# 1. Record architecture decisions - -## Status - -Accepted - -## Context - -We need to record the architectural decisions made on this project. - -## Decision - -We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). - -## Consequences - -See Michael Nygard's article, linked above. To create new ADRs we will copy and -paste from existing ones. diff --git a/docs/explanations/decisions/0002-switched-to-python-copier-template.md b/docs/explanations/decisions/0002-switched-to-python-copier-template.md deleted file mode 100644 index 66fe5d8..0000000 --- a/docs/explanations/decisions/0002-switched-to-python-copier-template.md +++ /dev/null @@ -1,28 +0,0 @@ -# 2. Adopt python-copier-template for project structure - -## Status - -Accepted - -## Context - -We should use the following [python-copier-template](https://github.com/DiamondLightSource/python-copier-template). -The template will ensure consistency in developer -environments and package management. - -## Decision - -We have switched to using the template. - -## Consequences - -This module will use a fixed set of tools as developed in `python-copier-template` -and can pull from this template to update the packaging to the latest techniques. - -As such, the developer environment may have changed, the following could be -different: - -- linting -- formatting -- pip venv setup -- CI/CD diff --git a/docs/explanations/decisions/COPYME b/docs/explanations/decisions/COPYME deleted file mode 100644 index b466c79..0000000 --- a/docs/explanations/decisions/COPYME +++ /dev/null @@ -1,19 +0,0 @@ -# 3. Short descriptive title - -Date: Today's date - -## Status - -Accepted - -## Context - -Background to allow us to make the decision, to show how we arrived at our conclusions. - -## Decision - -What decision we made. - -## Consequences - -What we will do as a result of this decision. diff --git a/docs/how-to/use-a-service-account.md b/docs/how-to/use-a-service-account.md index 19f7bec..914db37 100644 --- a/docs/how-to/use-a-service-account.md +++ b/docs/how-to/use-a-service-account.md @@ -17,14 +17,17 @@ This example uses a client called `p46UDC`, with `beamline` set to `p46` and the audience `tiled`. Keep the secret out of your code and pass it in through the environment, for example from a `.env` file that isn't committed. +The deployed Tiled is version 0.2.18, so install the matching client, which uses +`httpx`: + +```sh +pip install "tiled[client]==0.2.18" httpx +``` + ```python import os -try: - import httpx2 as httpx # the Tiled client uses httpx2 from version 0.2.21 -except ImportError: - import httpx - +import httpx from tiled.client import from_uri from tiled.queries import Key @@ -55,6 +58,9 @@ finished = visit.search(Key("stop.exit_status") == "success") print(len(finished)) ``` +From version 0.2.21 the Tiled client uses `httpx2`. When the deployment moves to it, +change the import to `import httpx2 as httpx`. + From here, everything in [Searching](search.md) works as it does for a person. ## Create a processed folder