diff --git a/docs/design/restricted-diagram.md b/docs/design/restricted-diagram.md new file mode 100644 index 000000000..93341d166 --- /dev/null +++ b/docs/design/restricted-diagram.md @@ -0,0 +1,217 @@ +# Restricted Diagrams + +**Issues:** [#865](https://github.com/datajoint/datajoint-python/issues/865), [#1110](https://github.com/datajoint/datajoint-python/issues/1110) + +## Motivation + +### Error-driven cascade is fragile + +The original cascade delete worked by trial-and-error: attempt `DELETE` on the parent, catch the FK integrity error, parse the MySQL error message to discover which child table is blocking, then recursively delete from that child first. + +This approach has several problems: + +- **MySQL 8 with limited privileges:** Returns error 1217 (`ROW_IS_REFERENCED`) instead of 1451 (`ROW_IS_REFERENCED_2`), which provides no table name. The cascade crashes ([#1110](https://github.com/datajoint/datajoint-python/issues/1110)). +- **PostgreSQL overhead:** PostgreSQL aborts the entire transaction on any error. Each failed delete attempt requires `SAVEPOINT` / `ROLLBACK TO SAVEPOINT` round-trips. +- **Fragile parsing:** Different MySQL versions and privilege levels produce different error message formats. + +### Graph-driven approach + +`drop()` already uses graph-driven traversal — walking the dependency graph in reverse topological order, dropping leaves first. The same pattern applies to cascade delete, with the addition of **restriction propagation** through FK attribute mappings. + +### Data subsetting + +`dj.Diagram` provides set operators for specifying subsets of *tables*. Per-node restrictions complete the functionality for specifying cross-sections of *data* — enabling delete, export, backup, and sharing. + +## Architecture + +Single `class Diagram(nx.DiGraph)` with all operational methods always available. Only visualization methods (`draw`, `make_dot`, `make_svg`, `make_png`, `make_image`, `make_mermaid`, `save`, `_repr_svg_`) are gated on `diagram_active`. + +`Dependencies` is the canonical store of the FK graph. `Diagram` copies from it and constructs derived views. + +### Instance attributes + +```python +self._connection # Connection +self._cascade_restrictions # dict[str, list] — per-node OR restrictions (cascade mode) +self._restrict_conditions # dict[str, AndList] — per-node AND restrictions (restrict mode) +self._restriction_attrs # dict[str, set] — restriction attribute names per node +self._part_integrity # str — "enforce", "ignore", or "cascade" (set by cascade()) +``` + +### Restriction modes + +A diagram operates in one of three states: **unrestricted** (initial), **cascade**, or **restrict**. The modes are mutually exclusive. `cascade` is applied once; `restrict` can be chained. + +```python +# cascade: applied once, OR at convergence, for delete +rd = dj.Diagram(schema).cascade(Session & 'subject_id=1') + +# restrict: chainable, AND at convergence, for export +rd = dj.Diagram(schema).restrict(Session & cond).restrict(Stimulus & cond2) + +# Mixing raises DataJointError +``` + +## Restriction Propagation + +A restriction applied to one table node propagates downstream through FK edges in topological order. Each downstream node accumulates a restriction derived from its restricted parent(s). + +### Propagation rules + +For edge `Parent → Child` with `attr_map`: + +| Condition | Child restriction | +|-----------|-------------------| +| Non-aliased AND `parent_attrs ⊆ child.primary_key` | Copy parent restriction directly | +| Aliased FK (`attr_map` renames columns) | `parent_ft.proj(**{fk: pk for fk, pk in attr_map.items()})` | +| Non-aliased AND `parent_attrs ⊄ child.primary_key` | `parent_ft.proj()` | + +Restrictions are applied via `restrict()` → `make_condition()`, ensuring `AndList` and `QueryExpression` objects are properly converted to SQL. Direct assignment to `_restriction` is never used, as `where_clause()` would produce invalid SQL from `str(AndList)` or `str(QueryExpression)`. + +### Converging paths + +A child node may have multiple restricted ancestors. The combination rule depends on the operator: + +``` +Session ──→ Recording ←── Stimulus + ↓ ↓ +subject=1 type="visual" +``` + +`Recording` receives two propagated restrictions: R1 from Session, R2 from Stimulus. + +**`cascade` — OR (union):** A recording is deleted if tainted by *any* restricted parent. Correct for referential integrity: if the parent row is being deleted, all child rows referencing it must go. Implemented by passing the full restriction list to `restrict()`, which creates an OrList. + +**`restrict` — AND (intersection):** A recording is included only if it satisfies *all* restricted ancestors. Correct for subsetting: only rows matching every condition are selected. Implemented by iterating restrictions and calling `restrict()` for each. + +| DataJoint type | Python type | SQL meaning | +|----------------|-------------|-------------| +| OR-combined restrictions | `list` | `WHERE (r1) OR (r2) OR ...` | +| AND-combined restrictions | `AndList` | `WHERE (r1) AND (r2) AND ...` | +| No restriction | empty `list` or `AndList()` | No WHERE clause (all rows) | + +### Multiple FK paths from same parent + +A child may reference the same parent through multiple FKs (e.g., `source_mouse` and `target_mouse` both referencing `Mouse`). These are represented as alias nodes in the dependency graph. Multiple FK paths from the same restricted parent always combine with **OR** — structural, not operation-dependent. + +### `part_integrity` + +| Mode | Behavior | +|------|----------| +| `"enforce"` | Data-driven post-check: raises only when rows were actually deleted from a Part without its master also being deleted. Avoids false positives when a Part appears in the cascade but has zero affected rows. | +| `"ignore"` | Allow deleting parts without masters | +| `"cascade"` | Propagate restriction upward from part to master, then re-propagate downstream | + +### Unloaded schemas + +If a child table lives in a schema not loaded into the dependency graph, the graph-driven delete won't know about it. The final parent `delete_quick()` fails with an FK error. Error-message parsing is retained as a **diagnostic fallback** to produce an actionable error: "activate schema X." + +## Methods + +### `cascade(self, table_expr, part_integrity="enforce") -> Diagram` + +Apply cascade restriction and propagate downstream. Returns a new `Diagram`. One-shot — cannot be called twice or mixed with `restrict()`. + +1. Verify no existing cascade or restrict restrictions +2. Copy diagram, seed `_cascade_restrictions[root]` with `list(table_expr.restriction)` +3. Propagate via `_propagate_restrictions(root, mode="cascade", part_integrity=part_integrity)` + +### `restrict(self, table_expr) -> Diagram` + +Apply restrict condition and propagate downstream. Returns a new `Diagram`. Chainable — can be called multiple times. Cannot be mixed with `cascade()`. + +1. Verify no existing cascade restrictions +2. Copy diagram, seed/extend `_restrict_conditions[root]` with `table_expr.restriction` +3. Propagate via `_propagate_restrictions(root, mode="restrict")` + +### `delete(self, transaction=True, prompt=None, dry_run=False) -> int | dict` + +Execute cascading delete. Requires `cascade()` first. + +1. If `dry_run`: return `preview()` without modifying data +2. Get non-alias nodes with restrictions in topological order +3. If `prompt`: show preview (table name + row count for each) +4. Start transaction +5. Delete in **reverse** topological order (leaves first) via `_restrict_freetable()` + `delete_quick()` +6. On `IntegrityError`: cancel transaction, parse FK error for actionable message about unloaded schemas +7. Post-check `part_integrity="enforce"`: if any part table had rows deleted but its master did not, cancel transaction and raise +8. Confirm/commit, return count from the root table + +### `drop(self, prompt=None, part_integrity="enforce", dry_run=False)` + +Drop all tables in `nodes_to_show` in reverse topological order. Pre-checks `part_integrity` structurally (tables, not rows). If `dry_run`, returns row counts without dropping. + +### `preview(self) -> dict[str, int]` + +Return `{full_table_name: row_count}` for each node with a restriction. Requires `cascade()` or `restrict()` first. Uses `_restrict_freetable()` to apply restrictions with correct OR/AND semantics. + +### `prune(self) -> Diagram` + +Remove tables with zero matching rows. With restrictions, removes nodes where the restricted query yields zero rows. Without restrictions, removes physically empty tables. Idempotent and chainable. + +### `_restrict_freetable(ft, restrictions, mode="cascade") -> FreeTable` + +Static helper. Applies restrictions to a `FreeTable` using `restrict()` for proper SQL generation. + +- **cascade mode:** Passes the entire restriction list to `restrict()`, creating an OrList (OR semantics). +- **restrict mode:** Iterates restrictions, calling `restrict()` for each (AND semantics). + +### `_from_table(cls, table_expr) -> Diagram` + +Classmethod factory for `Table.delete()` and `Table.drop()`. Creates a Diagram containing `table_expr` and all its descendants. + +## `Table` Integration + +```python +def delete(self, transaction=True, prompt=None, part_integrity="enforce", dry_run=False): + diagram = Diagram._from_table(self) + diagram = diagram.cascade(self, part_integrity=part_integrity) + return diagram.delete(transaction=transaction, prompt=prompt, dry_run=dry_run) + +def drop(self, prompt=None, part_integrity="enforce", dry_run=False): + if self.restriction: + raise DataJointError("A restricted Table cannot be dropped.") + diagram = Diagram._from_table(self) + diagram.drop(prompt=prompt, part_integrity=part_integrity, dry_run=dry_run) +``` + +## API Examples + +```python +# cascade: OR propagation for delete +rd = dj.Diagram(schema).cascade(Session & 'subject_id=1') +rd.preview() # show affected tables and row counts +rd.delete() # downstream only, OR at convergence + +# restrict: AND propagation for data subsetting +rd = (dj.Diagram(schema) + .restrict(Session & 'subject_id=1') + .restrict(Stimulus & 'type="visual"')) +rd.preview() # show selected tables and row counts + +# prune: remove tables with zero matching rows +rd = (dj.Diagram(schema) + .restrict(Subject & {'species': 'mouse'}) + .restrict(Session & 'session_date > "2024-01-01"') + .prune()) +rd.preview() # only tables with matching rows + +# dry_run: preview without executing +counts = (Session & 'subject_id=1').delete(dry_run=True) +# returns {full_table_name: affected_row_count} + +# Table.delete() delegates to Diagram internally +(Session & 'subject_id=1').delete() +``` + +## Advantages + +| | Error-driven | Graph-driven | +|---|---|---| +| MySQL 8 + limited privileges | Crashes ([#1110](https://github.com/datajoint/datajoint-python/issues/1110)) | Works — no error parsing needed | +| PostgreSQL | Savepoint overhead per attempt | No errors triggered | +| Multiple FKs to same child | One-at-a-time via retry loop | All paths resolved upfront | +| part_integrity enforcement | Post-hoc check after delete | Data-driven post-check (no false positives) | +| Unloaded schemas | Crash with opaque error | Clear error: "activate schema X" | +| Reusability | Delete-only | Delete, drop, export, prune | +| Inspectability | Opaque recursive cascade | `preview()` / `dry_run` before executing | diff --git a/pixi.lock b/pixi.lock index dcc82c2b5..02e7fbeee 100644 --- a/pixi.lock +++ b/pixi.lock @@ -5,6 +5,8 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 @@ -98,58 +100,26 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f5/11/02ebebb09ff2104b690457cb7bc6ed700c9e0ce88cf581486bb0a5d3c88b/faker-37.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f2/9f/bf231c2a3fac99d1d7f1d89c76594f158693f981a4aa02be406e9f036832/fonttools-4.59.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/e5/b8/9eea6630198cb303d131d95d285a024b3b8645b1763a2916fddb44ca8760/matplotlib-3.10.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/89/a3/00260f8df72b51afa1f182dd609533c77fa2407918c4c2813d87b4a56725/minio-7.2.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/53/b8/fbab973592e23ae313042d450fc26fa24282ebffba21ba373786e1ce63b4/pyparsing-3.2.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl - pypi: ./ linux-aarch64: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 @@ -254,58 +224,26 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-xorgproto-2024.1-h86ecc28_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_2.conda - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda @@ -365,64 +303,34 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl - - pypi: https://files.pythonhosted.org/packages/48/c5/d5e07995077e48220269c28a221e168c91123ad5ceee44d548f54a057fc0/ipython-9.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl - pypi: ./ dev: channels: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 @@ -515,33 +423,49 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + - pypi: https://files.pythonhosted.org/packages/02/78/79aa8169408996f5a71150abdea2e5e0f364df250c9e54ac385f115c7436/aiobotocore-3.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/46/98a01139f318b7a2f0ad1d1e3be2a028d13aeb7e05aaa340a27cdc47fdf0/botocore-1.42.61-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/11/02ebebb09ff2104b690457cb7bc6ed700c9e0ce88cf581486bb0a5d3c88b/faker-37.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/9f/bf231c2a3fac99d1d7f1d89c76594f158693f981a4aa02be406e9f036832/fonttools-4.59.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e5/b8/9eea6630198cb303d131d95d285a024b3b8645b1763a2916fddb44ca8760/matplotlib-3.10.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/89/a3/00260f8df72b51afa1f182dd609533c77fa2407918c4c2813d87b4a56725/minio-7.2.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -553,10 +477,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/49/737c1a6273c585719858261753da0b688454d1b634438ccba8a9c4eb5aab/polars-1.38.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/5a/61d60ec5cc0ab37cbd5a699edb2f9af2875b7fdfdfb2a4608ca3cc5f0448/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl @@ -572,10 +501,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/57/e1/64c264db50b68de8a438b60ceeb921b2f22da3ebb7ad6255150225d0beac/s3fs-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/31/5e7b23f9e43ff7fd46d243808d70c5e8daf3bc08ecf5a7fb84d5e38f7603/testcontainers-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl @@ -584,6 +516,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: ./ linux-aarch64: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 @@ -687,33 +621,49 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxxf86vm-1.1.6-h86ecc28_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-xorgproto-2024.1-h86ecc28_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_2.conda + - pypi: https://files.pythonhosted.org/packages/02/78/79aa8169408996f5a71150abdea2e5e0f364df250c9e54ac385f115c7436/aiobotocore-3.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/46/98a01139f318b7a2f0ad1d1e3be2a028d13aeb7e05aaa340a27cdc47fdf0/botocore-1.42.61-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/2e/7a/34c9402ad12bce609be4be1146a7d22a7fae8e9d752684b6315cce552a65/coverage-7.11.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl @@ -725,10 +675,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/49/737c1a6273c585719858261753da0b688454d1b634438ccba8a9c4eb5aab/polars-1.38.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/8c/3ac18d6f89dc05fe2c7c0ee1dc5b81f77a5c85ad59898232c2500fe2ebbf/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/27/11/574fe7d13acf30bfd0a8dd7fa1647040f2b8064f13f43e8c963b1e65093b/pre_commit-4.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl @@ -744,10 +699,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/57/e1/64c264db50b68de8a438b60ceeb921b2f22da3ebb7ad6255150225d0beac/s3fs-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/31/5e7b23f9e43ff7fd46d243808d70c5e8daf3bc08ecf5a7fb84d5e38f7603/testcontainers-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl @@ -756,6 +714,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda @@ -814,33 +774,48 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda + - pypi: https://files.pythonhosted.org/packages/02/78/79aa8169408996f5a71150abdea2e5e0f364df250c9e54ac385f115c7436/aiobotocore-3.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/46/98a01139f318b7a2f0ad1d1e3be2a028d13aeb7e05aaa340a27cdc47fdf0/botocore-1.42.61-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/48/c5/d5e07995077e48220269c28a221e168c91123ad5ceee44d548f54a057fc0/ipython-9.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl @@ -852,10 +827,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/49/737c1a6273c585719858261753da0b688454d1b634438ccba8a9c4eb5aab/polars-1.38.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/fb/599ff3709e6a303024efd7edfd08cf8de55c6ac39527d8f41cbc4399385f/polars_runtime_32-1.38.1-cp310-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl @@ -871,10 +851,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/57/e1/64c264db50b68de8a438b60ceeb921b2f22da3ebb7ad6255150225d0beac/s3fs-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/31/5e7b23f9e43ff7fd46d243808d70c5e8daf3bc08ecf5a7fb84d5e38f7603/testcontainers-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl @@ -883,12 +866,16 @@ environments: - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: ./ test: channels: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 @@ -981,15 +968,23 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + - pypi: https://files.pythonhosted.org/packages/02/78/79aa8169408996f5a71150abdea2e5e0f364df250c9e54ac385f115c7436/aiobotocore-3.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/46/98a01139f318b7a2f0ad1d1e3be2a028d13aeb7e05aaa340a27cdc47fdf0/botocore-1.42.61-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl @@ -997,16 +992,21 @@ environments: - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/11/02ebebb09ff2104b690457cb7bc6ed700c9e0ce88cf581486bb0a5d3c88b/faker-37.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/9f/bf231c2a3fac99d1d7f1d89c76594f158693f981a4aa02be406e9f036832/fonttools-4.59.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e5/b8/9eea6630198cb303d131d95d285a024b3b8645b1763a2916fddb44ca8760/matplotlib-3.10.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/89/a3/00260f8df72b51afa1f182dd609533c77fa2407918c4c2813d87b4a56725/minio-7.2.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl @@ -1016,9 +1016,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/49/737c1a6273c585719858261753da0b688454d1b634438ccba8a9c4eb5aab/polars-1.38.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/5a/61d60ec5cc0ab37cbd5a699edb2f9af2875b7fdfdfb2a4608ca3cc5f0448/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl @@ -1030,14 +1035,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/53/b8/fbab973592e23ae313042d450fc26fa24282ebffba21ba373786e1ce63b4/pyparsing-3.2.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/57/e1/64c264db50b68de8a438b60ceeb921b2f22da3ebb7ad6255150225d0beac/s3fs-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/31/5e7b23f9e43ff7fd46d243808d70c5e8daf3bc08ecf5a7fb84d5e38f7603/testcontainers-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl @@ -1045,6 +1051,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: ./ linux-aarch64: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 @@ -1148,15 +1156,23 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxxf86vm-1.1.6-h86ecc28_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-xorgproto-2024.1-h86ecc28_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_2.conda + - pypi: https://files.pythonhosted.org/packages/02/78/79aa8169408996f5a71150abdea2e5e0f364df250c9e54ac385f115c7436/aiobotocore-3.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/46/98a01139f318b7a2f0ad1d1e3be2a028d13aeb7e05aaa340a27cdc47fdf0/botocore-1.42.61-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl - pypi: https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/2e/7a/34c9402ad12bce609be4be1146a7d22a7fae8e9d752684b6315cce552a65/coverage-7.11.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl @@ -1164,16 +1180,21 @@ environments: - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl @@ -1183,9 +1204,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/49/737c1a6273c585719858261753da0b688454d1b634438ccba8a9c4eb5aab/polars-1.38.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/8c/3ac18d6f89dc05fe2c7c0ee1dc5b81f77a5c85ad59898232c2500fe2ebbf/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl @@ -1197,14 +1223,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/72/99/cafef234114a3b6d9f3aaed0723b437c40c57bdb7b3e4c3a575bc4890052/pytest-9.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/27/98/822b924a4a3eb58aacba84444c7439fce32680592f394de26af9c76e2569/pytest_env-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/57/e1/64c264db50b68de8a438b60ceeb921b2f22da3ebb7ad6255150225d0beac/s3fs-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/31/5e7b23f9e43ff7fd46d243808d70c5e8daf3bc08ecf5a7fb84d5e38f7603/testcontainers-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl @@ -1212,6 +1239,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda @@ -1270,15 +1299,23 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda + - pypi: https://files.pythonhosted.org/packages/02/78/79aa8169408996f5a71150abdea2e5e0f364df250c9e54ac385f115c7436/aiobotocore-3.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/46/98a01139f318b7a2f0ad1d1e3be2a028d13aeb7e05aaa340a27cdc47fdf0/botocore-1.42.61-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl @@ -1286,16 +1323,20 @@ environments: - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/48/c5/d5e07995077e48220269c28a221e168c91123ad5ceee44d548f54a057fc0/ipython-9.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl @@ -1305,9 +1346,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/49/737c1a6273c585719858261753da0b688454d1b634438ccba8a9c4eb5aab/polars-1.38.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/fb/599ff3709e6a303024efd7edfd08cf8de55c6ac39527d8f41cbc4399385f/polars_runtime_32-1.38.1-cp310-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl @@ -1319,14 +1365,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/27/98/822b924a4a3eb58aacba84444c7439fce32680592f394de26af9c76e2569/pytest_env-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/57/e1/64c264db50b68de8a438b60ceeb921b2f22da3ebb7ad6255150225d0beac/s3fs-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/31/5e7b23f9e43ff7fd46d243808d70c5e8daf3bc08ecf5a7fb84d5e38f7603/testcontainers-4.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl @@ -1334,6 +1381,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: ./ packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 @@ -1394,6 +1443,95 @@ packages: purls: [] size: 631452 timestamp: 1758743294412 +- pypi: https://files.pythonhosted.org/packages/02/78/79aa8169408996f5a71150abdea2e5e0f364df250c9e54ac385f115c7436/aiobotocore-3.2.1-py3-none-any.whl + name: aiobotocore + version: 3.2.1 + sha256: 68b7474af3e7124666b8e191805db5a7255d14e6187e0472481c845b6062e42e + requires_dist: + - aiohttp>=3.12.0,<4.0.0 + - aioitertools>=0.5.1,<1.0.0 + - botocore>=1.42.53,<1.42.62 + - python-dateutil>=2.1,<3.0.0 + - jmespath>=0.7.1,<2.0.0 + - multidict>=6.0.0,<7.0.0 + - typing-extensions>=4.14.0,<5.0.0 ; python_full_version < '3.11' + - wrapt>=1.10.10,<3.0.0 + - httpx>=0.25.1,<0.29 ; extra == 'httpx' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl + name: aiohappyeyeballs + version: 2.6.1 + sha256: f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl + name: aiohttp + version: 3.13.3 + sha256: 425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3 + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: aiohttp + version: 3.13.3 + sha256: 7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: aiohttp + version: 3.13.3 + sha256: f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0 + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and extra == 'speedups' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl + name: aioitertools + version: 0.13.0 + sha256: 0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be + requires_dist: + - typing-extensions>=4.0 ; python_full_version < '3.10' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + name: aiosignal + version: 1.4.0 + sha256: 053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e + requires_dist: + - frozenlist>=1.1.0 + - typing-extensions>=4.2 ; python_full_version < '3.13' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl name: annotated-types version: 0.7.0 @@ -1544,6 +1682,22 @@ packages: purls: [] size: 347530 timestamp: 1713896411580 +- pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + name: attrs + version: 25.4.0 + sha256: adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/88/46/98a01139f318b7a2f0ad1d1e3be2a028d13aeb7e05aaa340a27cdc47fdf0/botocore-1.42.61-py3-none-any.whl + name: botocore + version: 1.42.61 + sha256: 476059beb3f462042742950cf195d26bc313461a77189c16e37e205b0a924b26 + requires_dist: + - jmespath>=0.7.1,<2.0.0 + - python-dateutil>=2.1,<3.0.0 + - urllib3>=1.25.4,<1.27 ; python_full_version < '3.10' + - urllib3>=1.25.4,!=2.2.0,<3 ; python_full_version >= '3.10' + - awscrt==0.31.2 ; extra == 'crt' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda sha256: c30daba32ddebbb7ded490f0e371eae90f51e72db620554089103b4a6934b0d5 md5: 51a19bba1b8ebfb60df25cde030b7ebc @@ -1833,6 +1987,96 @@ packages: requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl + name: cryptography + version: 46.0.5 + sha256: 50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263 + requires_dist: + - cffi>=1.14 ; python_full_version == '3.8.*' and platform_python_implementation != 'PyPy' + - cffi>=2.0.0 ; python_full_version >= '3.9' and platform_python_implementation != 'PyPy' + - typing-extensions>=4.13.2 ; python_full_version < '3.11' + - bcrypt>=3.1.5 ; extra == 'ssh' + - nox[uv]>=2024.4.15 ; extra == 'nox' + - cryptography-vectors==46.0.5 ; extra == 'test' + - pytest>=7.4.0 ; extra == 'test' + - pytest-benchmark>=4.0 ; extra == 'test' + - pytest-cov>=2.10.1 ; extra == 'test' + - pytest-xdist>=3.5.0 ; extra == 'test' + - pretend>=0.7 ; extra == 'test' + - certifi>=2024 ; extra == 'test' + - pytest-randomly ; extra == 'test-randomorder' + - sphinx>=5.3.0 ; extra == 'docs' + - sphinx-rtd-theme>=3.0.0 ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - pyenchant>=3 ; extra == 'docstest' + - readme-renderer>=30.0 ; extra == 'docstest' + - sphinxcontrib-spelling>=7.3.1 ; extra == 'docstest' + - build>=1.0.0 ; extra == 'sdist' + - ruff>=0.11.11 ; extra == 'pep8test' + - mypy>=1.14 ; extra == 'pep8test' + - check-sdist ; extra == 'pep8test' + - click>=8.0.1 ; extra == 'pep8test' + requires_python: '>=3.8,!=3.9.0,!=3.9.1' +- pypi: https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl + name: cryptography + version: 46.0.5 + sha256: 3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed + requires_dist: + - cffi>=1.14 ; python_full_version == '3.8.*' and platform_python_implementation != 'PyPy' + - cffi>=2.0.0 ; python_full_version >= '3.9' and platform_python_implementation != 'PyPy' + - typing-extensions>=4.13.2 ; python_full_version < '3.11' + - bcrypt>=3.1.5 ; extra == 'ssh' + - nox[uv]>=2024.4.15 ; extra == 'nox' + - cryptography-vectors==46.0.5 ; extra == 'test' + - pytest>=7.4.0 ; extra == 'test' + - pytest-benchmark>=4.0 ; extra == 'test' + - pytest-cov>=2.10.1 ; extra == 'test' + - pytest-xdist>=3.5.0 ; extra == 'test' + - pretend>=0.7 ; extra == 'test' + - certifi>=2024 ; extra == 'test' + - pytest-randomly ; extra == 'test-randomorder' + - sphinx>=5.3.0 ; extra == 'docs' + - sphinx-rtd-theme>=3.0.0 ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - pyenchant>=3 ; extra == 'docstest' + - readme-renderer>=30.0 ; extra == 'docstest' + - sphinxcontrib-spelling>=7.3.1 ; extra == 'docstest' + - build>=1.0.0 ; extra == 'sdist' + - ruff>=0.11.11 ; extra == 'pep8test' + - mypy>=1.14 ; extra == 'pep8test' + - check-sdist ; extra == 'pep8test' + - click>=8.0.1 ; extra == 'pep8test' + requires_python: '>=3.8,!=3.9.0,!=3.9.1' +- pypi: https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl + name: cryptography + version: 46.0.5 + sha256: 351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad + requires_dist: + - cffi>=1.14 ; python_full_version == '3.8.*' and platform_python_implementation != 'PyPy' + - cffi>=2.0.0 ; python_full_version >= '3.9' and platform_python_implementation != 'PyPy' + - typing-extensions>=4.13.2 ; python_full_version < '3.11' + - bcrypt>=3.1.5 ; extra == 'ssh' + - nox[uv]>=2024.4.15 ; extra == 'nox' + - cryptography-vectors==46.0.5 ; extra == 'test' + - pytest>=7.4.0 ; extra == 'test' + - pytest-benchmark>=4.0 ; extra == 'test' + - pytest-cov>=2.10.1 ; extra == 'test' + - pytest-xdist>=3.5.0 ; extra == 'test' + - pretend>=0.7 ; extra == 'test' + - certifi>=2024 ; extra == 'test' + - pytest-randomly ; extra == 'test-randomorder' + - sphinx>=5.3.0 ; extra == 'docs' + - sphinx-rtd-theme>=3.0.0 ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - pyenchant>=3 ; extra == 'docstest' + - readme-renderer>=30.0 ; extra == 'docstest' + - sphinxcontrib-spelling>=7.3.1 ; extra == 'docstest' + - build>=1.0.0 ; extra == 'sdist' + - ruff>=0.11.11 ; extra == 'pep8test' + - mypy>=1.14 ; extra == 'pep8test' + - check-sdist ; extra == 'pep8test' + - click>=8.0.1 ; extra == 'pep8test' + requires_python: '>=3.8,!=3.9.0,!=3.9.1' - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl name: cycler version: 0.12.1 @@ -1848,31 +2092,46 @@ packages: requires_python: '>=3.8' - pypi: ./ name: datajoint - version: 0.14.6 - sha256: f761bb719d6afe0361d7e564bcc950ea76c79fbee9c334032459d0d4437a6423 + version: 2.1.1 + sha256: 267defaa9ea7f22a8497568e8a14679be178f78cd3b34a4132609a57f0f71227 requires_dist: + - deepdiff + - fsspec>=2023.1.0 + - networkx - numpy + - pandas + - pydantic-settings>=2.0.0 + - pydot - pymysql>=0.7.2 - - deepdiff - pyparsing - - ipython - - pandas - tqdm - - networkx - - pydot - - minio>=7.0.0 - - matplotlib - - faker - - urllib3 - - setuptools - - pydantic-settings>=2.0.0 - - pre-commit ; extra == 'dev' - - ruff ; extra == 'dev' + - pyarrow>=14.0.0 ; extra == 'arrow' + - adlfs>=2023.1.0 ; extra == 'azure' - codespell ; extra == 'dev' + - polars>=0.20.0 ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pyarrow>=14.0.0 ; extra == 'dev' - pytest ; extra == 'dev' - pytest-cov ; extra == 'dev' - requires_python: '>=3.9,<3.14' - editable: true + - ruff ; extra == 'dev' + - gcsfs>=2023.1.0 ; extra == 'gcs' + - polars>=0.20.0 ; extra == 'polars' + - psycopg2-binary>=2.9.0 ; extra == 'postgres' + - s3fs>=2023.1.0 ; extra == 's3' + - faker ; extra == 'test' + - ipython ; extra == 'test' + - matplotlib ; extra == 'test' + - polars>=0.20.0 ; extra == 'test' + - psycopg2-binary>=2.9.0 ; extra == 'test' + - pyarrow>=14.0.0 ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - requests ; extra == 'test' + - s3fs>=2023.1.0 ; extra == 'test' + - testcontainers[minio,mysql,postgres]>=4.0 ; extra == 'test' + - ipython ; extra == 'viz' + - matplotlib ; extra == 'viz' + requires_python: '>=3.10,<3.14' - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda sha256: 3b988146a50e165f0fa4e839545c679af88e4782ec284cc7b6d07dd226d6a068 md5: 679616eb5ad4e521c83da4650860aba7 @@ -2314,6 +2573,129 @@ packages: purls: [] size: 59391 timestamp: 1757438897523 +- pypi: https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl + name: frozenlist + version: 1.8.0 + sha256: f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: frozenlist + version: 1.8.0 + sha256: eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: frozenlist + version: 1.8.0 + sha256: fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl + name: fsspec + version: 2026.2.0 + sha256: 98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437 + requires_dist: + - adlfs ; extra == 'abfs' + - adlfs ; extra == 'adl' + - pyarrow>=1 ; extra == 'arrow' + - dask ; extra == 'dask' + - distributed ; extra == 'dask' + - pre-commit ; extra == 'dev' + - ruff>=0.5 ; extra == 'dev' + - numpydoc ; extra == 'doc' + - sphinx ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - yarl ; extra == 'doc' + - dropbox ; extra == 'dropbox' + - dropboxdrivefs ; extra == 'dropbox' + - requests ; extra == 'dropbox' + - adlfs ; extra == 'full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'full' + - dask ; extra == 'full' + - distributed ; extra == 'full' + - dropbox ; extra == 'full' + - dropboxdrivefs ; extra == 'full' + - fusepy ; extra == 'full' + - gcsfs>2024.2.0 ; extra == 'full' + - libarchive-c ; extra == 'full' + - ocifs ; extra == 'full' + - panel ; extra == 'full' + - paramiko ; extra == 'full' + - pyarrow>=1 ; extra == 'full' + - pygit2 ; extra == 'full' + - requests ; extra == 'full' + - s3fs>2024.2.0 ; extra == 'full' + - smbprotocol ; extra == 'full' + - tqdm ; extra == 'full' + - fusepy ; extra == 'fuse' + - gcsfs>2024.2.0 ; extra == 'gcs' + - pygit2 ; extra == 'git' + - requests ; extra == 'github' + - gcsfs ; extra == 'gs' + - panel ; extra == 'gui' + - pyarrow>=1 ; extra == 'hdfs' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'http' + - libarchive-c ; extra == 'libarchive' + - ocifs ; extra == 'oci' + - s3fs>2024.2.0 ; extra == 's3' + - paramiko ; extra == 'sftp' + - smbprotocol ; extra == 'smb' + - paramiko ; extra == 'ssh' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test' + - numpy ; extra == 'test' + - pytest ; extra == 'test' + - pytest-asyncio!=0.22.0 ; extra == 'test' + - pytest-benchmark ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-recording ; extra == 'test' + - pytest-rerunfailures ; extra == 'test' + - requests ; extra == 'test' + - aiobotocore>=2.5.4,<3.0.0 ; extra == 'test-downstream' + - dask[dataframe,test] ; extra == 'test-downstream' + - moto[server]>4,<5 ; extra == 'test-downstream' + - pytest-timeout ; extra == 'test-downstream' + - xarray ; extra == 'test-downstream' + - adlfs ; extra == 'test-full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test-full' + - backports-zstd ; python_full_version < '3.14' and extra == 'test-full' + - cloudpickle ; extra == 'test-full' + - dask ; extra == 'test-full' + - distributed ; extra == 'test-full' + - dropbox ; extra == 'test-full' + - dropboxdrivefs ; extra == 'test-full' + - fastparquet ; extra == 'test-full' + - fusepy ; extra == 'test-full' + - gcsfs ; extra == 'test-full' + - jinja2 ; extra == 'test-full' + - kerchunk ; extra == 'test-full' + - libarchive-c ; extra == 'test-full' + - lz4 ; extra == 'test-full' + - notebook ; extra == 'test-full' + - numpy ; extra == 'test-full' + - ocifs ; extra == 'test-full' + - pandas<3.0.0 ; extra == 'test-full' + - panel ; extra == 'test-full' + - paramiko ; extra == 'test-full' + - pyarrow ; extra == 'test-full' + - pyarrow>=1 ; extra == 'test-full' + - pyftpdlib ; extra == 'test-full' + - pygit2 ; extra == 'test-full' + - pytest ; extra == 'test-full' + - pytest-asyncio!=0.22.0 ; extra == 'test-full' + - pytest-benchmark ; extra == 'test-full' + - pytest-cov ; extra == 'test-full' + - pytest-mock ; extra == 'test-full' + - pytest-recording ; extra == 'test-full' + - pytest-rerunfailures ; extra == 'test-full' + - python-snappy ; extra == 'test-full' + - requests ; extra == 'test-full' + - smbprotocol ; extra == 'test-full' + - tqdm ; extra == 'test-full' + - urllib3 ; extra == 'test-full' + - zarr ; extra == 'test-full' + - zstandard ; python_full_version < '3.14' and extra == 'test-full' + - tqdm ; extra == 'tqdm' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.1-h2b0a6b4_0.conda sha256: b827285fe001806beeddcc30953d2bd07869aeb0efe4581d56432c92c06b0c48 md5: 2935d9c0526277bd42373cf23d49d51f @@ -2520,6 +2902,28 @@ packages: purls: [] size: 2201370 timestamp: 1754732518951 +- pypi: https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl + name: greenlet + version: 3.3.2 + sha256: ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986 + requires_dist: + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + - objgraph ; extra == 'test' + - psutil ; extra == 'test' + - setuptools ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: greenlet + version: 3.3.2 + sha256: b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab + requires_dist: + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + - objgraph ; extra == 'test' + - psutil ; extra == 'test' + - setuptools ; extra == 'test' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.43-h0c6a113_5.conda sha256: d36263cbcbce34ec463ce92bd72efa198b55d987959eab6210cc256a0e79573b md5: 67d00e9cfe751cfe581726c5eff7c184 @@ -2999,6 +3403,11 @@ packages: - docopt ; extra == 'testing' - pytest<9.0.0 ; extra == 'testing' requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl + name: jmespath + version: 1.1.0 + sha256: a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 md5: b38117a3c920364aff79f870c984b4a3 @@ -4272,6 +4681,27 @@ packages: - typing-extensions - urllib3 requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl + name: multidict + version: 6.7.1 + sha256: 935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 + requires_dist: + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: multidict + version: 6.7.1 + sha256: 9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 + requires_dist: + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: multidict + version: 6.7.1 + sha256: e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23 + requires_dist: + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 md5: 47e340acb35de30501a76c7c799c41d7 @@ -4972,6 +5402,58 @@ packages: - pytest-benchmark ; extra == 'testing' - coverage ; extra == 'testing' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/0a/49/737c1a6273c585719858261753da0b688454d1b634438ccba8a9c4eb5aab/polars-1.38.1-py3-none-any.whl + name: polars + version: 1.38.1 + sha256: a29479c48fed4984d88b656486d221f638cba45d3e961631a50ee5fdde38cb2c + requires_dist: + - polars-runtime-32==1.38.1 + - polars-runtime-64==1.38.1 ; extra == 'rt64' + - polars-runtime-compat==1.38.1 ; extra == 'rtcompat' + - polars-cloud>=0.4.0 ; extra == 'polars-cloud' + - numpy>=1.16.0 ; extra == 'numpy' + - pandas ; extra == 'pandas' + - polars[pyarrow] ; extra == 'pandas' + - pyarrow>=7.0.0 ; extra == 'pyarrow' + - pydantic ; extra == 'pydantic' + - fastexcel>=0.9 ; extra == 'calamine' + - openpyxl>=3.0.0 ; extra == 'openpyxl' + - xlsx2csv>=0.8.0 ; extra == 'xlsx2csv' + - xlsxwriter ; extra == 'xlsxwriter' + - polars[calamine,openpyxl,xlsx2csv,xlsxwriter] ; extra == 'excel' + - adbc-driver-manager[dbapi] ; extra == 'adbc' + - adbc-driver-sqlite[dbapi] ; extra == 'adbc' + - connectorx>=0.3.2 ; extra == 'connectorx' + - sqlalchemy ; extra == 'sqlalchemy' + - polars[pandas] ; extra == 'sqlalchemy' + - polars[adbc,connectorx,sqlalchemy] ; extra == 'database' + - fsspec ; extra == 'fsspec' + - deltalake>=1.0.0 ; extra == 'deltalake' + - pyiceberg>=0.7.1 ; extra == 'iceberg' + - gevent ; extra == 'async' + - cloudpickle ; extra == 'cloudpickle' + - matplotlib ; extra == 'graph' + - altair>=5.4.0 ; extra == 'plot' + - great-tables>=0.8.0 ; extra == 'style' + - tzdata ; sys_platform == 'win32' and extra == 'timezone' + - cudf-polars-cu12 ; extra == 'gpu' + - polars[async,cloudpickle,database,deltalake,excel,fsspec,graph,iceberg,numpy,pandas,plot,pyarrow,pydantic,style,timezone] ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a7/fb/599ff3709e6a303024efd7edfd08cf8de55c6ac39527d8f41cbc4399385f/polars_runtime_32-1.38.1-cp310-abi3-macosx_11_0_arm64.whl + name: polars-runtime-32 + version: 1.38.1 + sha256: c49acac34cc4049ed188f1eb67d6ff3971a39b4af7f7b734b367119970f313ac + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/dc/8c/3ac18d6f89dc05fe2c7c0ee1dc5b81f77a5c85ad59898232c2500fe2ebbf/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + name: polars-runtime-32 + version: 1.38.1 + sha256: fef2ef2626a954e010e006cc8e4de467ecf32d08008f130cea1c78911f545323 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/f2/5a/61d60ec5cc0ab37cbd5a699edb2f9af2875b7fdfdfb2a4608ca3cc5f0448/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: polars-runtime-32 + version: 1.38.1 + sha256: e8a5f7a8125e2d50e2e060296551c929aec09be23a9edcb2b12ca923f555a5ba + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl name: pre-commit version: 4.3.0 @@ -5001,6 +5483,36 @@ packages: requires_dist: - wcwidth requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: propcache + version: 0.4.1 + sha256: 333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl + name: propcache + version: 0.4.1 + sha256: cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: propcache + version: 0.4.1 + sha256: d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + name: psycopg2-binary + version: 2.9.11 + sha256: 8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: psycopg2-binary + version: 2.9.11 + sha256: 5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl + name: psycopg2-binary + version: 2.9.11 + sha256: 366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda sha256: 9c88f8c64590e9567c6c80823f0328e58d3b1efb0e1c539c0315ceca764e0973 md5: b3c17d95b5a10c6e64a21fa17573e70e @@ -5032,6 +5544,21 @@ packages: sha256: 1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0 requires_dist: - pytest ; extra == 'tests' +- pypi: https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl + name: pyarrow + version: 23.0.1 + sha256: 6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl + name: pyarrow + version: 23.0.1 + sha256: 9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl + name: pyarrow + version: 23.0.1 + sha256: 71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677 + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl name: pycparser version: '2.23' @@ -5203,28 +5730,6 @@ packages: - pytest-xdist ; extra == 'testing' - virtualenv ; extra == 'testing' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl - name: pytest-env - version: 1.1.5 - sha256: ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30 - requires_dist: - - pytest>=8.3.3 - - tomli>=2.0.1 ; python_full_version < '3.11' - - covdefaults>=2.3 ; extra == 'testing' - - coverage>=7.6.1 ; extra == 'testing' - - pytest-mock>=3.14 ; extra == 'testing' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/27/98/822b924a4a3eb58aacba84444c7439fce32680592f394de26af9c76e2569/pytest_env-1.2.0-py3-none-any.whl - name: pytest-env - version: 1.2.0 - sha256: d7e5b7198f9b83c795377c09feefa45d56083834e60d04767efd64819fc9da00 - requires_dist: - - pytest>=8.4.2 - - tomli>=2.2.1 ; python_full_version < '3.11' - - covdefaults>=2.3 ; extra == 'testing' - - coverage>=7.10.7 ; extra == 'testing' - - pytest-mock>=3.15.1 ; extra == 'testing' - requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda build_number: 100 sha256: 16cc30a5854f31ca6c3688337d34e37a79cdc518a06375fe3482ea8e2d6b34c8 @@ -5405,68 +5910,134 @@ packages: version: 0.14.9 sha256: 72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - name: setuptools - version: 80.9.0 - sha256: 062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 +- pypi: https://files.pythonhosted.org/packages/57/e1/64c264db50b68de8a438b60ceeb921b2f22da3ebb7ad6255150225d0beac/s3fs-2026.2.0-py3-none-any.whl + name: s3fs + version: 2026.2.0 + sha256: 65198835b86b1d5771112b0085d1da52a6ede36508b1aaa6cae2aedc765dfe10 requires_dist: - - pytest>=6,!=8.1.* ; extra == 'test' - - virtualenv>=13.0.0 ; extra == 'test' - - wheel>=0.44.0 ; extra == 'test' - - pip>=19.1 ; extra == 'test' - - packaging>=24.2 ; extra == 'test' - - jaraco-envs>=2.2 ; extra == 'test' - - pytest-xdist>=3 ; extra == 'test' - - jaraco-path>=3.7.2 ; extra == 'test' - - build[virtualenv]>=1.0.3 ; extra == 'test' - - filelock>=3.4.0 ; extra == 'test' - - ini2toml[lite]>=0.14 ; extra == 'test' - - tomli-w>=1.0.0 ; extra == 'test' - - pytest-timeout ; extra == 'test' - - pytest-perf ; sys_platform != 'cygwin' and extra == 'test' - - jaraco-develop>=7.21 ; python_full_version >= '3.9' and sys_platform != 'cygwin' and extra == 'test' - - pytest-home>=0.5 ; extra == 'test' - - pytest-subprocess ; extra == 'test' - - pyproject-hooks!=1.1 ; extra == 'test' - - jaraco-test>=5.5 ; extra == 'test' - - sphinx>=3.5 ; extra == 'doc' - - jaraco-packaging>=9.3 ; extra == 'doc' - - rst-linker>=1.9 ; extra == 'doc' - - furo ; extra == 'doc' - - sphinx-lint ; extra == 'doc' - - jaraco-tidelift>=1.4 ; extra == 'doc' - - pygments-github-lexers==0.0.5 ; extra == 'doc' - - sphinx-favicon ; extra == 'doc' - - sphinx-inline-tabs ; extra == 'doc' - - sphinx-reredirects ; extra == 'doc' - - sphinxcontrib-towncrier ; extra == 'doc' - - sphinx-notfound-page>=1,<2 ; extra == 'doc' - - pyproject-hooks!=1.1 ; extra == 'doc' - - towncrier<24.7 ; extra == 'doc' - - packaging>=24.2 ; extra == 'core' - - more-itertools>=8.8 ; extra == 'core' - - jaraco-text>=3.7 ; extra == 'core' - - importlib-metadata>=6 ; python_full_version < '3.10' and extra == 'core' - - tomli>=2.0.1 ; python_full_version < '3.11' and extra == 'core' - - wheel>=0.43.0 ; extra == 'core' - - platformdirs>=4.2.2 ; extra == 'core' - - jaraco-functools>=4 ; extra == 'core' - - more-itertools ; extra == 'core' - - pytest-checkdocs>=2.4 ; extra == 'check' - - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' - - ruff>=0.8.0 ; sys_platform != 'cygwin' and extra == 'check' - - pytest-cov ; extra == 'cover' - - pytest-enabler>=2.2 ; extra == 'enabler' - - pytest-mypy ; extra == 'type' - - mypy==1.14.* ; extra == 'type' - - importlib-metadata>=7.0.2 ; python_full_version < '3.10' and extra == 'type' - - jaraco-develop>=7.21 ; sys_platform != 'cygwin' and extra == 'type' - requires_python: '>=3.9' + - aiobotocore>=2.19.0,<4.0.0 + - fsspec==2026.2.0 + - aiohttp!=4.0.0a0,!=4.0.0a1 + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl name: six version: 1.17.0 sha256: 4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: sqlalchemy + version: 2.0.48 + sha256: 2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' + - typing-extensions>=4.6.0 + - greenlet>=1 ; extra == 'asyncio' + - mypy>=0.910 ; extra == 'mypy' + - pyodbc ; extra == 'mssql' + - pymssql ; extra == 'mssql-pymssql' + - pyodbc ; extra == 'mssql-pyodbc' + - mysqlclient>=1.4.0 ; extra == 'mysql' + - mysql-connector-python ; extra == 'mysql-connector' + - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' + - cx-oracle>=8 ; extra == 'oracle' + - oracledb>=1.0.1 ; extra == 'oracle-oracledb' + - psycopg2>=2.7 ; extra == 'postgresql' + - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' + - greenlet>=1 ; extra == 'postgresql-asyncpg' + - asyncpg ; extra == 'postgresql-asyncpg' + - psycopg2-binary ; extra == 'postgresql-psycopg2binary' + - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' + - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' + - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' + - pymysql ; extra == 'pymysql' + - greenlet>=1 ; extra == 'aiomysql' + - aiomysql>=0.2.0 ; extra == 'aiomysql' + - greenlet>=1 ; extra == 'aioodbc' + - aioodbc ; extra == 'aioodbc' + - greenlet>=1 ; extra == 'asyncmy' + - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' + - greenlet>=1 ; extra == 'aiosqlite' + - aiosqlite ; extra == 'aiosqlite' + - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' + - sqlcipher3-binary ; extra == 'sqlcipher' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl + name: sqlalchemy + version: 2.0.48 + sha256: e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4 + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' + - typing-extensions>=4.6.0 + - greenlet>=1 ; extra == 'asyncio' + - mypy>=0.910 ; extra == 'mypy' + - pyodbc ; extra == 'mssql' + - pymssql ; extra == 'mssql-pymssql' + - pyodbc ; extra == 'mssql-pyodbc' + - mysqlclient>=1.4.0 ; extra == 'mysql' + - mysql-connector-python ; extra == 'mysql-connector' + - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' + - cx-oracle>=8 ; extra == 'oracle' + - oracledb>=1.0.1 ; extra == 'oracle-oracledb' + - psycopg2>=2.7 ; extra == 'postgresql' + - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' + - greenlet>=1 ; extra == 'postgresql-asyncpg' + - asyncpg ; extra == 'postgresql-asyncpg' + - psycopg2-binary ; extra == 'postgresql-psycopg2binary' + - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' + - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' + - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' + - pymysql ; extra == 'pymysql' + - greenlet>=1 ; extra == 'aiomysql' + - aiomysql>=0.2.0 ; extra == 'aiomysql' + - greenlet>=1 ; extra == 'aioodbc' + - aioodbc ; extra == 'aioodbc' + - greenlet>=1 ; extra == 'asyncmy' + - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' + - greenlet>=1 ; extra == 'aiosqlite' + - aiosqlite ; extra == 'aiosqlite' + - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' + - sqlcipher3-binary ; extra == 'sqlcipher' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: sqlalchemy + version: 2.0.48 + sha256: b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' + - typing-extensions>=4.6.0 + - greenlet>=1 ; extra == 'asyncio' + - mypy>=0.910 ; extra == 'mypy' + - pyodbc ; extra == 'mssql' + - pymssql ; extra == 'mssql-pymssql' + - pyodbc ; extra == 'mssql-pyodbc' + - mysqlclient>=1.4.0 ; extra == 'mysql' + - mysql-connector-python ; extra == 'mysql-connector' + - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' + - cx-oracle>=8 ; extra == 'oracle' + - oracledb>=1.0.1 ; extra == 'oracle-oracledb' + - psycopg2>=2.7 ; extra == 'postgresql' + - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' + - greenlet>=1 ; extra == 'postgresql-asyncpg' + - asyncpg ; extra == 'postgresql-asyncpg' + - psycopg2-binary ; extra == 'postgresql-psycopg2binary' + - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' + - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' + - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' + - pymysql ; extra == 'pymysql' + - greenlet>=1 ; extra == 'aiomysql' + - aiomysql>=0.2.0 ; extra == 'aiomysql' + - greenlet>=1 ; extra == 'aioodbc' + - aioodbc ; extra == 'aioodbc' + - greenlet>=1 ; extra == 'asyncmy' + - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' + - greenlet>=1 ; extra == 'aiosqlite' + - aiosqlite ; extra == 'aiosqlite' + - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' + - sqlcipher3-binary ; extra == 'sqlcipher' + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl name: stack-data version: 0.6.3 @@ -5480,6 +6051,50 @@ packages: - pygments ; extra == 'tests' - littleutils ; extra == 'tests' - cython ; extra == 'tests' +- pypi: https://files.pythonhosted.org/packages/c8/31/5e7b23f9e43ff7fd46d243808d70c5e8daf3bc08ecf5a7fb84d5e38f7603/testcontainers-4.14.1-py3-none-any.whl + name: testcontainers + version: 4.14.1 + sha256: 03dfef4797b31c82e7b762a454b6afec61a2a512ad54af47ab41e4fa5415f891 + requires_dist: + - azure-cosmos>=4,<5 ; extra == 'cosmosdb' + - azure-storage-blob>=12,<13 ; extra == 'azurite' + - bcrypt>=5,<6 ; extra == 'registry' + - boto3>=1,<2 ; extra == 'aws' or extra == 'localstack' + - cassandra-driver>=3,<4 ; extra == 'scylla' + - chromadb-client>=1,<2 ; extra == 'chroma' + - cryptography ; extra == 'mailpit' or extra == 'sftp' + - docker + - google-cloud-datastore>=2,<3 ; extra == 'google' + - google-cloud-pubsub>=2,<3 ; extra == 'google' + - httpx ; extra == 'aws' or extra == 'generic' or extra == 'test-module-import' + - ibm-db-sa ; platform_machine != 'aarch64' and platform_machine != 'arm64' and extra == 'db2' + - influxdb>=5,<6 ; extra == 'influxdb' + - influxdb-client>=1,<2 ; extra == 'influxdb' + - kubernetes ; extra == 'k3s' + - minio>=7,<8 ; extra == 'minio' + - nats-py>=2,<3 ; extra == 'nats' + - neo4j>=6,<7 ; extra == 'neo4j' + - openfga-sdk ; extra == 'openfga' + - opensearch-py>=3,<4 ; python_full_version < '4' and extra == 'opensearch' + - oracledb>=3,<4 ; extra == 'oracle' or extra == 'oracle-free' + - pika>=1,<2 ; extra == 'rabbitmq' + - pymongo>=4,<5 ; extra == 'mongodb' + - pymssql>=2,<3 ; extra == 'mssql' + - pymysql[rsa]>=1,<2 ; extra == 'mysql' + - python-arango>=8,<9 ; extra == 'arangodb' + - python-dotenv + - python-keycloak>=6,<7 ; python_full_version < '4' and extra == 'keycloak' + - pyyaml>=6.0.3 ; extra == 'k3s' + - qdrant-client>=1,<2 ; extra == 'qdrant' + - redis>=7,<8 ; extra == 'generic' or extra == 'redis' + - selenium>=4,<5 ; extra == 'selenium' + - sqlalchemy>=2,<3 ; extra == 'db2' or extra == 'mssql' or extra == 'mysql' or extra == 'oracle' or extra == 'oracle-free' + - trino ; extra == 'trino' + - typing-extensions + - urllib3 + - weaviate-client>=4,<5 ; extra == 'weaviate' + - wrapt + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda sha256: a84ff687119e6d8752346d1d408d5cf360dee0badd487a472aa8ddedfdc219e1 md5: a0116df4f4ed05c303811a837d5b39d8 @@ -5678,6 +6293,30 @@ packages: version: 0.2.14 sha256: a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1 requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: wrapt + version: 2.1.2 + sha256: bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb + requires_dist: + - pytest ; extra == 'dev' + - setuptools ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: wrapt + version: 2.1.2 + sha256: 16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca + requires_dist: + - pytest ; extra == 'dev' + - setuptools ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl + name: wrapt + version: 2.1.2 + sha256: 4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e + requires_dist: + - pytest ; extra == 'dev' + - setuptools ; extra == 'dev' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.45-hb9d3cd8_0.conda sha256: a5d4af601f71805ec67403406e147c48d6bad7aaeae92b0622b7e2396842d3fe md5: 397a013c2dc5145a70737871aaa87e98 @@ -6090,6 +6729,33 @@ packages: purls: [] size: 566948 timestamp: 1726847598167 +- pypi: https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: yarl + version: 1.23.0 + sha256: 34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4 + requires_dist: + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl + name: yarl + version: 1.23.0 + sha256: 7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b + requires_dist: + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: yarl + version: 1.23.0 + sha256: 2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035 + requires_dist: + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda sha256: a4166e3d8ff4e35932510aaff7aa90772f84b4d07e9f6f83c614cba7ceefe0eb md5: 6432cb5d4ac0046c3ac0a8a0f95842f9 diff --git a/src/datajoint/builtin_codecs/attach.py b/src/datajoint/builtin_codecs/attach.py index aa10f2424..9100db5b0 100644 --- a/src/datajoint/builtin_codecs/attach.py +++ b/src/datajoint/builtin_codecs/attach.py @@ -107,6 +107,7 @@ def decode(self, stored: bytes, *, key: dict | None = None) -> str: config = (key or {}).get("_config") if config is None: from ..settings import config + assert config is not None download_path = Path(config.get("download_path", ".")) download_path.mkdir(parents=True, exist_ok=True) local_path = download_path / filename diff --git a/src/datajoint/builtin_codecs/filepath.py b/src/datajoint/builtin_codecs/filepath.py index a0400499b..cb03f4783 100644 --- a/src/datajoint/builtin_codecs/filepath.py +++ b/src/datajoint/builtin_codecs/filepath.py @@ -103,6 +103,7 @@ def encode(self, value: Any, *, key: dict | None = None, store_name: str | None config = (key or {}).get("_config") if config is None: from ..settings import config + assert config is not None path = str(value) diff --git a/src/datajoint/diagram.py b/src/datajoint/diagram.py index 75e00c21c..230d53b39 100644 --- a/src/datajoint/diagram.py +++ b/src/datajoint/diagram.py @@ -1,12 +1,18 @@ """ -Diagram visualization for DataJoint schemas. +Diagram for DataJoint schemas. -This module provides the Diagram class for visualizing schema structure -as directed acyclic graphs showing tables and their foreign key relationships. +This module provides the Diagram class for constructing derived views of the +dependency graph. Diagram supports set operators (+, -, *) for selecting subsets +of tables, restriction propagation (cascade, restrict) for selecting subsets of +data, and operations (delete, drop, preview) for acting on those selections. + +Visualization methods (draw, make_dot, make_svg, etc.) require matplotlib and +pygraphviz. All other methods are always available. """ from __future__ import annotations +import copy as copy_module import functools import inspect import io @@ -14,10 +20,12 @@ import networkx as nx -from .dependencies import topo_sort -from .errors import DataJointError +from .condition import AndList +from .dependencies import extract_master, topo_sort +from .errors import DataJointError, IntegrityError from .table import Table, lookup_class_name from .user_tables import Computed, Imported, Lookup, Manual, Part, _AliasNode, _get_tier +from .utils import user_choice try: from matplotlib import pyplot as plt @@ -37,1002 +45,1511 @@ logger = logging.getLogger(__name__.split(".")[0]) -if not diagram_active: # noqa: C901 - - class Diagram: +class Diagram(nx.DiGraph): # noqa: C901 + """ + Schema diagram as a directed acyclic graph (DAG). + + Visualizes tables and foreign key relationships derived from + ``connection.dependencies``. + + Parameters + ---------- + source : Table, Schema, or module + A table object, table class, schema, or module with a schema. + context : dict, optional + Namespace for resolving table class names. If None, uses caller's + frame globals/locals. + + Examples + -------- + >>> diag = dj.Diagram(schema.MyTable) + >>> diag.draw() + + Operators: + + - ``diag1 + diag2`` - union of diagrams + - ``diag1 - diag2`` - difference of diagrams + - ``diag1 * diag2`` - intersection of diagrams + - ``diag + n`` - expand n levels of successors (children) + - ``diag - n`` - expand n levels of predecessors (parents) + + >>> dj.Diagram(schema.Table) + 1 - 1 # immediate ancestors and descendants + + Notes + ----- + ``diagram + 1 - 1`` may differ from ``diagram - 1 + 1``. + Only tables loaded in the connection are displayed. + + Layout direction is controlled via ``dj.config.display.diagram_direction`` + (default ``"TB"``). Use ``dj.config.override()`` to change temporarily:: + + with dj.config.override(display_diagram_direction="LR"): + dj.Diagram(schema).draw() + """ + + def __init__(self, source, context=None) -> None: + if isinstance(source, Diagram): + # copy constructor + self.nodes_to_show = set(source.nodes_to_show) + self._expanded_nodes = set(source._expanded_nodes) + self.context = source.context + self._connection = source._connection + self._cascade_restrictions = copy_module.deepcopy(source._cascade_restrictions) + self._restrict_conditions = copy_module.deepcopy(source._restrict_conditions) + self._restriction_attrs = copy_module.deepcopy(source._restriction_attrs) + self._part_integrity = getattr(source, "_part_integrity", "enforce") + super().__init__(source) + return + + # get the caller's context + if context is None: + frame = inspect.currentframe().f_back + self.context = dict(frame.f_globals, **frame.f_locals) + del frame + else: + self.context = context + + # find connection in the source + try: + connection = source.connection + except AttributeError: + try: + connection = source.schema.connection + except AttributeError: + raise DataJointError("Could not find database connection in %s" % repr(source[0])) + + # initialize graph from dependencies + connection.dependencies.load() + super().__init__(connection.dependencies) + self._connection = connection + self._cascade_restrictions = {} + self._restrict_conditions = {} + self._restriction_attrs = {} + + # Enumerate nodes from all the items in the list + self.nodes_to_show = set() + try: + self.nodes_to_show.add(source.full_table_name) + except AttributeError: + try: + database = source.database + except AttributeError: + try: + database = source.schema.database + except AttributeError: + raise DataJointError("Cannot plot Diagram for %s" % repr(source)) + for node in self: + # Handle both MySQL backticks and PostgreSQL double quotes + if node.startswith("`%s`" % database) or node.startswith('"%s"' % database): + self.nodes_to_show.add(node) + # All nodes start as expanded + self._expanded_nodes = set(self.nodes_to_show) + + @classmethod + def from_sequence(cls, sequence) -> "Diagram": """ - Schema diagram (disabled). + Create combined Diagram from a sequence of sources. - Diagram visualization requires matplotlib and pygraphviz packages. - Install them to enable this feature. + Parameters + ---------- + sequence : iterable + Sequence of table objects, classes, or schemas. - See Also - -------- - https://docs.datajoint.com/how-to/installation/ + Returns + ------- + Diagram + Union of diagrams: ``Diagram(arg1) + ... + Diagram(argn)``. """ + return functools.reduce(lambda x, y: x + y, map(Diagram, sequence)) - def __init__(self, *args, **kwargs) -> None: - logger.warning("Please install matplotlib and pygraphviz libraries to enable the Diagram feature.") - -else: - - class Diagram(nx.DiGraph): + @classmethod + def _from_table(cls, table_expr) -> "Diagram": """ - Schema diagram as a directed acyclic graph (DAG). + Create a Diagram containing table_expr and all its descendants. - Visualizes tables and foreign key relationships derived from - ``connection.dependencies``. + Internal factory for ``Table.delete()`` and ``Table.drop()``. + Bypasses the normal ``__init__`` which does caller-frame introspection + and source-type resolution. Parameters ---------- - source : Table, Schema, or module - A table object, table class, schema, or module with a schema. - context : dict, optional - Namespace for resolving table class names. If None, uses caller's - frame globals/locals. + table_expr : Table + A table instance with ``connection`` and ``full_table_name``. - Examples - -------- - >>> diag = dj.Diagram(schema.MyTable) - >>> diag.draw() + Returns + ------- + Diagram + """ + conn = table_expr.connection + conn.dependencies.load() + descendants = set(conn.dependencies.descendants(table_expr.full_table_name)) + result = cls.__new__(cls) + nx.DiGraph.__init__(result, conn.dependencies) + result._connection = conn + result.context = {} + result.nodes_to_show = descendants + result._expanded_nodes = set(descendants) + result._cascade_restrictions = {} + result._restrict_conditions = {} + result._restriction_attrs = {} + return result + + def add_parts(self) -> "Diagram": + """ + Add part tables of all masters already in the diagram. - Operators: + Returns + ------- + Diagram + New diagram with part tables included. + """ - - ``diag1 + diag2`` - union of diagrams - - ``diag1 - diag2`` - difference of diagrams - - ``diag1 * diag2`` - intersection of diagrams - - ``diag + n`` - expand n levels of successors (children) - - ``diag - n`` - expand n levels of predecessors (parents) + def is_part(part, master): + part = [s.strip("`") for s in part.split(".")] + master = [s.strip("`") for s in master.split(".")] + return master[0] == part[0] and master[1] + "__" == part[1][: len(master[1]) + 2] - >>> dj.Diagram(schema.Table) + 1 - 1 # immediate ancestors and descendants + self = Diagram(self) # copy + self.nodes_to_show.update(n for n in self.nodes() if any(is_part(n, m) for m in self.nodes_to_show)) + return self - Notes - ----- - ``diagram + 1 - 1`` may differ from ``diagram - 1 + 1``. - Only tables loaded in the connection are displayed. + def collapse(self) -> "Diagram": + """ + Mark all nodes in this diagram as collapsed. - Layout direction is controlled via ``dj.config.display.diagram_direction`` - (default ``"TB"``). Use ``dj.config.override()`` to change temporarily:: + Collapsed nodes are shown as a single node per schema. When combined + with other diagrams using ``+``, expanded nodes win: if a node is + expanded in either operand, it remains expanded in the result. - with dj.config.override(display_diagram_direction="LR"): - dj.Diagram(schema).draw() + Returns + ------- + Diagram + A copy of this diagram with all nodes collapsed. + + Examples + -------- + >>> # Show schema1 expanded, schema2 collapsed into single nodes + >>> dj.Diagram(schema1) + dj.Diagram(schema2).collapse() + + >>> # Collapse all three schemas together + >>> (dj.Diagram(schema1) + dj.Diagram(schema2) + dj.Diagram(schema3)).collapse() + + >>> # Expand one table from collapsed schema + >>> dj.Diagram(schema).collapse() + dj.Diagram(SingleTable) """ + result = Diagram(self) + result._expanded_nodes = set() # All nodes collapsed + return result - def __init__(self, source, context=None) -> None: - if isinstance(source, Diagram): - # copy constructor - self.nodes_to_show = set(source.nodes_to_show) - self._expanded_nodes = set(source._expanded_nodes) - self.context = source.context - self._connection = source._connection - super().__init__(source) - return - - # get the caller's context - if context is None: - frame = inspect.currentframe().f_back - self.context = dict(frame.f_globals, **frame.f_locals) - del frame - else: - self.context = context + def __add__(self, arg) -> "Diagram": + """ + Union or downstream expansion. + + Parameters + ---------- + arg : Diagram or int + Another Diagram for union, or positive int for downstream expansion. - # find connection in the source + Returns + ------- + Diagram + Combined or expanded diagram. + """ + result = Diagram(self) # copy + try: + # Merge nodes and edges from the other diagram + result.add_nodes_from(arg.nodes(data=True)) + result.add_edges_from(arg.edges(data=True)) + result.nodes_to_show.update(arg.nodes_to_show) + # Merge contexts for class name lookups + result.context = {**result.context, **arg.context} + # Expanded wins: union of expanded nodes from both operands + result._expanded_nodes = self._expanded_nodes | arg._expanded_nodes + except AttributeError: try: - connection = source.connection + result.nodes_to_show.add(arg.full_table_name) + result._expanded_nodes.add(arg.full_table_name) except AttributeError: - try: - connection = source.schema.connection - except AttributeError: - raise DataJointError("Could not find database connection in %s" % repr(source[0])) + for i in range(arg): + new = nx.algorithms.boundary.node_boundary(result, result.nodes_to_show) + if not new: + break + # add nodes referenced by aliased nodes + new.update(nx.algorithms.boundary.node_boundary(result, (a for a in new if a.isdigit()))) + result.nodes_to_show.update(new) + # New nodes from expansion are expanded + result._expanded_nodes = result._expanded_nodes | result.nodes_to_show + return result + + def __sub__(self, arg) -> "Diagram": + """ + Difference or upstream expansion. - # initialize graph from dependencies - self._connection = connection - connection.dependencies.load() - super().__init__(connection.dependencies) + Parameters + ---------- + arg : Diagram or int + Another Diagram for difference, or positive int for upstream expansion. - # Enumerate nodes from all the items in the list - self.nodes_to_show = set() + Returns + ------- + Diagram + Reduced or expanded diagram. + """ + self = Diagram(self) # copy + try: + self.nodes_to_show.difference_update(arg.nodes_to_show) + except AttributeError: try: - self.nodes_to_show.add(source.full_table_name) + self.nodes_to_show.remove(arg.full_table_name) except AttributeError: - try: - database = source.database - except AttributeError: - try: - database = source.schema.database - except AttributeError: - raise DataJointError("Cannot plot Diagram for %s" % repr(source)) - for node in self: - # Handle both MySQL backticks and PostgreSQL double quotes - if node.startswith("`%s`" % database) or node.startswith('"%s"' % database): - self.nodes_to_show.add(node) - # All nodes start as expanded - self._expanded_nodes = set(self.nodes_to_show) - - @classmethod - def from_sequence(cls, sequence) -> "Diagram": - """ - Create combined Diagram from a sequence of sources. - - Parameters - ---------- - sequence : iterable - Sequence of table objects, classes, or schemas. - - Returns - ------- - Diagram - Union of diagrams: ``Diagram(arg1) + ... + Diagram(argn)``. - """ - return functools.reduce(lambda x, y: x + y, map(Diagram, sequence)) - - def add_parts(self) -> "Diagram": - """ - Add part tables of all masters already in the diagram. - - Returns - ------- - Diagram - New diagram with part tables included. - """ - - def is_part(part, master): - part = [s.strip("`") for s in part.split(".")] - master = [s.strip("`") for s in master.split(".")] - return master[0] == part[0] and master[1] + "__" == part[1][: len(master[1]) + 2] - - self = Diagram(self) # copy - self.nodes_to_show.update(n for n in self.nodes() if any(is_part(n, m) for m in self.nodes_to_show)) - return self - - def collapse(self) -> "Diagram": - """ - Mark all nodes in this diagram as collapsed. - - Collapsed nodes are shown as a single node per schema. When combined - with other diagrams using ``+``, expanded nodes win: if a node is - expanded in either operand, it remains expanded in the result. - - Returns - ------- - Diagram - A copy of this diagram with all nodes collapsed. - - Examples - -------- - >>> # Show schema1 expanded, schema2 collapsed into single nodes - >>> dj.Diagram(schema1) + dj.Diagram(schema2).collapse() - - >>> # Collapse all three schemas together - >>> (dj.Diagram(schema1) + dj.Diagram(schema2) + dj.Diagram(schema3)).collapse() - - >>> # Expand one table from collapsed schema - >>> dj.Diagram(schema).collapse() + dj.Diagram(SingleTable) - """ - result = Diagram(self) - result._expanded_nodes = set() # All nodes collapsed - return result + for i in range(arg): + graph = nx.DiGraph(self).reverse() + new = nx.algorithms.boundary.node_boundary(graph, self.nodes_to_show) + if not new: + break + # add nodes referenced by aliased nodes + new.update(nx.algorithms.boundary.node_boundary(graph, (a for a in new if a.isdigit()))) + self.nodes_to_show.update(new) + return self + + def __mul__(self, arg) -> "Diagram": + """ + Intersection of two diagrams. - def __add__(self, arg) -> "Diagram": - """ - Union or downstream expansion. - - Parameters - ---------- - arg : Diagram or int - Another Diagram for union, or positive int for downstream expansion. - - Returns - ------- - Diagram - Combined or expanded diagram. - """ - result = Diagram(self) # copy - try: - # Merge nodes and edges from the other diagram - result.add_nodes_from(arg.nodes(data=True)) - result.add_edges_from(arg.edges(data=True)) - result.nodes_to_show.update(arg.nodes_to_show) - # Merge contexts for class name lookups - result.context = {**result.context, **arg.context} - # Expanded wins: union of expanded nodes from both operands - result._expanded_nodes = self._expanded_nodes | arg._expanded_nodes - except AttributeError: - try: - result.nodes_to_show.add(arg.full_table_name) - result._expanded_nodes.add(arg.full_table_name) - except AttributeError: - for i in range(arg): - new = nx.algorithms.boundary.node_boundary(result, result.nodes_to_show) - if not new: - break - # add nodes referenced by aliased nodes - new.update(nx.algorithms.boundary.node_boundary(result, (a for a in new if a.isdigit()))) - result.nodes_to_show.update(new) - # New nodes from expansion are expanded - result._expanded_nodes = result._expanded_nodes | result.nodes_to_show - return result + Parameters + ---------- + arg : Diagram + Another Diagram. - def __sub__(self, arg) -> "Diagram": - """ - Difference or upstream expansion. - - Parameters - ---------- - arg : Diagram or int - Another Diagram for difference, or positive int for upstream expansion. - - Returns - ------- - Diagram - Reduced or expanded diagram. - """ - self = Diagram(self) # copy - try: - self.nodes_to_show.difference_update(arg.nodes_to_show) - except AttributeError: - try: - self.nodes_to_show.remove(arg.full_table_name) - except AttributeError: - for i in range(arg): - graph = nx.DiGraph(self).reverse() - new = nx.algorithms.boundary.node_boundary(graph, self.nodes_to_show) - if not new: - break - # add nodes referenced by aliased nodes - new.update(nx.algorithms.boundary.node_boundary(graph, (a for a in new if a.isdigit()))) - self.nodes_to_show.update(new) - return self - - def __mul__(self, arg) -> "Diagram": - """ - Intersection of two diagrams. - - Parameters - ---------- - arg : Diagram - Another Diagram. - - Returns - ------- - Diagram - Diagram with nodes present in both operands. - """ - self = Diagram(self) # copy - self.nodes_to_show.intersection_update(arg.nodes_to_show) - return self - - def topo_sort(self) -> list[str]: - """ - Return nodes in topological order. - - Returns - ------- - list[str] - Node names in topological order. - """ - return topo_sort(self) - - def _make_graph(self) -> nx.DiGraph: - """ - Build graph object ready for drawing. - - Returns - ------- - nx.DiGraph - Graph with nodes relabeled to class names. - """ - # mark "distinguished" tables, i.e. those that introduce new primary key - # attributes - # Filter nodes_to_show to only include nodes that exist in the graph - valid_nodes = self.nodes_to_show.intersection(set(self.nodes())) - for name in valid_nodes: - foreign_attributes = set( - attr for p in self.in_edges(name, data=True) for attr in p[2]["attr_map"] if p[2]["primary"] - ) - self.nodes[name]["distinguished"] = ( - "primary_key" in self.nodes[name] and foreign_attributes < self.nodes[name]["primary_key"] - ) - # include aliased nodes that are sandwiched between two displayed nodes - gaps = set(nx.algorithms.boundary.node_boundary(self, valid_nodes)).intersection( - nx.algorithms.boundary.node_boundary(nx.DiGraph(self).reverse(), valid_nodes) + Returns + ------- + Diagram + Diagram with nodes present in both operands. + """ + self = Diagram(self) # copy + self.nodes_to_show.intersection_update(arg.nodes_to_show) + return self + + def topo_sort(self) -> list[str]: + """ + Return nodes in topological order. + + Returns + ------- + list[str] + Node names in topological order. + """ + return topo_sort(self) + + def cascade(self, table_expr, part_integrity="enforce"): + """ + Apply cascade restriction and propagate downstream. + + OR at convergence — a child row is affected if *any* restricted + ancestor taints it. Used for delete. + + Can only be called once on an unrestricted Diagram. Cannot be + mixed with ``restrict()``. + + Parameters + ---------- + table_expr : QueryExpression + A restricted table expression + (e.g., ``Session & 'subject_id=1'``). + part_integrity : str, optional + ``"enforce"`` (default), ``"ignore"``, or ``"cascade"``. + + Returns + ------- + Diagram + New Diagram with cascade restrictions applied. + """ + if self._cascade_restrictions or self._restrict_conditions: + raise DataJointError( + "cascade() can only be called once on an unrestricted Diagram. " + "cascade and restrict modes are mutually exclusive." ) - nodes = valid_nodes.union(a for a in gaps if a.isdigit()) - # construct subgraph and rename nodes to class names - graph = nx.DiGraph(nx.DiGraph(self).subgraph(nodes)) - nx.set_node_attributes(graph, name="node_type", values={n: _get_tier(n) for n in graph}) - # relabel nodes to class names - mapping = {node: lookup_class_name(node, self.context) or node for node in graph.nodes()} - new_names = list(mapping.values()) - if len(new_names) > len(set(new_names)): - raise DataJointError("Some classes have identical names. The Diagram cannot be plotted.") - nx.relabel_nodes(graph, mapping, copy=False) - return graph - - def _apply_collapse(self, graph: nx.DiGraph) -> tuple[nx.DiGraph, dict[str, str]]: - """ - Apply collapse logic to the graph. - - Nodes in nodes_to_show but not in _expanded_nodes are collapsed into - single schema nodes. - - Parameters - ---------- - graph : nx.DiGraph - The graph from _make_graph(). - - Returns - ------- - tuple[nx.DiGraph, dict[str, str]] - Modified graph and mapping of collapsed schema labels to their table count. - """ - # Filter to valid nodes (those that exist in the underlying graph) - valid_nodes = self.nodes_to_show.intersection(set(self.nodes())) - valid_expanded = self._expanded_nodes.intersection(set(self.nodes())) - - # If all nodes are expanded, no collapse needed - if valid_expanded >= valid_nodes: - return graph, {} - - # Map full_table_names to class_names - full_to_class = {node: lookup_class_name(node, self.context) or node for node in valid_nodes} - class_to_full = {v: k for k, v in full_to_class.items()} - - # Identify expanded class names - expanded_class_names = {full_to_class.get(node, node) for node in valid_expanded} - - # Identify nodes to collapse (class names) - nodes_to_collapse = set(graph.nodes()) - expanded_class_names - - if not nodes_to_collapse: - return graph, {} - - # Group collapsed nodes by schema - collapsed_by_schema = {} # schema_name -> list of class_names - for class_name in nodes_to_collapse: - full_name = class_to_full.get(class_name) - if full_name: - parts = full_name.replace('"', "`").split("`") - if len(parts) >= 2: - schema_name = parts[1] - if schema_name not in collapsed_by_schema: - collapsed_by_schema[schema_name] = [] - collapsed_by_schema[schema_name].append(class_name) - - if not collapsed_by_schema: - return graph, {} - - # Determine labels for collapsed schemas - schema_modules = {} - for schema_name, class_names in collapsed_by_schema.items(): - schema_modules[schema_name] = set() - for class_name in class_names: - cls = self._resolve_class(class_name) - if cls is not None and hasattr(cls, "__module__"): - module_name = cls.__module__.split(".")[-1] - schema_modules[schema_name].add(module_name) - - # Collect module names for ALL schemas in the diagram (not just collapsed) - all_schema_modules = {} # schema_name -> module_name - for node in graph.nodes(): - full_name = class_to_full.get(node) - if full_name: - parts = full_name.replace('"', "`").split("`") - if len(parts) >= 2: - db_schema = parts[1] - cls = self._resolve_class(node) - if cls is not None and hasattr(cls, "__module__"): - module_name = cls.__module__.split(".")[-1] - all_schema_modules[db_schema] = module_name - - # Check which module names are shared by multiple schemas - module_to_schemas = {} - for db_schema, module_name in all_schema_modules.items(): - if module_name not in module_to_schemas: - module_to_schemas[module_name] = [] - module_to_schemas[module_name].append(db_schema) - - ambiguous_modules = {m for m, schemas in module_to_schemas.items() if len(schemas) > 1} - - # Determine labels for collapsed schemas - collapsed_labels = {} # schema_name -> label - for schema_name, modules in schema_modules.items(): - if len(modules) == 1: - module_name = next(iter(modules)) - # Use database schema name if module is ambiguous - if module_name in ambiguous_modules: - label = schema_name - else: - label = module_name - else: - label = schema_name - collapsed_labels[schema_name] = label - - # Build counts using final labels - collapsed_counts = {} # label -> count of tables - for schema_name, class_names in collapsed_by_schema.items(): - label = collapsed_labels[schema_name] - collapsed_counts[label] = len(class_names) - - # Create new graph with collapsed nodes - new_graph = nx.DiGraph() - - # Map old node names to new names (collapsed nodes -> schema label) - node_mapping = {} - for node in graph.nodes(): - full_name = class_to_full.get(node) - if full_name: - parts = full_name.replace('"', "`").split("`") - if len(parts) >= 2 and node in nodes_to_collapse: - schema_name = parts[1] - node_mapping[node] = collapsed_labels[schema_name] + result = Diagram(self) + result._part_integrity = part_integrity + node = table_expr.full_table_name + if node not in result.nodes(): + raise DataJointError(f"Table {node} is not in the diagram.") + # Seed restriction + restriction = AndList(table_expr.restriction) + result._cascade_restrictions[node] = [restriction] if restriction else [] + result._restriction_attrs[node] = set(table_expr.restriction_attributes) + # Propagate downstream + result._propagate_restrictions(node, mode="cascade", part_integrity=part_integrity) + return result + + def _restricted_table(self, node): + """ + Return a FreeTable for ``node`` with this diagram's restrictions applied. + + Cascade restrictions are OR-combined (a row is affected if ANY + FK reference points to a deleted row). Restrict conditions are + AND-combined (a row is included only when ALL ancestor conditions + are satisfied). + """ + from .table import FreeTable + + ft = FreeTable(self._connection, node) + restrictions = (self._cascade_restrictions or self._restrict_conditions).get(node, []) + if not restrictions: + return ft + if self._cascade_restrictions: + # OR semantics — passing a list to restrict() creates an OrList + return ft.restrict(restrictions) + else: + # AND semantics — each restriction narrows further + for r in restrictions: + ft = ft.restrict(r) + return ft + + def restrict(self, table_expr): + """ + Apply restrict condition and propagate downstream. + + AND at convergence — a child row is included only if it satisfies + *all* restricted ancestors. Used for export. Can be chained. + + Cannot be called on a cascade-restricted Diagram. + + Parameters + ---------- + table_expr : QueryExpression + A restricted table expression. + + Returns + ------- + Diagram + New Diagram with restrict conditions applied. + """ + if self._cascade_restrictions: + raise DataJointError( + "Cannot apply restrict() on a cascade-restricted Diagram. " + "cascade and restrict modes are mutually exclusive." + ) + result = Diagram(self) + node = table_expr.full_table_name + if node not in result.nodes(): + raise DataJointError(f"Table {node} is not in the diagram.") + # Seed restriction (AND accumulation) + result._restrict_conditions.setdefault(node, AndList()).extend(table_expr.restriction) + result._restriction_attrs.setdefault(node, set()).update(table_expr.restriction_attributes) + # Propagate downstream + result._propagate_restrictions(node, mode="restrict") + return result + + def _propagate_restrictions(self, start_node, mode, part_integrity="enforce"): + """ + Propagate restrictions from start_node to all its descendants. + + Walks the dependency graph in topological order, applying + propagation rules at each edge. Only processes descendants of + start_node to avoid duplicate propagation when chaining. + """ + from .table import FreeTable + + sorted_nodes = topo_sort(self) + # Only propagate through descendants of start_node + allowed_nodes = {start_node} | set(nx.descendants(self, start_node)) + propagated_edges = set() + visited_masters = set() + + restrictions = self._cascade_restrictions if mode == "cascade" else self._restrict_conditions + + # Multiple passes to handle part_integrity="cascade" upward propagation + max_passes = 10 + for _ in range(max_passes): + any_new = False + + for node in sorted_nodes: + if node not in restrictions or node not in allowed_nodes: + continue + + # Build parent FreeTable with current restriction + parent_ft = self._restricted_table(node) + + parent_attrs = self._restriction_attrs.get(node, set()) + + for _, target, edge_props in self.out_edges(node, data=True): + attr_map = edge_props.get("attr_map", {}) + aliased = edge_props.get("aliased", False) + + if target.isdigit(): + # Alias node — follow through to real child + for _, child_node, _ in self.out_edges(target, data=True): + edge_key = (node, target, child_node) + if edge_key in propagated_edges: + continue + propagated_edges.add(edge_key) + was_new = child_node not in restrictions + self._apply_propagation_rule( + parent_ft, + parent_attrs, + child_node, + attr_map, + True, + mode, + restrictions, + ) + if was_new and child_node in restrictions: + any_new = True else: - node_mapping[node] = node + edge_key = (node, target) + if edge_key in propagated_edges: + continue + propagated_edges.add(edge_key) + was_new = target not in restrictions + self._apply_propagation_rule( + parent_ft, + parent_attrs, + target, + attr_map, + aliased, + mode, + restrictions, + ) + if was_new and target in restrictions: + any_new = True + + # part_integrity="cascade": propagate up from part to master + if part_integrity == "cascade" and mode == "cascade": + master_name = extract_master(target) + if ( + master_name + and master_name in self.nodes() + and master_name not in restrictions + and master_name not in visited_masters + ): + visited_masters.add(master_name) + child_ft = self._restricted_table(target) + master_ft = FreeTable(self._connection, master_name) + from .condition import make_condition + + master_restr = make_condition( + master_ft, + (master_ft.proj() & child_ft.proj()).to_arrays(), + master_ft.restriction_attributes, + ) + restrictions[master_name] = [master_restr] + self._restriction_attrs[master_name] = set() + allowed_nodes.add(master_name) + allowed_nodes.update(nx.descendants(self, master_name)) + any_new = True + + if not any_new: + break + + def _apply_propagation_rule( + self, + parent_ft, + parent_attrs, + child_node, + attr_map, + aliased, + mode, + restrictions, + ): + """ + Apply one of the 3 propagation rules to a parent→child edge. + + Rules (from table.py restriction propagation): + + 1. Non-aliased AND parent restriction attrs ⊆ child PK: + Copy parent restriction directly. + 2. Aliased FK (attr_map renames columns): + ``parent.proj(**{fk: pk for fk, pk in attr_map.items()})`` + 3. Non-aliased AND parent restriction attrs ⊄ child PK: + ``parent.proj()`` + """ + child_pk = self.nodes[child_node].get("primary_key", set()) + + if not aliased and parent_attrs and parent_attrs <= child_pk: + # Rule 1: copy parent restriction directly + parent_restr = restrictions.get( + parent_ft.full_table_name, + [] if mode == "cascade" else AndList(), + ) + if mode == "cascade": + restrictions.setdefault(child_node, []).extend(parent_restr) + else: + restrictions.setdefault(child_node, AndList()).extend(parent_restr) + child_attrs = set(parent_attrs) + elif aliased: + # Rule 2: aliased FK — project with renaming + child_item = parent_ft.proj(**{fk: pk for fk, pk in attr_map.items()}) + if mode == "cascade": + restrictions.setdefault(child_node, []).append(child_item) + else: + restrictions.setdefault(child_node, AndList()).append(child_item) + child_attrs = set(attr_map.keys()) + else: + # Rule 3: non-aliased, restriction attrs ⊄ child PK — project + child_item = parent_ft.proj() + if mode == "cascade": + restrictions.setdefault(child_node, []).append(child_item) + else: + restrictions.setdefault(child_node, AndList()).append(child_item) + child_attrs = set(attr_map.values()) + + self._restriction_attrs.setdefault(child_node, set()).update(child_attrs) + + def delete(self, transaction=True, prompt=None, dry_run=False): + """ + Execute cascading delete using cascade restrictions. + + Parameters + ---------- + transaction : bool, optional + Wrap in a transaction. Default True. + prompt : bool or None, optional + Show preview and ask confirmation. Default ``dj.config['safemode']``. + dry_run : bool, optional + If True, return affected row counts without deleting. Default False. + + Returns + ------- + int or dict[str, int] + Number of rows deleted from the root table, or (if ``dry_run``) + a mapping of full table name to affected row count. + """ + if dry_run: + return self.preview() + + prompt = self._connection._config["safemode"] if prompt is None else prompt + + if not self._cascade_restrictions: + raise DataJointError("No cascade restrictions applied. Call cascade() first.") + + conn = self._connection + + # Get non-alias nodes with restrictions in topological order + all_sorted = topo_sort(self) + tables = [t for t in all_sorted if not t.isdigit() and t in self._cascade_restrictions] + + # Preview + if prompt: + for t in tables: + ft = self._restricted_table(t) + logger.info("{table} ({count} tuples)".format(table=t, count=len(ft))) + + # Start transaction + if transaction: + if not conn.in_transaction: + conn.start_transaction() + else: + if not prompt: + transaction = False else: - # Alias nodes - check if they should be collapsed - # An alias node should be collapsed if ALL its neighbors are collapsed - neighbors = set(graph.predecessors(node)) | set(graph.successors(node)) - if neighbors and neighbors <= nodes_to_collapse: - # Get schema from first neighbor - neighbor = next(iter(neighbors)) - full_name = class_to_full.get(neighbor) - if full_name: - parts = full_name.replace('"', "`").split("`") - if len(parts) >= 2: - schema_name = parts[1] - node_mapping[node] = collapsed_labels[schema_name] - continue - node_mapping[node] = node + raise DataJointError( + "Delete cannot use a transaction within an " + "ongoing transaction. Set transaction=False " + "or prompt=False." + ) + + # Execute deletes in reverse topological order (leaves first) + root_count = 0 + deleted_tables = set() + try: + for table_name in reversed(tables): + ft = self._restricted_table(table_name) + count = ft.delete_quick(get_count=True) + if count > 0: + deleted_tables.add(table_name) + logger.info("Deleting {count} rows from {table}".format(count=count, table=table_name)) + if table_name == tables[0]: + root_count = count + except IntegrityError as error: + if transaction: + conn.cancel_transaction() + match = conn.adapter.parse_foreign_key_error(error.args[0]) + if match: + raise DataJointError( + "Delete blocked by table {child} in an unloaded " + "schema. Activate all dependent schemas before " + "deleting.".format(child=match["child"]) + ) from None + raise DataJointError("Delete blocked by FK in unloaded/inaccessible schema.") from None + except: + if transaction: + conn.cancel_transaction() + raise + + # Post-check part_integrity="enforce": roll back if a part table + # had rows deleted without its master also having rows deleted. + if getattr(self, "_part_integrity", "enforce") == "enforce" and deleted_tables: + for table_name in deleted_tables: + master = extract_master(table_name) + if master and master not in deleted_tables: + if transaction: + conn.cancel_transaction() + raise DataJointError( + f"Attempt to delete part table {table_name} before " + f"its master {master}. Delete from the master first, " + f"or use part_integrity='ignore' or 'cascade'." + ) + + # Confirm and commit + if root_count == 0: + if prompt: + logger.warning("Nothing to delete.") + if transaction: + conn.cancel_transaction() + elif not transaction: + logger.info("Delete completed") + else: + if not prompt or user_choice("Commit deletes?", default="no") == "yes": + if transaction: + conn.commit_transaction() + if prompt: + logger.info("Delete committed.") + else: + if transaction: + conn.cancel_transaction() + if prompt: + logger.warning("Delete cancelled") + root_count = 0 + return root_count + + def drop(self, prompt=None, part_integrity="enforce", dry_run=False): + """ + Drop all tables in the diagram in reverse topological order. + + Parameters + ---------- + prompt : bool or None, optional + Show preview and ask confirmation. Default ``dj.config['safemode']``. + part_integrity : str, optional + ``"enforce"`` (default) or ``"ignore"``. + dry_run : bool, optional + If True, return row counts without dropping. Default False. + + Returns + ------- + dict[str, int] or None + If ``dry_run``, mapping of full table name to row count. + """ + from .table import FreeTable + + prompt = self._connection._config["safemode"] if prompt is None else prompt + conn = self._connection - # Build reverse mapping: label -> schema_name - label_to_schema = {label: schema for schema, label in collapsed_labels.items()} - - # Add nodes - added_collapsed = set() - for old_node, new_node in node_mapping.items(): - if new_node in collapsed_counts: - # This is a collapsed schema node - if new_node not in added_collapsed: - schema_name = label_to_schema.get(new_node, new_node) - new_graph.add_node( - new_node, - node_type=None, - collapsed=True, - table_count=collapsed_counts[new_node], - schema_name=schema_name, + tables = [t for t in topo_sort(self) if not t.isdigit() and t in self.nodes_to_show] + + if part_integrity == "enforce": + for part in tables: + master = extract_master(part) + if master and master not in tables: + raise DataJointError( + "Attempt to drop part table {part} before its " "master {master}. Drop the master first.".format( + part=part, master=master ) - added_collapsed.add(new_node) - else: - new_graph.add_node(new_node, **graph.nodes[old_node]) - - # Add edges (avoiding self-loops and duplicates) - for src, dest, data in graph.edges(data=True): - new_src = node_mapping[src] - new_dest = node_mapping[dest] - if new_src != new_dest and not new_graph.has_edge(new_src, new_dest): - new_graph.add_edge(new_src, new_dest, **data) - - return new_graph, collapsed_counts - - def _resolve_class(self, name: str): - """ - Safely resolve a table class from a dotted name without eval(). - - Parameters - ---------- - name : str - Dotted class name like "MyTable" or "Module.MyTable". - - Returns - ------- - type or None - The table class if found, otherwise None. - """ - parts = name.split(".") - obj = self.context.get(parts[0]) - for part in parts[1:]: - if obj is None: - return None - obj = getattr(obj, part, None) - if obj is not None and isinstance(obj, type) and issubclass(obj, Table): - return obj - return None - - @staticmethod - def _encapsulate_edge_attributes(graph: nx.DiGraph) -> None: - """ - Encapsulate edge attr_map in double quotes for pydot compatibility. - - Modifies graph in place. - - See Also - -------- - https://github.com/pydot/pydot/issues/258#issuecomment-795798099 - """ - for u, v, *_, edgedata in graph.edges(data=True): - if "attr_map" in edgedata: - graph.edges[u, v]["attr_map"] = '"{0}"'.format(edgedata["attr_map"]) - - @staticmethod - def _encapsulate_node_names(graph: nx.DiGraph) -> None: - """ - Encapsulate node names in double quotes for pydot compatibility. - - Modifies graph in place. - - See Also - -------- - https://github.com/datajoint/datajoint-python/pull/1176 - """ - nx.relabel_nodes( - graph, - {node: '"{0}"'.format(node) for node in graph.nodes()}, - copy=False, + ) + + if dry_run: + result = {} + for t in tables: + count = len(FreeTable(conn, t)) + result[t] = count + logger.info("{table} ({count} tuples)".format(table=t, count=count)) + return result + + do_drop = True + if prompt: + for t in tables: + logger.info("{table} ({count} tuples)".format(table=t, count=len(FreeTable(conn, t)))) + do_drop = user_choice("Proceed?", default="no") == "yes" + if do_drop: + for t in reversed(tables): + FreeTable(conn, t).drop_quick() + logger.info("Tables dropped. Restart kernel.") + + def preview(self): + """ + Show affected tables and row counts without modifying data. + + Returns + ------- + dict[str, int] + Mapping of full table name to affected row count. + """ + restrictions = self._cascade_restrictions or self._restrict_conditions + if not restrictions: + raise DataJointError("No restrictions applied. " "Call cascade() or restrict() first.") + + result = {} + for node in topo_sort(self): + if node.isdigit() or node not in restrictions: + continue + result[node] = len(self._restricted_table(node)) + + for t, count in result.items(): + logger.info("{table} ({count} tuples)".format(table=t, count=count)) + return result + + def prune(self): + """ + Remove tables with zero matching rows from the diagram. + + Without prior restrictions, removes physically empty tables. + With restrictions (``cascade()`` or ``restrict()``), removes + tables where the restricted query yields zero rows. + + Returns + ------- + Diagram + New Diagram with empty tables removed. + """ + from .table import FreeTable + + result = Diagram(self) + restrictions = result._cascade_restrictions or result._restrict_conditions + + if restrictions: + # Restricted: check row counts under restriction + for node in list(restrictions): + if node.isdigit(): + continue + if len(result._restricted_table(node)) == 0: + restrictions.pop(node) + result._restriction_attrs.pop(node, None) + result.nodes_to_show.discard(node) + else: + # Unrestricted: check physical row counts + for node in list(result.nodes_to_show): + if node.isdigit(): + continue + ft = FreeTable(self._connection, node) + if len(ft) == 0: + result.nodes_to_show.discard(node) + + return result + + def _make_graph(self) -> nx.DiGraph: + """ + Build graph object ready for drawing. + + Returns + ------- + nx.DiGraph + Graph with nodes relabeled to class names. + """ + # mark "distinguished" tables, i.e. those that introduce new primary key + # attributes + # Filter nodes_to_show to only include nodes that exist in the graph + valid_nodes = self.nodes_to_show.intersection(set(self.nodes())) + for name in valid_nodes: + foreign_attributes = set( + attr for p in self.in_edges(name, data=True) for attr in p[2]["attr_map"] if p[2]["primary"] ) + self.nodes[name]["distinguished"] = ( + "primary_key" in self.nodes[name] and foreign_attributes < self.nodes[name]["primary_key"] + ) + # include aliased nodes that are sandwiched between two displayed nodes + gaps = set(nx.algorithms.boundary.node_boundary(self, valid_nodes)).intersection( + nx.algorithms.boundary.node_boundary(nx.DiGraph(self).reverse(), valid_nodes) + ) + nodes = valid_nodes.union(a for a in gaps if a.isdigit()) + # construct subgraph and rename nodes to class names + graph = nx.DiGraph(nx.DiGraph(self).subgraph(nodes)) + nx.set_node_attributes(graph, name="node_type", values={n: _get_tier(n) for n in graph}) + # relabel nodes to class names + mapping = {node: lookup_class_name(node, self.context) or node for node in graph.nodes()} + new_names = list(mapping.values()) + if len(new_names) > len(set(new_names)): + raise DataJointError("Some classes have identical names. The Diagram cannot be plotted.") + nx.relabel_nodes(graph, mapping, copy=False) + return graph + + def _apply_collapse(self, graph: nx.DiGraph) -> tuple[nx.DiGraph, dict[str, str]]: + """ + Apply collapse logic to the graph. + + Nodes in nodes_to_show but not in _expanded_nodes are collapsed into + single schema nodes. + + Parameters + ---------- + graph : nx.DiGraph + The graph from _make_graph(). + + Returns + ------- + tuple[nx.DiGraph, dict[str, str]] + Modified graph and mapping of collapsed schema labels to their table count. + """ + # Filter to valid nodes (those that exist in the underlying graph) + valid_nodes = self.nodes_to_show.intersection(set(self.nodes())) + valid_expanded = self._expanded_nodes.intersection(set(self.nodes())) - def make_dot(self): - """ - Generate a pydot graph object. - - Returns - ------- - pydot.Dot - The graph object ready for rendering. - - Notes - ----- - Layout direction is controlled via ``dj.config.display.diagram_direction``. - Tables are grouped by schema, with the Python module name shown as the - group label when available. - """ - direction = self._connection._config.display.diagram_direction - graph = self._make_graph() - - # Apply collapse logic if needed - graph, collapsed_counts = self._apply_collapse(graph) - - # Build schema mapping: class_name -> schema_name - # Group by database schema, label with Python module name if 1:1 mapping - schema_map = {} # class_name -> schema_name - schema_modules = {} # schema_name -> set of module names - - for full_name in self.nodes_to_show: - # Extract schema from full table name like `schema`.`table` or "schema"."table" + # If all nodes are expanded, no collapse needed + if valid_expanded >= valid_nodes: + return graph, {} + + # Map full_table_names to class_names + full_to_class = {node: lookup_class_name(node, self.context) or node for node in valid_nodes} + class_to_full = {v: k for k, v in full_to_class.items()} + + # Identify expanded class names + expanded_class_names = {full_to_class.get(node, node) for node in valid_expanded} + + # Identify nodes to collapse (class names) + nodes_to_collapse = set(graph.nodes()) - expanded_class_names + + if not nodes_to_collapse: + return graph, {} + + # Group collapsed nodes by schema + collapsed_by_schema = {} # schema_name -> list of class_names + for class_name in nodes_to_collapse: + full_name = class_to_full.get(class_name) + if full_name: + parts = full_name.replace('"', "`").split("`") + if len(parts) >= 2: + schema_name = parts[1] + if schema_name not in collapsed_by_schema: + collapsed_by_schema[schema_name] = [] + collapsed_by_schema[schema_name].append(class_name) + + if not collapsed_by_schema: + return graph, {} + + # Determine labels for collapsed schemas + schema_modules = {} + for schema_name, class_names in collapsed_by_schema.items(): + schema_modules[schema_name] = set() + for class_name in class_names: + cls = self._resolve_class(class_name) + if cls is not None and hasattr(cls, "__module__"): + module_name = cls.__module__.split(".")[-1] + schema_modules[schema_name].add(module_name) + + # Collect module names for ALL schemas in the diagram (not just collapsed) + all_schema_modules = {} # schema_name -> module_name + for node in graph.nodes(): + full_name = class_to_full.get(node) + if full_name: parts = full_name.replace('"', "`").split("`") if len(parts) >= 2: - schema_name = parts[1] # schema is between first pair of backticks - class_name = lookup_class_name(full_name, self.context) or full_name - schema_map[class_name] = schema_name - - # Collect all module names for this schema - if schema_name not in schema_modules: - schema_modules[schema_name] = set() - cls = self._resolve_class(class_name) + db_schema = parts[1] + cls = self._resolve_class(node) if cls is not None and hasattr(cls, "__module__"): module_name = cls.__module__.split(".")[-1] - schema_modules[schema_name].add(module_name) - - # Determine cluster labels: use module name if 1:1, else database schema name - cluster_labels = {} # schema_name -> label - for schema_name, modules in schema_modules.items(): - if len(modules) == 1: - cluster_labels[schema_name] = next(iter(modules)) - else: - cluster_labels[schema_name] = schema_name - - # Disambiguate labels if multiple schemas share the same module name - # (e.g., all defined in __main__ in a notebook) - label_counts = {} - for label in cluster_labels.values(): - label_counts[label] = label_counts.get(label, 0) + 1 - - for schema_name, label in cluster_labels.items(): - if label_counts[label] > 1: - # Multiple schemas share this module name - add schema name - cluster_labels[schema_name] = f"{label} ({schema_name})" - - # Assign alias nodes (orange dots) to the same schema as their child table - for node, data in graph.nodes(data=True): - if data.get("node_type") is _AliasNode: - # Find the child (successor) - the table that declares the renamed FK - successors = list(graph.successors(node)) - if successors and successors[0] in schema_map: - schema_map[node] = schema_map[successors[0]] - - # Assign collapsed nodes to their schema so they appear in the cluster - for node, data in graph.nodes(data=True): - if data.get("collapsed") and data.get("schema_name"): - schema_map[node] = data["schema_name"] - - scale = 1.2 # scaling factor for fonts and boxes - label_props = { # http://matplotlib.org/examples/color/named_colors.html - None: dict( - shape="circle", - color="#FFFF0040", - fontcolor="yellow", - fontsize=round(scale * 8), - size=0.4 * scale, - fixed=False, - ), - _AliasNode: dict( - shape="circle", - color="#FF880080", - fontcolor="#FF880080", - fontsize=round(scale * 0), - size=0.05 * scale, - fixed=True, - ), - Manual: dict( - shape="box", - color="#00FF0030", - fontcolor="darkgreen", - fontsize=round(scale * 10), - size=0.4 * scale, - fixed=False, - ), - Lookup: dict( - shape="plaintext", - color="#00000020", - fontcolor="black", - fontsize=round(scale * 8), - size=0.4 * scale, - fixed=False, - ), - Computed: dict( - shape="ellipse", - color="#FF000020", - fontcolor="#7F0000A0", - fontsize=round(scale * 10), - size=0.4 * scale, - fixed=False, - ), - Imported: dict( - shape="ellipse", - color="#00007F40", - fontcolor="#00007FA0", - fontsize=round(scale * 10), - size=0.4 * scale, - fixed=False, - ), - Part: dict( - shape="plaintext", - color="#00000000", - fontcolor="black", - fontsize=round(scale * 8), - size=0.1 * scale, - fixed=False, - ), - "collapsed": dict( - shape="box3d", - color="#80808060", - fontcolor="#404040", - fontsize=round(scale * 10), - size=0.5 * scale, - fixed=False, - ), - } - # Build node_props, handling collapsed nodes specially - node_props = {} - for node, d in graph.nodes(data=True): - if d.get("collapsed"): - node_props[node] = label_props["collapsed"] + all_schema_modules[db_schema] = module_name + + # Check which module names are shared by multiple schemas + module_to_schemas = {} + for db_schema, module_name in all_schema_modules.items(): + if module_name not in module_to_schemas: + module_to_schemas[module_name] = [] + module_to_schemas[module_name].append(db_schema) + + ambiguous_modules = {m for m, schemas in module_to_schemas.items() if len(schemas) > 1} + + # Determine labels for collapsed schemas + collapsed_labels = {} # schema_name -> label + for schema_name, modules in schema_modules.items(): + if len(modules) == 1: + module_name = next(iter(modules)) + # Use database schema name if module is ambiguous + if module_name in ambiguous_modules: + label = schema_name else: - node_props[node] = label_props[d["node_type"]] - - self._encapsulate_node_names(graph) - self._encapsulate_edge_attributes(graph) - dot = nx.drawing.nx_pydot.to_pydot(graph) - dot.set_rankdir(direction) - for node in dot.get_nodes(): - node.set_shape("circle") - name = node.get_name().strip('"') - props = node_props[name] - node.set_fontsize(props["fontsize"]) - node.set_fontcolor(props["fontcolor"]) - node.set_shape(props["shape"]) - node.set_fontname("arial") - node.set_fixedsize("shape" if props["fixed"] else False) - node.set_width(props["size"]) - node.set_height(props["size"]) - - # Handle collapsed nodes specially - node_data = graph.nodes.get(f'"{name}"', {}) - if node_data.get("collapsed"): - table_count = node_data.get("table_count", 0) - label = f"({table_count} tables)" if table_count != 1 else "(1 table)" - node.set_label(label) - node.set_tooltip(f"Collapsed schema: {table_count} tables") + label = module_name + else: + label = schema_name + collapsed_labels[schema_name] = label + + # Build counts using final labels + collapsed_counts = {} # label -> count of tables + for schema_name, class_names in collapsed_by_schema.items(): + label = collapsed_labels[schema_name] + collapsed_counts[label] = len(class_names) + + # Create new graph with collapsed nodes + new_graph = nx.DiGraph() + + # Map old node names to new names (collapsed nodes -> schema label) + node_mapping = {} + for node in graph.nodes(): + full_name = class_to_full.get(node) + if full_name: + parts = full_name.replace('"', "`").split("`") + if len(parts) >= 2 and node in nodes_to_collapse: + schema_name = parts[1] + node_mapping[node] = collapsed_labels[schema_name] else: - cls = self._resolve_class(name) - if cls is not None: - description = cls().describe(context=self.context).split("\n") - description = ( - ( - "-" * 30 - if q.startswith("---") - else (q.replace("->", "→") if "->" in q else q.split(":")[0]) - ) - for q in description - if not q.startswith("#") - ) - node.set_tooltip(" ".join(description)) - # Strip module prefix from label if it matches the cluster label - display_name = name - schema_name = schema_map.get(name) - if schema_name and "." in name: - cluster_label = cluster_labels.get(schema_name) - if cluster_label and name.startswith(cluster_label + "."): - display_name = name[len(cluster_label) + 1 :] - node.set_label("<" + display_name + ">" if node.get("distinguished") == "True" else display_name) - node.set_color(props["color"]) - node.set_style("filled") - - for edge in dot.get_edges(): - # see https://graphviz.org/doc/info/attrs.html - src = edge.get_source() - dest = edge.get_destination() - props = graph.get_edge_data(src, dest) - if props is None: - raise DataJointError("Could not find edge with source '{}' and destination '{}'".format(src, dest)) - edge.set_color("#00000040") - edge.set_style("solid" if props.get("primary") else "dashed") - dest_node_type = graph.nodes[dest].get("node_type") - master_part = dest_node_type is Part and dest.startswith(src + ".") - edge.set_weight(3 if master_part else 1) - edge.set_arrowhead("none") - edge.set_penwidth(0.75 if props.get("multi") else 2) - - # Group nodes into schema clusters (always on) - if schema_map: - import pydot - - # Group nodes by schema - schemas = {} - for node in list(dot.get_nodes()): - name = node.get_name().strip('"') - schema_name = schema_map.get(name) - if schema_name: - if schema_name not in schemas: - schemas[schema_name] = [] - schemas[schema_name].append(node) - - # Create clusters for each schema - # Use Python module name if 1:1 mapping, otherwise database schema name - for schema_name, nodes in schemas.items(): - label = cluster_labels.get(schema_name, schema_name) - cluster = pydot.Cluster( - f"cluster_{schema_name}", - label=label, - style="dashed", - color="gray", - fontcolor="gray", + node_mapping[node] = node + else: + # Alias nodes - check if they should be collapsed + # An alias node should be collapsed if ALL its neighbors are collapsed + neighbors = set(graph.predecessors(node)) | set(graph.successors(node)) + if neighbors and neighbors <= nodes_to_collapse: + # Get schema from first neighbor + neighbor = next(iter(neighbors)) + full_name = class_to_full.get(neighbor) + if full_name: + parts = full_name.replace('"', "`").split("`") + if len(parts) >= 2: + schema_name = parts[1] + node_mapping[node] = collapsed_labels[schema_name] + continue + node_mapping[node] = node + + # Build reverse mapping: label -> schema_name + label_to_schema = {label: schema for schema, label in collapsed_labels.items()} + + # Add nodes + added_collapsed = set() + for old_node, new_node in node_mapping.items(): + if new_node in collapsed_counts: + # This is a collapsed schema node + if new_node not in added_collapsed: + schema_name = label_to_schema.get(new_node, new_node) + new_graph.add_node( + new_node, + node_type=None, + collapsed=True, + table_count=collapsed_counts[new_node], + schema_name=schema_name, ) - for node in nodes: - cluster.add_node(node) - dot.add_subgraph(cluster) + added_collapsed.add(new_node) + else: + new_graph.add_node(new_node, **graph.nodes[old_node]) + + # Add edges (avoiding self-loops and duplicates) + for src, dest, data in graph.edges(data=True): + new_src = node_mapping[src] + new_dest = node_mapping[dest] + if new_src != new_dest and not new_graph.has_edge(new_src, new_dest): + new_graph.add_edge(new_src, new_dest, **data) + + return new_graph, collapsed_counts + + def _resolve_class(self, name: str): + """ + Safely resolve a table class from a dotted name without eval(). - return dot + Parameters + ---------- + name : str + Dotted class name like "MyTable" or "Module.MyTable". - def make_svg(self): - from IPython.display import SVG + Returns + ------- + type or None + The table class if found, otherwise None. + """ + parts = name.split(".") + obj = self.context.get(parts[0]) + for part in parts[1:]: + if obj is None: + return None + obj = getattr(obj, part, None) + if obj is not None and isinstance(obj, type) and issubclass(obj, Table): + return obj + return None + + @staticmethod + def _encapsulate_edge_attributes(graph: nx.DiGraph) -> None: + """ + Encapsulate edge attr_map in double quotes for pydot compatibility. - return SVG(self.make_dot().create_svg()) + Modifies graph in place. - def make_png(self): - return io.BytesIO(self.make_dot().create_png()) + See Also + -------- + https://github.com/pydot/pydot/issues/258#issuecomment-795798099 + """ + for u, v, *_, edgedata in graph.edges(data=True): + if "attr_map" in edgedata: + graph.edges[u, v]["attr_map"] = '"{0}"'.format(edgedata["attr_map"]) - def make_image(self): - if plot_active: - return plt.imread(self.make_png()) - else: - raise DataJointError("pyplot was not imported") - - def make_mermaid(self) -> str: - """ - Generate Mermaid diagram syntax. - - Produces a flowchart in Mermaid syntax that can be rendered in - Markdown documentation, GitHub, or https://mermaid.live. - - Returns - ------- - str - Mermaid flowchart syntax. - - Notes - ----- - Layout direction is controlled via ``dj.config.display.diagram_direction``. - Tables are grouped by schema using Mermaid subgraphs, with the Python - module name shown as the group label when available. - - Examples - -------- - >>> print(dj.Diagram(schema).make_mermaid()) - flowchart TB - subgraph my_pipeline - Mouse[Mouse]:::manual - Session[Session]:::manual - Neuron([Neuron]):::computed - end - Mouse --> Session - Session --> Neuron - """ - graph = self._make_graph() - direction = self._connection._config.display.diagram_direction - - # Apply collapse logic if needed - graph, collapsed_counts = self._apply_collapse(graph) - - # Build schema mapping for grouping - schema_map = {} # class_name -> schema_name - schema_modules = {} # schema_name -> set of module names - - for full_name in self.nodes_to_show: - parts = full_name.replace('"', "`").split("`") - if len(parts) >= 2: - schema_name = parts[1] - class_name = lookup_class_name(full_name, self.context) or full_name - schema_map[class_name] = schema_name + @staticmethod + def _encapsulate_node_names(graph: nx.DiGraph) -> None: + """ + Encapsulate node names in double quotes for pydot compatibility. - # Collect all module names for this schema - if schema_name not in schema_modules: - schema_modules[schema_name] = set() - cls = self._resolve_class(class_name) - if cls is not None and hasattr(cls, "__module__"): - module_name = cls.__module__.split(".")[-1] - schema_modules[schema_name].add(module_name) + Modifies graph in place. - # Determine cluster labels: use module name if 1:1, else database schema name - cluster_labels = {} - for schema_name, modules in schema_modules.items(): - if len(modules) == 1: - cluster_labels[schema_name] = next(iter(modules)) - else: - cluster_labels[schema_name] = schema_name - - # Assign alias nodes to the same schema as their child table - for node, data in graph.nodes(data=True): - if data.get("node_type") is _AliasNode: - successors = list(graph.successors(node)) - if successors and successors[0] in schema_map: - schema_map[node] = schema_map[successors[0]] - - lines = [f"flowchart {direction}"] - - # Define class styles matching Graphviz colors - lines.append(" classDef manual fill:#90EE90,stroke:#006400") - lines.append(" classDef lookup fill:#D3D3D3,stroke:#696969") - lines.append(" classDef computed fill:#FFB6C1,stroke:#8B0000") - lines.append(" classDef imported fill:#ADD8E6,stroke:#00008B") - lines.append(" classDef part fill:#FFFFFF,stroke:#000000") - lines.append(" classDef collapsed fill:#808080,stroke:#404040") - lines.append("") - - # Shape mapping: Manual=box, Computed/Imported=stadium, Lookup/Part=box - shape_map = { - Manual: ("[", "]"), # box - Lookup: ("[", "]"), # box - Computed: ("([", "])"), # stadium/pill - Imported: ("([", "])"), # stadium/pill - Part: ("[", "]"), # box - _AliasNode: ("((", "))"), # circle - None: ("((", "))"), # circle - } - - tier_class = { - Manual: "manual", - Lookup: "lookup", - Computed: "computed", - Imported: "imported", - Part: "part", - _AliasNode: "", - None: "", - } - - # Group nodes by schema into subgraphs (including collapsed nodes) + See Also + -------- + https://github.com/datajoint/datajoint-python/pull/1176 + """ + nx.relabel_nodes( + graph, + {node: '"{0}"'.format(node) for node in graph.nodes()}, + copy=False, + ) + + def make_dot(self): + """ + Generate a pydot graph object. + + Returns + ------- + pydot.Dot + The graph object ready for rendering. + + Raises + ------ + DataJointError + If pygraphviz/pydot is not installed. + + Notes + ----- + Layout direction is controlled via ``dj.config.display.diagram_direction``. + Tables are grouped by schema, with the Python module name shown as the + group label when available. + """ + if not diagram_active: + raise DataJointError("Install pygraphviz and pydot libraries to enable diagram visualization.") + direction = self._connection._config.display.diagram_direction + graph = self._make_graph() + + # Apply collapse logic if needed + graph, collapsed_counts = self._apply_collapse(graph) + + # Build schema mapping: class_name -> schema_name + # Group by database schema, label with Python module name if 1:1 mapping + schema_map = {} # class_name -> schema_name + schema_modules = {} # schema_name -> set of module names + + for full_name in self.nodes_to_show: + # Extract schema from full table name like `schema`.`table` or "schema"."table" + parts = full_name.replace('"', "`").split("`") + if len(parts) >= 2: + schema_name = parts[1] # schema is between first pair of backticks + class_name = lookup_class_name(full_name, self.context) or full_name + schema_map[class_name] = schema_name + + # Collect all module names for this schema + if schema_name not in schema_modules: + schema_modules[schema_name] = set() + cls = self._resolve_class(class_name) + if cls is not None and hasattr(cls, "__module__"): + module_name = cls.__module__.split(".")[-1] + schema_modules[schema_name].add(module_name) + + # Determine cluster labels: use module name if 1:1, else database schema name + cluster_labels = {} # schema_name -> label + for schema_name, modules in schema_modules.items(): + if len(modules) == 1: + cluster_labels[schema_name] = next(iter(modules)) + else: + cluster_labels[schema_name] = schema_name + + # Disambiguate labels if multiple schemas share the same module name + # (e.g., all defined in __main__ in a notebook) + label_counts = {} + for label in cluster_labels.values(): + label_counts[label] = label_counts.get(label, 0) + 1 + + for schema_name, label in cluster_labels.items(): + if label_counts[label] > 1: + # Multiple schemas share this module name - add schema name + cluster_labels[schema_name] = f"{label} ({schema_name})" + + # Assign alias nodes (orange dots) to the same schema as their child table + for node, data in graph.nodes(data=True): + if data.get("node_type") is _AliasNode: + # Find the child (successor) - the table that declares the renamed FK + successors = list(graph.successors(node)) + if successors and successors[0] in schema_map: + schema_map[node] = schema_map[successors[0]] + + # Assign collapsed nodes to their schema so they appear in the cluster + for node, data in graph.nodes(data=True): + if data.get("collapsed") and data.get("schema_name"): + schema_map[node] = data["schema_name"] + + scale = 1.2 # scaling factor for fonts and boxes + label_props = { # http://matplotlib.org/examples/color/named_colors.html + None: dict( + shape="circle", + color="#FFFF0040", + fontcolor="yellow", + fontsize=round(scale * 8), + size=0.4 * scale, + fixed=False, + ), + _AliasNode: dict( + shape="circle", + color="#FF880080", + fontcolor="#FF880080", + fontsize=round(scale * 0), + size=0.05 * scale, + fixed=True, + ), + Manual: dict( + shape="box", + color="#00FF0030", + fontcolor="darkgreen", + fontsize=round(scale * 10), + size=0.4 * scale, + fixed=False, + ), + Lookup: dict( + shape="plaintext", + color="#00000020", + fontcolor="black", + fontsize=round(scale * 8), + size=0.4 * scale, + fixed=False, + ), + Computed: dict( + shape="ellipse", + color="#FF000020", + fontcolor="#7F0000A0", + fontsize=round(scale * 10), + size=0.4 * scale, + fixed=False, + ), + Imported: dict( + shape="ellipse", + color="#00007F40", + fontcolor="#00007FA0", + fontsize=round(scale * 10), + size=0.4 * scale, + fixed=False, + ), + Part: dict( + shape="plaintext", + color="#00000000", + fontcolor="black", + fontsize=round(scale * 8), + size=0.1 * scale, + fixed=False, + ), + "collapsed": dict( + shape="box3d", + color="#80808060", + fontcolor="#404040", + fontsize=round(scale * 10), + size=0.5 * scale, + fixed=False, + ), + } + # Build node_props, handling collapsed nodes specially + node_props = {} + for node, d in graph.nodes(data=True): + if d.get("collapsed"): + node_props[node] = label_props["collapsed"] + else: + node_props[node] = label_props[d["node_type"]] + + self._encapsulate_node_names(graph) + self._encapsulate_edge_attributes(graph) + dot = nx.drawing.nx_pydot.to_pydot(graph) + dot.set_rankdir(direction) + for node in dot.get_nodes(): + node.set_shape("circle") + name = node.get_name().strip('"') + props = node_props[name] + node.set_fontsize(props["fontsize"]) + node.set_fontcolor(props["fontcolor"]) + node.set_shape(props["shape"]) + node.set_fontname("arial") + node.set_fixedsize("shape" if props["fixed"] else False) + node.set_width(props["size"]) + node.set_height(props["size"]) + + # Handle collapsed nodes specially + node_data = graph.nodes.get(f'"{name}"', {}) + if node_data.get("collapsed"): + table_count = node_data.get("table_count", 0) + label = f"({table_count} tables)" if table_count != 1 else "(1 table)" + node.set_label(label) + node.set_tooltip(f"Collapsed schema: {table_count} tables") + else: + cls = self._resolve_class(name) + if cls is not None: + description = cls().describe(context=self.context).split("\n") + description = ( + ("-" * 30 if q.startswith("---") else (q.replace("->", "→") if "->" in q else q.split(":")[0])) + for q in description + if not q.startswith("#") + ) + node.set_tooltip(" ".join(description)) + # Strip module prefix from label if it matches the cluster label + display_name = name + schema_name = schema_map.get(name) + if schema_name and "." in name: + cluster_label = cluster_labels.get(schema_name) + if cluster_label and name.startswith(cluster_label + "."): + display_name = name[len(cluster_label) + 1 :] + node.set_label("<" + display_name + ">" if node.get("distinguished") == "True" else display_name) + node.set_color(props["color"]) + node.set_style("filled") + + for edge in dot.get_edges(): + # see https://graphviz.org/doc/info/attrs.html + src = edge.get_source() + dest = edge.get_destination() + props = graph.get_edge_data(src, dest) + if props is None: + raise DataJointError("Could not find edge with source '{}' and destination '{}'".format(src, dest)) + edge.set_color("#00000040") + edge.set_style("solid" if props.get("primary") else "dashed") + dest_node_type = graph.nodes[dest].get("node_type") + master_part = dest_node_type is Part and dest.startswith(src + ".") + edge.set_weight(3 if master_part else 1) + edge.set_arrowhead("none") + edge.set_penwidth(0.75 if props.get("multi") else 2) + + # Group nodes into schema clusters (always on) + if schema_map: + import pydot + + # Group nodes by schema schemas = {} - for node, data in graph.nodes(data=True): - if data.get("collapsed"): - # Collapsed nodes use their schema_name attribute - schema_name = data.get("schema_name") - else: - schema_name = schema_map.get(node) + for node in list(dot.get_nodes()): + name = node.get_name().strip('"') + schema_name = schema_map.get(name) if schema_name: if schema_name not in schemas: schemas[schema_name] = [] - schemas[schema_name].append((node, data)) + schemas[schema_name].append(node) - # Add nodes grouped by schema subgraphs + # Create clusters for each schema + # Use Python module name if 1:1 mapping, otherwise database schema name for schema_name, nodes in schemas.items(): label = cluster_labels.get(schema_name, schema_name) - lines.append(f" subgraph {label}") - for node, data in nodes: - safe_id = node.replace(".", "_").replace(" ", "_") - if data.get("collapsed"): - # Collapsed node - show only table count - table_count = data.get("table_count", 0) - count_text = f"{table_count} tables" if table_count != 1 else "1 table" - lines.append(f' {safe_id}[["({count_text})"]]:::collapsed') - else: - # Regular node - tier = data.get("node_type") - left, right = shape_map.get(tier, ("[", "]")) - cls = tier_class.get(tier, "") - # Strip module prefix from display name if it matches the cluster label - display_name = node - if "." in node and node.startswith(label + "."): - display_name = node[len(label) + 1 :] - class_suffix = f":::{cls}" if cls else "" - lines.append(f" {safe_id}{left}{display_name}{right}{class_suffix}") - lines.append(" end") - - lines.append("") - - # Add edges - for src, dest, data in graph.edges(data=True): - safe_src = src.replace(".", "_").replace(" ", "_") - safe_dest = dest.replace(".", "_").replace(" ", "_") - # Solid arrow for primary FK, dotted for non-primary - style = "-->" if data.get("primary") else "-.->" - lines.append(f" {safe_src} {style} {safe_dest}") - - return "\n".join(lines) - - def _repr_svg_(self): - return self.make_svg()._repr_svg_() - - def draw(self): - if plot_active: - plt.imshow(self.make_image()) - plt.gca().axis("off") - plt.show() + cluster = pydot.Cluster( + f"cluster_{schema_name}", + label=label, + style="dashed", + color="gray", + fontcolor="gray", + ) + for node in nodes: + cluster.add_node(node) + dot.add_subgraph(cluster) + + return dot + + def make_svg(self): + from IPython.display import SVG + + return SVG(self.make_dot().create_svg()) + + def make_png(self): + return io.BytesIO(self.make_dot().create_png()) + + def make_image(self): + if plot_active: + return plt.imread(self.make_png()) + else: + raise DataJointError("pyplot was not imported") + + def make_mermaid(self) -> str: + """ + Generate Mermaid diagram syntax. + + Produces a flowchart in Mermaid syntax that can be rendered in + Markdown documentation, GitHub, or https://mermaid.live. + + Returns + ------- + str + Mermaid flowchart syntax. + + Notes + ----- + Layout direction is controlled via ``dj.config.display.diagram_direction``. + Tables are grouped by schema using Mermaid subgraphs, with the Python + module name shown as the group label when available. + + Examples + -------- + >>> print(dj.Diagram(schema).make_mermaid()) + flowchart TB + subgraph my_pipeline + Mouse[Mouse]:::manual + Session[Session]:::manual + Neuron([Neuron]):::computed + end + Mouse --> Session + Session --> Neuron + """ + graph = self._make_graph() + direction = self._connection._config.display.diagram_direction + + # Apply collapse logic if needed + graph, collapsed_counts = self._apply_collapse(graph) + + # Build schema mapping for grouping + schema_map = {} # class_name -> schema_name + schema_modules = {} # schema_name -> set of module names + + for full_name in self.nodes_to_show: + parts = full_name.replace('"', "`").split("`") + if len(parts) >= 2: + schema_name = parts[1] + class_name = lookup_class_name(full_name, self.context) or full_name + schema_map[class_name] = schema_name + + # Collect all module names for this schema + if schema_name not in schema_modules: + schema_modules[schema_name] = set() + cls = self._resolve_class(class_name) + if cls is not None and hasattr(cls, "__module__"): + module_name = cls.__module__.split(".")[-1] + schema_modules[schema_name].add(module_name) + + # Determine cluster labels: use module name if 1:1, else database schema name + cluster_labels = {} + for schema_name, modules in schema_modules.items(): + if len(modules) == 1: + cluster_labels[schema_name] = next(iter(modules)) else: - raise DataJointError("pyplot was not imported") - - def save(self, filename: str, format: str | None = None) -> None: - """ - Save diagram to file. - - Parameters - ---------- - filename : str - Output filename. - format : str, optional - File format (``'png'``, ``'svg'``, or ``'mermaid'``). - Inferred from extension if None. - - Raises - ------ - DataJointError - If format is unsupported. - - Notes - ----- - Layout direction is controlled via ``dj.config.display.diagram_direction``. - Tables are grouped by schema, with the Python module name shown as the - group label when available. - """ - if format is None: - if filename.lower().endswith(".png"): - format = "png" - elif filename.lower().endswith(".svg"): - format = "svg" - elif filename.lower().endswith((".mmd", ".mermaid")): - format = "mermaid" - if format is None: - raise DataJointError("Could not infer format from filename. Specify format explicitly.") - if format.lower() == "png": - with open(filename, "wb") as f: - f.write(self.make_png().getbuffer().tobytes()) - elif format.lower() == "svg": - with open(filename, "w") as f: - f.write(self.make_svg().data) - elif format.lower() == "mermaid": - with open(filename, "w") as f: - f.write(self.make_mermaid()) + cluster_labels[schema_name] = schema_name + + # Assign alias nodes to the same schema as their child table + for node, data in graph.nodes(data=True): + if data.get("node_type") is _AliasNode: + successors = list(graph.successors(node)) + if successors and successors[0] in schema_map: + schema_map[node] = schema_map[successors[0]] + + lines = [f"flowchart {direction}"] + + # Define class styles matching Graphviz colors + lines.append(" classDef manual fill:#90EE90,stroke:#006400") + lines.append(" classDef lookup fill:#D3D3D3,stroke:#696969") + lines.append(" classDef computed fill:#FFB6C1,stroke:#8B0000") + lines.append(" classDef imported fill:#ADD8E6,stroke:#00008B") + lines.append(" classDef part fill:#FFFFFF,stroke:#000000") + lines.append(" classDef collapsed fill:#808080,stroke:#404040") + lines.append("") + + # Shape mapping: Manual=box, Computed/Imported=stadium, Lookup/Part=box + shape_map = { + Manual: ("[", "]"), # box + Lookup: ("[", "]"), # box + Computed: ("([", "])"), # stadium/pill + Imported: ("([", "])"), # stadium/pill + Part: ("[", "]"), # box + _AliasNode: ("((", "))"), # circle + None: ("((", "))"), # circle + } + + tier_class = { + Manual: "manual", + Lookup: "lookup", + Computed: "computed", + Imported: "imported", + Part: "part", + _AliasNode: "", + None: "", + } + + # Group nodes by schema into subgraphs (including collapsed nodes) + schemas = {} + for node, data in graph.nodes(data=True): + if data.get("collapsed"): + # Collapsed nodes use their schema_name attribute + schema_name = data.get("schema_name") else: - raise DataJointError("Unsupported file format") + schema_name = schema_map.get(node) + if schema_name: + if schema_name not in schemas: + schemas[schema_name] = [] + schemas[schema_name].append((node, data)) + + # Add nodes grouped by schema subgraphs + for schema_name, nodes in schemas.items(): + label = cluster_labels.get(schema_name, schema_name) + lines.append(f" subgraph {label}") + for node, data in nodes: + safe_id = node.replace(".", "_").replace(" ", "_") + if data.get("collapsed"): + # Collapsed node - show only table count + table_count = data.get("table_count", 0) + count_text = f"{table_count} tables" if table_count != 1 else "1 table" + lines.append(f' {safe_id}[["({count_text})"]]:::collapsed') + else: + # Regular node + tier = data.get("node_type") + left, right = shape_map.get(tier, ("[", "]")) + cls = tier_class.get(tier, "") + # Strip module prefix from display name if it matches the cluster label + display_name = node + if "." in node and node.startswith(label + "."): + display_name = node[len(label) + 1 :] + class_suffix = f":::{cls}" if cls else "" + lines.append(f" {safe_id}{left}{display_name}{right}{class_suffix}") + lines.append(" end") + + lines.append("") + + # Add edges + for src, dest, data in graph.edges(data=True): + safe_src = src.replace(".", "_").replace(" ", "_") + safe_dest = dest.replace(".", "_").replace(" ", "_") + # Solid arrow for primary FK, dotted for non-primary + style = "-->" if data.get("primary") else "-.->" + lines.append(f" {safe_src} {style} {safe_dest}") + + return "\n".join(lines) + + def _repr_svg_(self): + return self.make_svg()._repr_svg_() + + def draw(self): + if plot_active: + plt.imshow(self.make_image()) + plt.gca().axis("off") + plt.show() + else: + raise DataJointError("pyplot was not imported") + + def save(self, filename: str, format: str | None = None) -> None: + """ + Save diagram to file. - @staticmethod - def _layout(graph, **kwargs): - return pydot_layout(graph, prog="dot", **kwargs) + Parameters + ---------- + filename : str + Output filename. + format : str, optional + File format (``'png'``, ``'svg'``, or ``'mermaid'``). + Inferred from extension if None. + + Raises + ------ + DataJointError + If format is unsupported. + + Notes + ----- + Layout direction is controlled via ``dj.config.display.diagram_direction``. + Tables are grouped by schema, with the Python module name shown as the + group label when available. + """ + if format is None: + if filename.lower().endswith(".png"): + format = "png" + elif filename.lower().endswith(".svg"): + format = "svg" + elif filename.lower().endswith((".mmd", ".mermaid")): + format = "mermaid" + if format is None: + raise DataJointError("Could not infer format from filename. Specify format explicitly.") + if format.lower() == "png": + with open(filename, "wb") as f: + f.write(self.make_png().getbuffer().tobytes()) + elif format.lower() == "svg": + with open(filename, "w") as f: + f.write(self.make_svg().data) + elif format.lower() == "mermaid": + with open(filename, "w") as f: + f.write(self.make_mermaid()) + else: + raise DataJointError("Unsupported file format") + + @staticmethod + def _layout(graph, **kwargs): + return pydot_layout(graph, prog="dot", **kwargs) diff --git a/src/datajoint/hash_registry.py b/src/datajoint/hash_registry.py index 331c836cd..00ed35386 100644 --- a/src/datajoint/hash_registry.py +++ b/src/datajoint/hash_registry.py @@ -130,7 +130,7 @@ def build_hash_path( return f"_hash/{schema_name}/{content_hash}" -def get_store_backend(store_name: str | None = None, config=None) -> StorageBackend: +def get_store_backend(store_name: str | None = None, config: Any = None) -> StorageBackend: """ Get a StorageBackend for hash-addressed storage. @@ -153,7 +153,7 @@ def get_store_backend(store_name: str | None = None, config=None) -> StorageBack return StorageBackend(spec) -def get_store_subfolding(store_name: str | None = None, config=None) -> tuple[int, ...] | None: +def get_store_subfolding(store_name: str | None = None, config: Any = None) -> tuple[int, ...] | None: """ Get the subfolding configuration for a store. @@ -182,7 +182,7 @@ def put_hash( data: bytes, schema_name: str, store_name: str | None = None, - config=None, + config: Any = None, ) -> dict[str, Any]: """ Store content using hash-addressed storage. @@ -231,7 +231,7 @@ def put_hash( } -def get_hash(metadata: dict[str, Any], config=None) -> bytes: +def get_hash(metadata: dict[str, Any], config: Any = None) -> bytes: """ Retrieve content using stored metadata. @@ -275,7 +275,7 @@ def get_hash(metadata: dict[str, Any], config=None) -> bytes: def delete_path( path: str, store_name: str | None = None, - config=None, + config: Any = None, ) -> bool: """ Delete content at the specified path from storage. diff --git a/src/datajoint/table.py b/src/datajoint/table.py index 256fab6e9..6907cc7c4 100644 --- a/src/datajoint/table.py +++ b/src/datajoint/table.py @@ -18,13 +18,12 @@ AccessError, DataJointError, DuplicateError, - IntegrityError, UnknownAttributeError, ) from .expression import QueryExpression from .heading import Heading from .staged_insert import staged_insert1 as _staged_insert1 -from .utils import get_master, is_camel_case, user_choice +from .utils import is_camel_case, user_choice logger = logging.getLogger(__name__.split(".")[0]) @@ -974,10 +973,15 @@ def delete( transaction: bool = True, prompt: bool | None = None, part_integrity: str = "enforce", - ) -> int: + dry_run: bool = False, + ) -> int | dict[str, int]: """ Deletes the contents of the table and its dependent tables, recursively. + Uses graph-driven cascade: builds a dependency diagram, propagates + restrictions to all descendants, then deletes in reverse topological + order (leaves first). + Args: transaction: If `True`, use of the entire delete becomes an atomic transaction. This is the default and recommended behavior. Set to `False` if this delete is @@ -988,187 +992,25 @@ def delete( - ``"enforce"`` (default): Error if parts would be deleted without masters. - ``"ignore"``: Allow deleting parts without masters (breaks integrity). - ``"cascade"``: Also delete masters when parts are deleted (maintains integrity). + dry_run: If `True`, return a dict mapping full table names to affected + row counts without deleting any data. Default False. Returns: - Number of deleted rows (excluding those from dependent tables). + Number of deleted rows (excluding those from dependent tables), or + (if ``dry_run``) a dict mapping full table name to affected row count. Raises: - DataJointError: Delete exceeds maximum number of delete attempts. DataJointError: When deleting within an existing transaction. DataJointError: Deleting a part table before its master (when part_integrity="enforce"). ValueError: Invalid part_integrity value. """ if part_integrity not in ("enforce", "ignore", "cascade"): - raise ValueError(f"part_integrity must be 'enforce', 'ignore', or 'cascade', got {part_integrity!r}") - deleted = set() - visited_masters = set() - - def cascade(table): - """service function to perform cascading deletes recursively.""" - max_attempts = 50 - for _ in range(max_attempts): - # Set savepoint before delete attempt (for PostgreSQL transaction handling) - savepoint_name = f"cascade_delete_{id(table)}" - if transaction: - table.connection.query(f"SAVEPOINT {savepoint_name}") - - try: - delete_count = table.delete_quick(get_count=True) - except IntegrityError as error: - # Rollback to savepoint so we can continue querying (PostgreSQL requirement) - if transaction: - table.connection.query(f"ROLLBACK TO SAVEPOINT {savepoint_name}") - # Use adapter to parse FK error message - match = table.connection.adapter.parse_foreign_key_error(error.args[0]) - if match is None: - raise DataJointError( - "Cascading deletes failed because the error message is missing foreign key information. " - "Make sure you have REFERENCES privilege to all dependent tables." - ) from None - - # Strip quotes from parsed values for backend-agnostic processing - quote_chars = ("`", '"') - - def strip_quotes(s): - if s and any(s.startswith(q) for q in quote_chars): - return s.strip('`"') - return s - - # Extract schema and table name from child (work with unquoted names) - child_table_raw = strip_quotes(match["child"]) - if "." in child_table_raw: - child_parts = child_table_raw.split(".") - child_schema = strip_quotes(child_parts[0]) - child_table_name = strip_quotes(child_parts[1]) - else: - # Add schema from current table - schema_parts = table.full_table_name.split(".") - child_schema = strip_quotes(schema_parts[0]) - child_table_name = child_table_raw - - # If FK/PK attributes not in error message, query information_schema - if match["fk_attrs"] is None or match["pk_attrs"] is None: - constraint_query = table.connection.adapter.get_constraint_info_sql( - strip_quotes(match["name"]), - child_schema, - child_table_name, - ) - - results = table.connection.query( - constraint_query, - args=(strip_quotes(match["name"]), child_schema, child_table_name), - ).fetchall() - if results: - match["fk_attrs"], match["parent"], match["pk_attrs"] = list(map(list, zip(*results))) - match["parent"] = match["parent"][0] # All rows have same parent - - # Build properly quoted full table name for FreeTable - child_full_name = ( - f"{table.connection.adapter.quote_identifier(child_schema)}." - f"{table.connection.adapter.quote_identifier(child_table_name)}" - ) - - # Restrict child by table if - # 1. if table's restriction attributes are not in child's primary key - # 2. if child renames any attributes - # Otherwise restrict child by table's restriction. - child = FreeTable(table.connection, child_full_name) - if set(table.restriction_attributes) <= set(child.primary_key) and match["fk_attrs"] == match["pk_attrs"]: - child._restriction = table._restriction - child._restriction_attributes = table.restriction_attributes - elif match["fk_attrs"] != match["pk_attrs"]: - child &= table.proj(**dict(zip(match["fk_attrs"], match["pk_attrs"]))) - else: - child &= table.proj() - - master_name = get_master(child.full_table_name, table.connection.adapter) - if ( - part_integrity == "cascade" - and master_name - and master_name != table.full_table_name - and master_name not in visited_masters - ): - master = FreeTable(table.connection, master_name) - master._restriction_attributes = set() - master._restriction = [ - make_condition( # &= may cause in target tables in subquery - master, - (master.proj() & child.proj()).to_arrays(), - master._restriction_attributes, - ) - ] - visited_masters.add(master_name) - cascade(master) - else: - cascade(child) - else: - # Successful delete - release savepoint - if transaction: - table.connection.query(f"RELEASE SAVEPOINT {savepoint_name}") - deleted.add(table.full_table_name) - logger.info("Deleting {count} rows from {table}".format(count=delete_count, table=table.full_table_name)) - break - else: - raise DataJointError("Exceeded maximum number of delete attempts.") - return delete_count - - prompt = self.connection._config["safemode"] if prompt is None else prompt - - # Start transaction - if transaction: - if not self.connection.in_transaction: - self.connection.start_transaction() - else: - if not prompt: - transaction = False - else: - raise DataJointError( - "Delete cannot use a transaction within an ongoing transaction. Set transaction=False or prompt=False." - ) + raise ValueError(f"part_integrity must be 'enforce', 'ignore', or 'cascade', " f"got {part_integrity!r}") + from .diagram import Diagram - # Cascading delete - try: - delete_count = cascade(self) - except: - if transaction: - self.connection.cancel_transaction() - raise - - if part_integrity == "enforce": - # Avoid deleting from part before master (See issue #151) - for part in deleted: - master = get_master(part, self.connection.adapter) - if master and master not in deleted: - if transaction: - self.connection.cancel_transaction() - raise DataJointError( - "Attempt to delete part table {part} before deleting from its master {master} first. " - "Use part_integrity='ignore' to allow, or part_integrity='cascade' to also delete master.".format( - part=part, master=master - ) - ) - - # Confirm and commit - if delete_count == 0: - if prompt: - logger.warning("Nothing to delete.") - if transaction: - self.connection.cancel_transaction() - elif not transaction: - logger.info("Delete completed") - else: - if not prompt or user_choice("Commit deletes?", default="no") == "yes": - if transaction: - self.connection.commit_transaction() - if prompt: - logger.info("Delete committed.") - else: - if transaction: - self.connection.cancel_transaction() - if prompt: - logger.warning("Delete cancelled") - delete_count = 0 # Reset count when delete is cancelled - return delete_count + diagram = Diagram._from_table(self) + diagram = diagram.cascade(self, part_integrity=part_integrity) + return diagram.delete(transaction=transaction, prompt=prompt, dry_run=dry_run) def drop_quick(self): """ @@ -1208,42 +1050,34 @@ def drop_quick(self): else: logger.info("Nothing to drop: table %s is not declared" % self.full_table_name) - def drop(self, prompt: bool | None = None): + def drop(self, prompt: bool | None = None, part_integrity: str = "enforce", dry_run: bool = False): """ Drop the table and all tables that reference it, recursively. + Uses graph-driven traversal: builds a dependency diagram and drops + in reverse topological order (leaves first). + Args: prompt: If `True`, show what will be dropped and ask for confirmation. If `False`, drop without confirmation. Default is `dj.config['safemode']`. + part_integrity: Policy for master-part integrity. One of: + - ``"enforce"`` (default): Error if parts would be dropped without masters. + - ``"ignore"``: Allow dropping parts without masters. + dry_run: If `True`, return a dict mapping full table names to row + counts without dropping any tables. Default False. + + Returns: + dict[str, int] or None: If ``dry_run``, mapping of full table name + to row count. Otherwise None. """ if self.restriction: raise DataJointError( - "A table with an applied restriction cannot be dropped. Call drop() on the unrestricted Table." + "A table with an applied restriction cannot be dropped. " "Call drop() on the unrestricted Table." ) - prompt = self.connection._config["safemode"] if prompt is None else prompt - - self.connection.dependencies.load() - do_drop = True - tables = [table for table in self.connection.dependencies.descendants(self.full_table_name) if not table.isdigit()] - - # avoid dropping part tables without their masters: See issue #374 - for part in tables: - master = get_master(part, self.connection.adapter) - if master and master not in tables: - raise DataJointError( - "Attempt to drop part table {part} before dropping its master. Drop {master} first.".format( - part=part, master=master - ) - ) + from .diagram import Diagram - if prompt: - for table in tables: - logger.info(table + " (%d tuples)" % len(FreeTable(self.connection, table))) - do_drop = user_choice("Proceed?", default="no") == "yes" - if do_drop: - for table in reversed(tables): - FreeTable(self.connection, table).drop_quick() - logger.info("Tables dropped. Restart kernel.") + diagram = Diagram._from_table(self) + return diagram.drop(prompt=prompt, part_integrity=part_integrity, dry_run=dry_run) def describe(self, context=None, printout=False): """ diff --git a/src/datajoint/user_tables.py b/src/datajoint/user_tables.py index 4c2ba8d4c..ced5f4c25 100644 --- a/src/datajoint/user_tables.py +++ b/src/datajoint/user_tables.py @@ -239,7 +239,7 @@ def delete(self, part_integrity: str = "enforce", **kwargs): ) super().delete(part_integrity=part_integrity, **kwargs) - def drop(self, part_integrity: str = "enforce"): + def drop(self, part_integrity: str = "enforce", dry_run: bool = False): """ Drop a Part table. @@ -248,12 +248,13 @@ def drop(self, part_integrity: str = "enforce"): - ``"enforce"`` (default): Error - drop master instead. - ``"ignore"``: Allow direct drop (breaks master-part structure). Note: ``"cascade"`` is not supported for drop (too destructive). + dry_run: If `True`, return row counts without dropping. Default False. Raises: DataJointError: If part_integrity="enforce" (direct Part drops prohibited) """ if part_integrity == "ignore": - super().drop() + return super().drop(part_integrity="ignore", dry_run=dry_run) elif part_integrity == "enforce": raise DataJointError("Cannot drop a Part directly. Drop master instead, or use part_integrity='ignore' to force.") else: diff --git a/src/datajoint/version.py b/src/datajoint/version.py index 871a28cbb..9a1d4aff2 100644 --- a/src/datajoint/version.py +++ b/src/datajoint/version.py @@ -1,4 +1,4 @@ # version bump auto managed by Github Actions: # label_prs.yaml(prep), release.yaml(bump), post_release.yaml(edit) # manually set this version will be eventually overwritten by the above actions -__version__ = "2.1.1" +__version__ = "2.2.0.dev0" diff --git a/tests/integration/test_cascade_delete.py b/tests/integration/test_cascade_delete.py index caf5f331b..2964bb877 100644 --- a/tests/integration/test_cascade_delete.py +++ b/tests/integration/test_cascade_delete.py @@ -188,3 +188,86 @@ class Observation(dj.Manual): assert remaining_obs[0]["obs_id"] == 3 assert remaining_obs[0]["subject_id"] == 2 assert remaining_obs[0]["measurement"] == 15.3 + + +def test_delete_dry_run(schema_by_backend): + """dry_run=True returns affected row counts without deleting data.""" + + @schema_by_backend + class Parent(dj.Manual): + definition = """ + parent_id : int + --- + name : varchar(255) + """ + + @schema_by_backend + class Child(dj.Manual): + definition = """ + -> Parent + child_id : int + --- + data : varchar(255) + """ + + Parent.insert1((1, "P1")) + Parent.insert1((2, "P2")) + Child.insert1((1, 1, "C1-1")) + Child.insert1((1, 2, "C1-2")) + Child.insert1((2, 1, "C2-1")) + + # dry_run on restricted delete + counts = (Parent & {"parent_id": 1}).delete(dry_run=True) + + assert isinstance(counts, dict) + assert counts[Parent.full_table_name] == 1 + assert counts[Child.full_table_name] == 2 + + # Data must still be intact + assert len(Parent()) == 2 + assert len(Child()) == 3 + + # dry_run on unrestricted delete + counts_all = Parent.delete(dry_run=True) + assert counts_all[Parent.full_table_name] == 2 + assert counts_all[Child.full_table_name] == 3 + + # Still intact + assert len(Parent()) == 2 + assert len(Child()) == 3 + + +def test_drop_dry_run(schema_by_backend): + """dry_run=True returns row counts without dropping tables.""" + + @schema_by_backend + class Parent(dj.Manual): + definition = """ + parent_id : int + --- + name : varchar(255) + """ + + @schema_by_backend + class Child(dj.Manual): + definition = """ + -> Parent + child_id : int + --- + data : varchar(255) + """ + + Parent.insert1((1, "P1")) + Child.insert1((1, 1, "C1")) + + counts = Parent.drop(dry_run=True) + + assert isinstance(counts, dict) + assert counts[Parent.full_table_name] == 1 + assert counts[Child.full_table_name] == 1 + + # Tables must still exist and have data + assert Parent.is_declared + assert Child.is_declared + assert len(Parent()) == 1 + assert len(Child()) == 1 diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index 35230ea4e..1f8144f0f 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -3,6 +3,7 @@ """ import subprocess +import sys import pytest @@ -31,7 +32,7 @@ def test_cli_help(capsys): def test_cli_config(): process = subprocess.Popen( - ["dj"], + [sys.executable, "-m", "datajoint.cli"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -50,7 +51,7 @@ def test_cli_config(): def test_cli_args(): process = subprocess.Popen( - ["dj", "-u", "test_user", "-p", "test_pass", "--host", "test_host"], + [sys.executable, "-m", "datajoint.cli", "-u", "test_user", "-p", "test_pass", "--host", "test_host"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -82,7 +83,9 @@ class IJ(dj.Lookup): # Pass credentials via CLI args to avoid prompting for username process = subprocess.Popen( [ - "dj", + sys.executable, + "-m", + "datajoint.cli", "-u", db_creds_root["user"], "-p", diff --git a/tests/integration/test_erd.py b/tests/integration/test_erd.py index 95077da50..92a8ad682 100644 --- a/tests/integration/test_erd.py +++ b/tests/integration/test_erd.py @@ -1,6 +1,8 @@ +import pytest as _pytest + import datajoint as dj -from tests.schema_simple import LOCALS_SIMPLE, A, B, D, E, G, L +from tests.schema_simple import LOCALS_SIMPLE, A, B, D, E, G, L, Profile, Website def test_decorator(schema_simp): @@ -61,3 +63,105 @@ def test_part_table_parsing(schema_simp): graph = erd._make_graph() assert "OutfitLaunch" in graph.nodes() assert "OutfitLaunch.OutfitPiece" in graph.nodes() + + +# --- prune() tests --- + + +@_pytest.fixture +def schema_simp_pop(schema_simp): + """Populate the simple schema for prune tests.""" + Profile().delete() + Website().delete() + G().delete() + E().delete() + D().delete() + B().delete() + L().delete() + A().delete() + + A().insert(A.contents, skip_duplicates=True) + L().insert(L.contents, skip_duplicates=True) + B().populate() + D().populate() + E().populate() + G().populate() + yield schema_simp + + +def test_prune_unrestricted(schema_simp_pop): + """Prune on unrestricted diagram removes physically empty tables.""" + diag = dj.Diagram(schema_simp_pop, context=LOCALS_SIMPLE) + original_count = len(diag.nodes_to_show) + pruned = diag.prune() + + # Populated tables (A, L, B, B.C, D, E, E.F, G, etc.) should survive + for cls in (A, B, D, E, L): + assert cls.full_table_name in pruned.nodes_to_show, f"{cls.__name__} should not be pruned" + + # Empty tables like Profile should be removed + assert Profile.full_table_name not in pruned.nodes_to_show, "empty Profile should be pruned" + + # Pruned diagram should have fewer nodes + assert len(pruned.nodes_to_show) < original_count + + +def test_prune_after_restrict(schema_simp_pop): + """Prune after restrict removes tables with zero matching rows.""" + diag = dj.Diagram(schema_simp_pop, context=LOCALS_SIMPLE) + restricted = diag.restrict(A & "id_a=0") + counts = restricted.preview() + + pruned = restricted.prune() + pruned_counts = pruned.preview() + + # Every table in pruned preview should have > 0 rows + assert all(c > 0 for c in pruned_counts.values()), "pruned diagram should have no zero-count tables" + + # Tables with zero rows in the original preview should be gone + for table, count in counts.items(): + if count == 0: + assert table not in pruned._restrict_conditions, f"{table} had 0 rows but was not pruned" + + +def test_prune_after_cascade(schema_simp_pop): + """Prune after cascade removes tables with zero matching rows.""" + diag = dj.Diagram(schema_simp_pop, context=LOCALS_SIMPLE) + cascaded = diag.cascade(A & "id_a=0") + counts = cascaded.preview() + + pruned = cascaded.prune() + pruned_counts = pruned.preview() + + assert all(c > 0 for c in pruned_counts.values()) + + for table, count in counts.items(): + if count == 0: + assert table not in pruned._cascade_restrictions, f"{table} had 0 rows but was not pruned" + + +def test_prune_idempotent(schema_simp_pop): + """Pruning twice gives the same result.""" + diag = dj.Diagram(schema_simp_pop, context=LOCALS_SIMPLE) + restricted = diag.restrict(A & "id_a=0") + pruned_once = restricted.prune() + pruned_twice = pruned_once.prune() + + assert pruned_once.nodes_to_show == pruned_twice.nodes_to_show + assert set(pruned_once._restrict_conditions) == set(pruned_twice._restrict_conditions) + + +def test_prune_then_restrict(schema_simp_pop): + """Restrict can be called after prune.""" + diag = dj.Diagram(schema_simp_pop, context=LOCALS_SIMPLE) + pruned = diag.restrict(A & "id_a < 5").prune() + # Restrict again on the same seed table with a tighter condition + further = pruned.restrict(A & "id_a=0") + + # Should not raise; further restriction should narrow results + counts = further.preview() + assert all(c >= 0 for c in counts.values()) + # Tighter restriction should produce fewer or equal rows + pruned_counts = pruned.preview() + for table in counts: + assert counts[table] <= pruned_counts.get(table, 0)