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 3e1ea09..1cedce7 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,15 @@ 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](https://diamondlightsource.github.io/dls-tiled). + ## Plugins Plugins are FastAPI routers. Enable one by listing it under `routers:` in the 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/explanations/architecture.md b/docs/explanations/architecture.md new file mode 100644 index 0000000..185568c --- /dev/null +++ b/docs/explanations/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`](https://github.com/DiamondLightSource/dls-tiled/blob/main/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](../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 +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/explanations/authorization.md b/docs/explanations/authorization.md new file mode 100644 index 0000000..d147b6d --- /dev/null +++ b/docs/explanations/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`](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). + +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](https://github.com/DiamondLightSource/dls-tiled#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/explanations/decisions.md b/docs/explanations/decisions.md new file mode 100644 index 0000000..23d8409 --- /dev/null +++ b/docs/explanations/decisions.md @@ -0,0 +1,10 @@ +# Architectural Decision Records + +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/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/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/how-to/create-nexus-files.md b/docs/how-to/create-nexus-files.md new file mode 100644 index 0000000..9313577 --- /dev/null +++ b/docs/how-to/create-nexus-files.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](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). + +```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/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/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/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/how-to/search.md b/docs/how-to/search.md new file mode 100644 index 0000000..e8639dd --- /dev/null +++ b/docs/how-to/search.md @@ -0,0 +1,283 @@ +# 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 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() + +# 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. + +### 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. + +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. +- 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 + +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/how-to/stream-live-data.md b/docs/how-to/stream-live-data.md new file mode 100644 index 0000000..c9f5b01 --- /dev/null +++ b/docs/how-to/stream-live-data.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](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. + +## 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](search.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. 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..914db37 --- /dev/null +++ b/docs/how-to/use-a-service-account.md @@ -0,0 +1,146 @@ +# 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. + +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 + +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 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 + +:::{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/images/deployment-architecture.png b/docs/images/deployment-architecture.png new file mode 100644 index 0000000..86235d3 Binary files /dev/null and b/docs/images/deployment-architecture.png differ 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..e4ddc95 --- /dev/null +++ b/docs/tutorials/getting-started.md @@ -0,0 +1,91 @@ +# Getting started with Tiled + +[Tiled](https://blueskyproject.io/tiled/) is the data access service for scans +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. + +| 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) | +| 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 + +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" },