diff --git a/README.md b/README.md
index 844f37f..9eb1bfb 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,3 @@
-from timdex_dataset_api import TIMDEXDataset
-
# timdex-dataset-api
Python library for interacting with a TIMDEX parquet dataset located remotely or in S3. This library is often abbreviated as "TDA".
@@ -11,9 +9,10 @@ Python library for interacting with a TIMDEX parquet dataset located remotely or
- To run unit tests: `make test`
- To lint the repo: `make lint`
-The library version number is set in [`timdex_dataset_api/__init__.py`](timdex_dataset_api/__init__.py), e.g.:
-```python
-__version__ = "2.1.0"
+The library version number is set in [`pyproject.toml`](pyproject.toml), e.g.:
+```toml
+[project]
+version = "5.0.0"
```
Updating the version number when making changes to the library will prompt applications that install it, when they have _their_ dependencies updated, to pickup the new version.
@@ -74,15 +73,6 @@ With the env var `MINIO_S3_ENDPOINT_URL` set, this library will configure `pyarr
## Usage
-Currently, the most common use cases are:
- * **Transmogrifier**: uses TDA to **write** to the parquet dataset
- * **TIMDEX-Index-Manager (TIM)**: uses TDA to **read** from the parquet dataset
-
-Beyond those two ETL run use cases, others are emerging where this library proves helpful:
-
- * yielding only the current version of all records in the dataset, useful for quickly re-indexing to Opensearch
- * high throughput (time) + memory safe (space) access to the dataset for analysis
-
For both reading and writing, the following env vars are recommended:
```shell
TDA_LOG_LEVEL=INFO
@@ -105,15 +95,19 @@ timdex_dataset = TIMDEXDataset("s3://my-bucket/path/to/dataset")
timdex_dataset = TIMDEXDataset("/path/to/dataset")
```
-All read methods for `TIMDEXDataset` allow for the same group of filters which are defined in `timdex_dataset_api.dataset.DatasetFilters`. Examples are shown below.
+Source-specific operations are available on composed objects such as
+`timdex_dataset.records` and `timdex_dataset.embeddings`.
+
+All read methods for `timdex_dataset.records` allow for the same group of filters.
+Examples are shown below.
```python
-# read a single row, no filtering
-single_record_dict = next(timdex_dataset.read_dicts_iter())
+# read a single record row, no filtering
+single_record_dict = next(timdex_dataset.records.read_dicts_iter())
# get batches of records, filtering to a particular run
-for batch in timdex_dataset.read_batches_iter(
+for batch in timdex_dataset.records.read_batches_iter(
source="alma",
run_date="2025-06-01",
run_id="abc123"
@@ -123,7 +117,7 @@ for batch in timdex_dataset.read_batches_iter(
# use convenience method to yield only transformed records
# NOTE: this is what TIM uses for indexing to Opensearch for a given ETL run
-for transformed_record in timdex_dataset.read_transformed_records_iter(
+for transformed_record in timdex_dataset.records.read_transformed_records_iter(
source="aspace",
run_date="2025-06-01",
run_id="ghi789"
@@ -133,7 +127,7 @@ for transformed_record in timdex_dataset.read_transformed_records_iter(
# load all records for a given run into a pandas dataframe
# NOTE: this can be potentially expensive memory-wise if the run is large
-run_df = timdex_dataset.read_dataframe(
+run_df = timdex_dataset.records.read_dataframe(
source="dspace",
run_date="2025-06-01",
run_id="def456"
@@ -146,7 +140,9 @@ See [docs/reading.md](docs/reading.md) for more information.
At this time, the only application that writes to the ETL parquet dataset is Transmogrifier.
-To write records to the dataset, you must prepare an iterator of `timdex_dataset_api.record.DatasetRecord`. Here is some pseudocode for how a dataset write can work:
+To write records to the dataset, you must prepare an iterator of
+`timdex_dataset_api.records.DatasetRecord`. Here is some pseudocode for how a
+record dataset write can work:
```python
from timdex_dataset_api import DatasetRecord, TIMDEXDataset
@@ -171,5 +167,5 @@ records_iter = records_to_write_iter()
# finally, perform the write, relying on the library to handle efficient batching
timdex_dataset = TIMDEXDataset("/path/to/dataset")
-timdex_dataset.write(records_iter=records_iter)
+timdex_dataset.records.write(records_iter)
```
\ No newline at end of file
diff --git a/docs/reading.md b/docs/reading.md
index f45f52f..e5597d3 100644
--- a/docs/reading.md
+++ b/docs/reading.md
@@ -1,28 +1,35 @@
# Reading data from TIMDEXDataset
-This guide explains how `TIMDEXDataset` read methods work and how to use them effectively.
+This guide explains how TIMDEXDataset data source read methods work and how to use them effectively.
-- `TIMDEXDataset` and `TIMDEXDatasetMetadata` both maintain an in-memory DuckDB context. You can issue DuckDB SQL against the views/tables they create.
+- `TIMDEXDataset` maintains an in-memory DuckDB context. You can issue DuckDB SQL against the views/tables they create.
+- Source-specific read methods are exposed on `timdex_dataset.records` and `timdex_dataset.embeddings`.
- Read methods use a two-step query flow for performance:
1) a metadata query determines which Parquet files and row offsets are relevant
2) a data query reads just those rows and returns the requested columns
-- Prefer simple key/value `DatasetFilters` for most use cases; add a `where=` SQL predicate when you need more advanced logic (e.g., ranges, `BETWEEN`, `>`, `<`, `IN`).
+- Prefer simple key/value filters for most use cases; add a `where=` SQL predicate when you need more advanced logic (e.g., ranges, `BETWEEN`, `>`, `<`, `IN`).
## Available read methods
+The shared read methods below are available on both `timdex_dataset.records` and
+`timdex_dataset.embeddings`:
+
- `read_batches_iter(...)`: yields `pyarrow.RecordBatch`
- `read_dicts_iter(...)`: yields Python `dict` per row
- `read_dataframe(...)`: returns a pandas `DataFrame`
- `read_dataframes_iter(...)`: yields pandas `DataFrame` batches
+
+Additionally, `timdex_dataset.records` provides:
+
- `read_transformed_records_iter(...)`: yields `transformed_record` dictionaries only
-All accept the same `DatasetFilters` and the optional `where=` SQL predicate.
+All accept the same key/value filters and the optional `where=` SQL predicate.
## Filters vs. where=
-- `DatasetFilters` are key/value arguments on read methods. They are validated and translated into SQL and will cover most queries.
+- Key/value filters are keyword arguments on read methods. They are validated and translated into SQL and will cover most queries.
- Examples: `source="alma"`, `run_date="2024-12-01"`, `run_type="daily"`, `action="index"`
-- `where=` is an optional raw SQL WHERE predicate string, combined with `DatasetFilters` using `AND`. Use it for:
+- `where=` is an optional raw SQL WHERE predicate string, combined with these filters using `AND`. Use it for:
- date/time ranges (BETWEEN, >, <)
- set membership (IN (...))
- complex boolean logic (AND/OR grouping)
@@ -46,7 +53,7 @@ This pattern keeps reads fast and memory-efficient even for large datasets.
The following diagram shows the flow for an example query:
```python
-for record_dict in td.read_dicts_iter(
+for record_dict in td.records.read_dicts_iter(
table="records",
source="dspace",
run_date="2025-09-01",
@@ -65,7 +72,7 @@ sequenceDiagram
participant P as Parquet files
U->>TD: Perform query
- Note left of TD: read_dicts_iter(
table="records",
source="dspace",
run_date="2025-09-01",
run_id="abc123")
+ Note left of TD: records.read_dicts_iter(
table="records",
source="dspace",
run_date="2025-09-01",
run_id="abc123")
TD->>TDM: build_meta_query(table, filters, where=None)
Note right of TDM: (Metadata Query)
SELECT r.timdex_record_id, r.run_id, r.filename, r.run_record_offset
FROM metadata.records r
WHERE r.source = 'dspace'
AND r.run_date = '2025-09-01'
AND r.run_id = 'abc123'
ORDER BY r.filename, r.run_record_offset
@@ -88,75 +95,88 @@ from timdex_dataset_api import TIMDEXDataset
td = TIMDEXDataset("s3://my-bucket/timdex-dataset") # example instance
# 1) Get a single record as a dict
-first = next(td.read_dicts_iter())
+first = next(td.records.read_dicts_iter())
# 2) Read batches with simple filters
-for batch in td.read_batches_iter(source="alma", run_date="2025-06-01", run_id="abc123"):
+for batch in td.records.read_batches_iter(
+ source="alma",
+ run_date="2025-06-01",
+ run_id="abc123",
+):
... # process pyarrow.RecordBatch
# 3) DataFrame of one run
-df = td.read_dataframe(source="dspace", run_date="2025-06-01", run_id="def456")
+df = td.records.read_dataframe(
+ source="dspace",
+ run_date="2025-06-01",
+ run_id="def456",
+)
# 4) Only transformed records (used by indexer)
-for rec in td.read_transformed_records_iter(source="aspace", run_type="daily"):
+for rec in td.records.read_transformed_records_iter(
+ source="aspace",
+ run_type="daily",
+):
... # rec is a dict of the transformed_record
```
## `where=` examples
-Advanced filtering that complements `DatasetFilters`.
+Advanced filtering that complements key/value filters.
```python
# date range with BETWEEN
where = "run_date BETWEEN '2024-12-01' AND '2024-12-31'"
-df = td.read_dataframe(source="alma", where=where)
+df = td.records.read_dataframe(source="alma", where=where)
# greater-than on a timestamp (if present in columns)
where = "run_timestamp > '2024-12-01T10:00:00Z'"
-df = td.read_dataframe(source="aspace", run_type="daily", where=where)
+df = td.records.read_dataframe(source="aspace", run_type="daily", where=where)
# combine set membership and action
where = "run_id IN ('run-1', 'run-3', 'run-5') AND action = 'index'"
-df = td.read_dataframe(source="alma", where=where)
+df = td.records.read_dataframe(source="alma", where=where)
# combine filters (AND) with where=
where = "run_type = 'daily' AND action = 'index'"
-df = td.read_dataframe(source="libguides", where=where)
+df = td.records.read_dataframe(source="libguides", where=where)
```
Validation tips:
- Use only a predicate (no SELECT/FROM, no trailing semicolon).
- Column names must exist in the target table/view (e.g., records or current_records).
-- `DatasetFilters` + `where=` are ANDed; if the combination yields zero rows, you’ll get an empty result.
+- Key/value filters + `where=` are ANDed; if the combination yields zero rows, you’ll get an empty result.
## Choosing a table
-By default, read methods query the `records` view (all versions). To get only the latest version per `timdex_record_id`, target the `current_records` view:
+For `timdex_dataset.records`, read methods query the `records` table by default (all versions). To get only the latest version per `timdex_record_id`, target the `current_records` view:
```python
# ALL records in the 'libguides' source
-all_libguides_df = td.read_dataframe(table="records", source="libguides")
+all_libguides_df = td.records.read_dataframe(table="records", source="libguides")
# latest unique records across the dataset
-current_df = td.read_dataframe(table="current_records")
+current_df = td.records.read_dataframe(table="current_records")
# current records for a source and specific run
-current_df = td.read_dataframe(table="current_records", source="alma", run_id="run-5")
+current_df = td.records.read_dataframe(
+ table="current_records",
+ source="alma",
+ run_id="run-5",
+)
```
## DuckDB context
-- `TIMDEXDataset` exposes a DuckDB connection used for data queries against Parquet.
-- `TIMDEXDatasetMetadata` exposes a DuckDB connection used for metadata queries and provides views:
- - `metadata.records`: all record versions with run metadata
- - `metadata.current_records`: latest record per `timdex_record_id`
- - `metadata.append_deltas`: incremental write tracking
+- `TIMDEXDataset` exposes a DuckDB connection used for metadata and data queries against Parquet.
+- `TIMDEXDataSource` provides a base class that data sources extend
+ - each data source class defines "tables" that are available for that source in the `metadata` schema
You can execute raw DuckDB SQL for inspection and debugging:
```python
-# access metadata connection
-conn = td.metadata.conn # DuckDB connection
+# access dataset DuckDB connection
+conn = td.conn # DuckDB connection
# peek at view schemas
print(conn.sql("DESCRIBE metadata.records").to_df())
diff --git a/migrations/001_2025_05_30_backfill_run_timestamp_column.py b/migrations/001_2025_05_30_backfill_run_timestamp_column.py
index e952c87..f0fb9cf 100644
--- a/migrations/001_2025_05_30_backfill_run_timestamp_column.py
+++ b/migrations/001_2025_05_30_backfill_run_timestamp_column.py
@@ -41,7 +41,8 @@
from pyarrow import fs
from timdex_dataset_api.config import configure_dev_logger, configure_logger
-from timdex_dataset_api.dataset import TIMDEX_DATASET_SCHEMA, TIMDEXDataset
+from timdex_dataset_api.dataset import TIMDEXDataset
+from timdex_dataset_api.records import TIMDEXRecords
configure_dev_logger()
@@ -125,7 +126,7 @@ def backfill_parquet_file(
# Create run_timestamp column using the exact schema definition
num_rows = len(table)
- run_timestamp_field = TIMDEX_DATASET_SCHEMA.field("run_timestamp")
+ run_timestamp_field = TIMDEXRecords.SCHEMA.field("run_timestamp")
run_timestamp_array = pa.array(
[creation_date] * num_rows, type=run_timestamp_field.type
)
diff --git a/migrations/002_2025_06_25_consistent_run_timestamp_per_etl_run.py b/migrations/002_2025_06_25_consistent_run_timestamp_per_etl_run.py
index 1014adb..e0c1247 100644
--- a/migrations/002_2025_06_25_consistent_run_timestamp_per_etl_run.py
+++ b/migrations/002_2025_06_25_consistent_run_timestamp_per_etl_run.py
@@ -50,7 +50,8 @@
from timdex_dataset_api import TIMDEXDatasetMetadata
from timdex_dataset_api.config import configure_dev_logger, configure_logger
-from timdex_dataset_api.dataset import TIMDEX_DATASET_SCHEMA, TIMDEXDataset
+from timdex_dataset_api.dataset import TIMDEXDataset
+from timdex_dataset_api.records import TIMDEXRecords
configure_dev_logger()
@@ -174,7 +175,7 @@ def backfill_parquet_file(
# set new run_timestamp value
num_rows = len(table)
- run_timestamp_field = TIMDEX_DATASET_SCHEMA.field("run_timestamp")
+ run_timestamp_field = TIMDEXRecords.SCHEMA.field("run_timestamp")
new_run_timestamp_array = pa.array(
[new_run_timestamp] * num_rows, type=run_timestamp_field.type
)
diff --git a/pyproject.toml b/pyproject.toml
index c3fa714..cd7848f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "timdex_dataset_api"
-version = "4.1.0"
+version = "5.0.0"
description = "Python library for interacting with a TIMDEX parquet dataset"
readme = "README.md"
requires-python = ">=3.12"
diff --git a/tests/conftest.py b/tests/conftest.py
index d2ad0f4..b7bc2f0 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -19,7 +19,7 @@
TIMDEXEmbeddings,
)
from timdex_dataset_api.metadata import TIMDEXDatasetMetadata
-from timdex_dataset_api.record import DatasetRecord
+from timdex_dataset_api.records import DatasetRecord
@pytest.fixture(autouse=True)
@@ -81,7 +81,7 @@ def timdex_dataset(tmp_path, timdex_dataset_config) -> TIMDEXDataset:
dataset = TIMDEXDataset(
str(tmp_path / "basic_dataset/"), config=timdex_dataset_config
)
- dataset.write(
+ dataset.records.write(
generate_sample_records(
num_records=1000,
source="alma",
@@ -111,7 +111,7 @@ def timdex_dataset_multi_source(tmp_path_factory) -> TIMDEXDataset:
("libguides", "jkl123"),
("gismit", "mno456"),
]:
- dataset.write(
+ dataset.records.write(
generate_sample_records(
num_records=1000,
source=source,
@@ -170,7 +170,7 @@ def timdex_dataset_with_runs(
for num_records, source, run_date, run_type, action, run_id in (
alma_runs + dspace_runs
):
- dataset.write(
+ dataset.records.write(
generate_sample_records(
num_records=num_records,
source=source,
@@ -208,7 +208,7 @@ def timdex_dataset_same_day_runs(tmp_path) -> TIMDEXDataset:
]
for num_records, source, run_date, run_type, action, run_id, run_timestamp in runs:
- dataset.write(
+ dataset.records.write(
generate_sample_records(
num_records=num_records,
source=source,
@@ -268,7 +268,7 @@ def timdex_metadata_with_deltas(
action="index",
run_id="run-delta-1",
)
- td.write(records)
+ td.records.write(records)
# return fresh TIMDEXDataset's metadata
return TIMDEXDataset(timdex_dataset_with_runs.location).metadata
@@ -281,7 +281,7 @@ def timdex_metadata_merged_deltas(
"""TIMDEXDatasetMetadata after merging append deltas to static database file."""
# copy directory of a dataset with runs
dataset_location = str(tmp_path / "cloned_dataset_with_runs/")
- shutil.copytree(timdex_metadata_with_deltas.location, dataset_location)
+ shutil.copytree(timdex_metadata_with_deltas.timdex_dataset.location, dataset_location)
# clone dataset with runs using new dataset location
td = TIMDEXDataset(dataset_location, config=timdex_dataset_with_runs.config)
@@ -306,11 +306,11 @@ def timdex_embeddings_with_runs(timdex_dataset_empty) -> TIMDEXEmbeddings:
timdex_dataset = timdex_dataset_empty
# write matching records for embeddings
- timdex_dataset.write(
+ timdex_dataset.records.write(
generate_sample_records(100, source="alma", run_id="abc123"),
write_append_deltas=False,
)
- timdex_dataset.write(
+ timdex_dataset.records.write(
generate_sample_records(50, source="alma", run_id="def456"),
write_append_deltas=False,
)
@@ -327,7 +327,8 @@ def timdex_embeddings_with_runs(timdex_dataset_empty) -> TIMDEXEmbeddings:
generate_sample_embeddings_for_run(timdex_dataset, run_id="def456")
)
- # reload TIMDEXDataset instance once more
+ # rebuild metadata to include embeddings, then reload
+ timdex_dataset.metadata.rebuild_dataset_metadata()
return TIMDEXDataset(timdex_dataset_empty.location).embeddings
@@ -343,7 +344,7 @@ def timdex_dataset_for_embeddings_views(timdex_dataset_empty) -> TIMDEXDataset:
timdex_dataset_dataset = timdex_dataset_empty
# scenario 1: apple - single full run
- timdex_dataset_dataset.write(
+ timdex_dataset_dataset.records.write(
generate_sample_records(
num_records=10,
source="apple",
@@ -355,7 +356,7 @@ def timdex_dataset_for_embeddings_views(timdex_dataset_empty) -> TIMDEXDataset:
)
# scenario 2: orange - full run + daily run
- timdex_dataset_dataset.write(
+ timdex_dataset_dataset.records.write(
generate_sample_records(
num_records=10,
source="orange",
@@ -365,7 +366,7 @@ def timdex_dataset_for_embeddings_views(timdex_dataset_empty) -> TIMDEXDataset:
),
write_append_deltas=False,
)
- timdex_dataset_dataset.write(
+ timdex_dataset_dataset.records.write(
generate_sample_records(
num_records=5,
source="orange",
@@ -377,7 +378,7 @@ def timdex_dataset_for_embeddings_views(timdex_dataset_empty) -> TIMDEXDataset:
)
# scenario 3: lemon - full run + daily run (daily will be embedded twice)
- timdex_dataset_dataset.write(
+ timdex_dataset_dataset.records.write(
generate_sample_records(
num_records=10,
source="lemon",
@@ -387,7 +388,7 @@ def timdex_dataset_for_embeddings_views(timdex_dataset_empty) -> TIMDEXDataset:
),
write_append_deltas=False,
)
- timdex_dataset_dataset.write(
+ timdex_dataset_dataset.records.write(
generate_sample_records(
num_records=5,
source="lemon",
diff --git a/tests/test_dataset.py b/tests/test_dataset.py
index 8457fb8..7c14d6e 100644
--- a/tests/test_dataset.py
+++ b/tests/test_dataset.py
@@ -1,9 +1,8 @@
-# ruff: noqa: D205, D209, SLF001, PLR2004
+# ruff: noqa: SLF001, PLR2004
import glob
import os
from datetime import date
-from pathlib import Path
from unittest.mock import MagicMock, patch
import pyarrow as pa
@@ -17,9 +16,11 @@
)
-def test_dataset_init_success(tmp_path):
+def test_dataset_parse_location_local_uses_local_filesystem(tmp_path):
timdex_dataset = TIMDEXDataset(str(tmp_path / "path/to/dataset"))
- assert isinstance(timdex_dataset.dataset.filesystem, fs.LocalFileSystem)
+ filesystem, path = timdex_dataset.parse_location(timdex_dataset.records.data_root)
+ assert isinstance(filesystem, fs.LocalFileSystem)
+ assert path.endswith("/data/records")
def test_dataset_init_env_vars_set_config(monkeypatch, tmp_path):
@@ -42,116 +43,27 @@ def test_dataset_init_custom_config_object(monkeypatch, tmp_path):
assert timdex_dataset.config.max_rows_per_file == 42
-@patch("timdex_dataset_api.dataset.fs.LocalFileSystem")
-@patch("timdex_dataset_api.dataset.ds.dataset")
-def test_load_pyarrow_dataset_default_uses_data_records_root(
- mock_pyarrow_ds, mock_local_fs, tmp_path
-):
- """Ensure load_pyarrow_dataset() without args calls pyarrow.dataset with the
- dataset's data_records_root path as the source and the proper filesystem."""
- mock_local_fs.return_value = MagicMock()
- mock_pyarrow_ds.return_value = MagicMock()
-
- location = str(Path(tmp_path) / "local/path/to/default_dataset")
-
- timdex_dataset = TIMDEXDataset(location=location)
- # call the explicit loader to exercise the code path
- dataset_obj = timdex_dataset.load_pyarrow_dataset()
-
- mock_pyarrow_ds.assert_called_with(
- f"{location}/data/records",
- schema=timdex_dataset.schema,
- format="parquet",
- partitioning="hive",
- filesystem=mock_local_fs.return_value,
- )
- assert dataset_obj == mock_pyarrow_ds.return_value
- assert timdex_dataset.dataset == mock_pyarrow_ds.return_value
-
-
-@patch("timdex_dataset_api.dataset.fs.LocalFileSystem")
-@patch("timdex_dataset_api.dataset.ds.dataset")
-def test_load_pyarrow_dataset_with_parquet_files_list(
- mock_pyarrow_ds, mock_local_fs, tmp_path
-):
- """Ensure load_pyarrow_dataset(parquet_files=...) passes the explicit list
- of parquet files as the source to pyarrow.dataset."""
- mock_local_fs.return_value = MagicMock()
- mock_pyarrow_ds.return_value = MagicMock()
-
- location = str(Path(tmp_path) / "local/path/to/dataset_with_files")
-
- timdex_dataset = TIMDEXDataset(location=location)
-
- parquet_files = [
- f"{timdex_dataset.data_records_root}/source=alma/run_date=2024-12-01/part-0.parquet",
- f"{timdex_dataset.data_records_root}/source=alma/run_date=2024-12-01/part-1.parquet",
- ]
-
- dataset_obj = timdex_dataset.load_pyarrow_dataset(parquet_files=parquet_files)
-
- mock_pyarrow_ds.assert_called_with(
- parquet_files,
- schema=timdex_dataset.schema,
- format="parquet",
- partitioning="hive",
- filesystem=mock_local_fs.return_value,
- )
- assert dataset_obj == mock_pyarrow_ds.return_value
- assert timdex_dataset.dataset == mock_pyarrow_ds.return_value
-
-
-@patch("timdex_dataset_api.dataset.fs.LocalFileSystem")
-@patch("timdex_dataset_api.dataset.ds.dataset")
-def test_dataset_load_local_sets_filesystem_and_dataset_success(
- mock_pyarrow_ds, mock_local_fs, tmp_path
-):
- mock_local_fs.return_value = MagicMock()
- mock_pyarrow_ds.return_value = MagicMock()
-
- location = str(Path(tmp_path) / "local/path/to/dataset")
-
- timdex_dataset = TIMDEXDataset(location=location)
-
- mock_pyarrow_ds.assert_called_once_with(
- f"{location}/data/records",
- schema=timdex_dataset.schema,
- format="parquet",
- partitioning="hive",
- filesystem=mock_local_fs.return_value,
- )
-
- assert timdex_dataset.dataset == mock_pyarrow_ds.return_value
-
-
@patch("timdex_dataset_api.dataset.TIMDEXDataset.get_s3_filesystem")
-@patch("timdex_dataset_api.dataset.ds.dataset")
-def test_dataset_load_s3_sets_filesystem_and_dataset_success(
- mock_pyarrow_ds, mock_get_s3_fs, s3_bucket_mocked
+def test_dataset_parse_location_s3_sets_filesystem_and_path(
+ mock_get_s3_fs, s3_bucket_mocked
):
mock_get_s3_fs.return_value = MagicMock()
- mock_pyarrow_ds.return_value = MagicMock()
timdex_dataset = TIMDEXDataset(location="s3://timdex/path/to/dataset")
+ filesystem, path = timdex_dataset.parse_location(timdex_dataset.records.data_root)
- mock_pyarrow_ds.assert_called_with(
- "timdex/path/to/dataset/data/records",
- schema=timdex_dataset.schema,
- format="parquet",
- partitioning="hive",
- filesystem=mock_get_s3_fs.return_value,
- )
- assert timdex_dataset.dataset == mock_pyarrow_ds.return_value
+ assert filesystem == mock_get_s3_fs.return_value
+ assert path == "timdex/path/to/dataset/data/records"
def test_filters_single_nonpartition_success(timdex_dataset_multi_source):
- df = timdex_dataset_multi_source.read_dataframe(run_id="abc123")
+ df = timdex_dataset_multi_source.records.read_dataframe(run_id="abc123")
assert df is not None
assert set(df["run_id"].unique().tolist()) == {"abc123"}
def test_filters_multi_nonpartition_success(timdex_dataset_multi_source):
- df = timdex_dataset_multi_source.read_dataframe(
+ df = timdex_dataset_multi_source.records.read_dataframe(
timdex_record_id="alma:0",
source="alma",
run_type="daily",
@@ -164,30 +76,36 @@ def test_filters_multi_nonpartition_success(timdex_dataset_multi_source):
def test_filters_or_nonpartition_success(timdex_dataset_multi_source):
- df = timdex_dataset_multi_source.read_dataframe(timdex_record_id=["alma:0", "alma:1"])
+ df = timdex_dataset_multi_source.records.read_dataframe(
+ timdex_record_id=["alma:0", "alma:1"]
+ )
assert df is not None
assert set(df["timdex_record_id"].tolist()) == {"alma:0", "alma:1"}
def test_filters_run_date_str_success(timdex_dataset_multi_source):
- df = timdex_dataset_multi_source.read_dataframe(run_date="2024-12-01")
+ df = timdex_dataset_multi_source.records.read_dataframe(run_date="2024-12-01")
assert df is not None
- df_empty = timdex_dataset_multi_source.read_dataframe(run_date="2024-12-02")
+ df_empty = timdex_dataset_multi_source.records.read_dataframe(run_date="2024-12-02")
assert df_empty is None or len(df_empty) == 0
def test_filters_run_date_obj_success(timdex_dataset_multi_source):
- df = timdex_dataset_multi_source.read_dataframe(run_date=date(2024, 12, 1))
+ df = timdex_dataset_multi_source.records.read_dataframe(run_date=date(2024, 12, 1))
assert df is not None
- df_empty = timdex_dataset_multi_source.read_dataframe(run_date=date(2024, 12, 2))
+ df_empty = timdex_dataset_multi_source.records.read_dataframe(
+ run_date=date(2024, 12, 2)
+ )
assert df_empty is None or len(df_empty) == 0
def test_filters_ymd_success(timdex_dataset_multi_source):
# metadata filters do not expose partition y/m/d; use run_date equivalents
- df = timdex_dataset_multi_source.read_dataframe(run_date=date(2024, 12, 1))
+ df = timdex_dataset_multi_source.records.read_dataframe(run_date=date(2024, 12, 1))
assert df is not None
- df_empty = timdex_dataset_multi_source.read_dataframe(run_date=date(2025, 12, 1))
+ df_empty = timdex_dataset_multi_source.records.read_dataframe(
+ run_date=date(2025, 12, 1)
+ )
assert df_empty is None or len(df_empty) == 0
@@ -195,7 +113,7 @@ def test_filters_run_date_invalid_raise_error(timdex_dataset_multi_source):
with pytest.raises(
ConversionException, match="Conversion Error: Unimplemented type for cast"
):
- timdex_dataset_multi_source.read_dataframe(run_date=999)
+ timdex_dataset_multi_source.records.read_dataframe(run_date=999)
def test_dataset_get_s3_filesystem_success(mocker):
@@ -211,35 +129,27 @@ def test_dataset_get_s3_filesystem_success(mocker):
assert isinstance(s3_filesystem, pa._s3fs.S3FileSystem)
-def test_dataset_timdex_dataset_validate_success(timdex_dataset):
- assert timdex_dataset.dataset.to_table().validate() is None # where None is valid
-
-
-def test_dataset_timdex_dataset_row_count_success(timdex_dataset):
- assert timdex_dataset.dataset.count_rows() == timdex_dataset.dataset.count_rows()
-
-
def test_dataset_all_records_not_current_and_not_deduped(
timdex_dataset_with_runs_with_metadata,
):
- all_records_df = timdex_dataset_with_runs_with_metadata.read_dataframe()
+ all_records_df = timdex_dataset_with_runs_with_metadata.records.read_dataframe()
# assert counts reflect all records from dataset, no deduping
assert all_records_df.source.value_counts().to_dict() == {"alma": 254, "dspace": 194}
# assert run_date min/max dates align with min/max for all runs
- assert all_records_df.run_date.min() == date(2024, 12, 1)
- assert all_records_df.run_date.max() == date(2025, 2, 5)
+ assert all_records_df.run_date.min().date() == date(2024, 12, 1)
+ assert all_records_df.run_date.max().date() == date(2025, 2, 5)
def test_dataset_records_data_structure_is_idempotent(timdex_dataset_with_runs):
- assert os.path.exists(timdex_dataset_with_runs.data_records_root)
- start_file_count = glob.glob(f"{timdex_dataset_with_runs.data_records_root}/**/*")
+ assert os.path.exists(timdex_dataset_with_runs.records.data_root)
+ start_file_count = glob.glob(f"{timdex_dataset_with_runs.records.data_root}/**/*")
- timdex_dataset_with_runs.create_data_structure()
+ timdex_dataset_with_runs.records.create_data_structure()
- assert os.path.exists(timdex_dataset_with_runs.data_records_root)
- end_file_count = glob.glob(f"{timdex_dataset_with_runs.data_records_root}/**/*")
+ assert os.path.exists(timdex_dataset_with_runs.records.data_root)
+ end_file_count = glob.glob(f"{timdex_dataset_with_runs.records.data_root}/**/*")
assert start_file_count == end_file_count
@@ -247,7 +157,7 @@ def test_dataset_duckdb_context_created_on_init(timdex_dataset):
assert isinstance(timdex_dataset.conn, DuckDBPyConnection)
-def test_dataset_duckdb_context_creates_data_schema(timdex_dataset):
+def test_dataset_duckdb_context_does_not_create_data_schema(timdex_dataset):
assert (
timdex_dataset.conn.query("""
select count(*)
@@ -255,16 +165,14 @@ def test_dataset_duckdb_context_creates_data_schema(timdex_dataset):
where catalog_name = 'memory'
and schema_name = 'data';
""").fetchone()[0]
- == 1
+ == 0
)
def test_dataset_preload_current_records_default_false(timdex_dataset):
assert timdex_dataset.preload_current_records is False
- assert timdex_dataset.metadata.preload_current_records is False
def test_dataset_preload_current_records_flag_true(tmp_path):
td = TIMDEXDataset(str(tmp_path), preload_current_records=True)
assert td.preload_current_records is True
- assert td.metadata.preload_current_records is True
diff --git a/tests/test_embeddings.py b/tests/test_embeddings.py
index 62364d5..6a74e4f 100644
--- a/tests/test_embeddings.py
+++ b/tests/test_embeddings.py
@@ -1,26 +1,18 @@
# ruff: noqa: PLR2004
import json
-import math
import os
-from datetime import UTC, date, datetime
+from datetime import UTC, datetime
import pandas as pd
import pyarrow as pa
import pyarrow.dataset as ds
import pytest
-from tests.utils import generate_sample_embeddings_for_run
-from timdex_dataset_api.embeddings import (
- METADATA_SELECT_FILTER_COLUMNS,
- TIMDEX_DATASET_EMBEDDINGS_SCHEMA,
- DatasetEmbedding,
- TIMDEXEmbeddings,
-)
+from tests.utils import generate_sample_embeddings_for_run, generate_sample_records
+from timdex_dataset_api import TIMDEXDataset
+from timdex_dataset_api.embeddings import DatasetEmbedding, TIMDEXEmbeddings
-EMBEDDINGS_COLUMNS_SET = set(TIMDEX_DATASET_EMBEDDINGS_SCHEMA.names)
-EMBEDDINGS_WITH_METADATA_COLUMNS_SET = EMBEDDINGS_COLUMNS_SET | set(
- METADATA_SELECT_FILTER_COLUMNS
-)
+EMBEDDINGS_AVAILABLE_COLUMNS_SET = set(TIMDEXEmbeddings.AVAILABLE_READ_COLUMNS)
def test_dataset_embedding_init():
@@ -83,7 +75,8 @@ def test_embeddings_data_root_property(timdex_dataset_empty):
timdex_embeddings = TIMDEXEmbeddings(timdex_dataset_empty)
expected = f"{timdex_dataset_empty.location.removesuffix('/')}/data/embeddings"
- assert timdex_embeddings.data_embeddings_root == expected
+ assert timdex_embeddings.data_root == expected
+ assert os.path.exists(expected)
def test_embeddings_write_basic(timdex_dataset_empty, sample_embeddings_generator):
@@ -95,7 +88,7 @@ def test_embeddings_write_basic(timdex_dataset_empty, sample_embeddings_generato
# verify written data can be read
dataset = ds.dataset(
- timdex_embeddings.data_embeddings_root, format="parquet", partitioning="hive"
+ timdex_embeddings.data_root, format="parquet", partitioning="hive"
)
assert dataset.count_rows() == 100
@@ -116,45 +109,32 @@ def test_embeddings_write_schema_applied(
# manually load dataset to confirm schema
dataset = ds.dataset(
- timdex_embeddings.data_embeddings_root,
+ timdex_embeddings.data_root,
format="parquet",
partitioning="hive",
)
- assert set(dataset.schema.names) == set(TIMDEX_DATASET_EMBEDDINGS_SCHEMA.names)
-
-
-def test_embeddings_create_batches(timdex_dataset_empty, sample_embeddings_generator):
- timdex_embeddings = TIMDEXEmbeddings(timdex_dataset_empty)
- total_embeddings = 101
- timdex_dataset_empty.config.write_batch_size = 50
-
- batches = list(
- timdex_embeddings.create_embedding_batches(
- sample_embeddings_generator(total_embeddings)
- )
- )
-
- assert len(batches) == math.ceil(
- total_embeddings / timdex_dataset_empty.config.write_batch_size
- )
+ assert set(dataset.schema.names) == set(TIMDEXEmbeddings.SCHEMA.names)
def test_embeddings_read_batches_yields_pyarrow_record_batches(
timdex_dataset_empty, sample_embeddings_generator, sample_records_generator
):
# write matching records and rebuild metadata
- timdex_dataset_empty.write(
+ timdex_dataset_empty.records.write(
sample_records_generator(100, source="alma", run_id="test-run"),
write_append_deltas=False,
)
timdex_dataset_empty.metadata.rebuild_dataset_metadata()
timdex_dataset_empty.refresh()
- # write embeddings and refresh to pick up new views
+ # write embeddings
timdex_dataset_empty.embeddings.write(
sample_embeddings_generator(100, run_id="test-run")
)
+
+ # rebuild metadata to include embeddings, then refresh
+ timdex_dataset_empty.metadata.rebuild_dataset_metadata()
timdex_dataset_empty.refresh()
batches = timdex_dataset_empty.embeddings.read_batches_iter()
@@ -165,7 +145,7 @@ def test_embeddings_read_batches_yields_pyarrow_record_batches(
def test_embeddings_read_batches_all_columns_by_default(timdex_embeddings_with_runs):
batches = timdex_embeddings_with_runs.read_batches_iter()
batch = next(batches)
- assert set(batch.column_names) == EMBEDDINGS_WITH_METADATA_COLUMNS_SET
+ assert set(batch.column_names) == EMBEDDINGS_AVAILABLE_COLUMNS_SET
def test_embeddings_read_batches_filter_columns(timdex_embeddings_with_runs):
@@ -217,7 +197,7 @@ def test_embeddings_read_batches_gets_full_dataset(timdex_embeddings_with_runs):
batches = timdex_embeddings_with_runs.read_batches_iter()
table = pa.Table.from_batches(batches)
dataset = ds.dataset(
- timdex_embeddings_with_runs.data_embeddings_root,
+ timdex_embeddings_with_runs.data_root,
format="parquet",
partitioning="hive",
)
@@ -232,7 +212,7 @@ def test_embeddings_read_batches_with_filters_gets_subset_of_dataset(
)
table = pa.Table.from_batches(batches)
dataset = ds.dataset(
- timdex_embeddings_with_runs.data_embeddings_root,
+ timdex_embeddings_with_runs.data_root,
format="parquet",
partitioning="hive",
)
@@ -279,7 +259,7 @@ def test_embeddings_read_dataframes_yields_dataframes(timdex_embeddings_with_run
def test_embeddings_read_dataframe_gets_full_dataset(timdex_embeddings_with_runs):
df = timdex_embeddings_with_runs.read_dataframe()
dataset = ds.dataset(
- timdex_embeddings_with_runs.data_embeddings_root,
+ timdex_embeddings_with_runs.data_root,
format="parquet",
partitioning="hive",
)
@@ -293,7 +273,7 @@ def test_embeddings_read_dicts_yields_dictionary_for_each_embeddings_record(
dict_iter = timdex_embeddings_with_runs.read_dicts_iter()
record = next(dict_iter)
assert isinstance(record, dict)
- assert set(record.keys()) == EMBEDDINGS_WITH_METADATA_COLUMNS_SET
+ assert set(record.keys()) == EMBEDDINGS_AVAILABLE_COLUMNS_SET
def test_current_embeddings_view_single_run(timdex_dataset_for_embeddings_views):
@@ -301,6 +281,9 @@ def test_current_embeddings_view_single_run(timdex_dataset_for_embeddings_views)
# write embeddings for run "apple-1"
td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="apple-1"))
+
+ # rebuild metadata to include embeddings, then refresh
+ td.metadata.rebuild_dataset_metadata()
td.refresh()
# query current_embeddings for apple source using read_dataframe
@@ -308,7 +291,7 @@ def test_current_embeddings_view_single_run(timdex_dataset_for_embeddings_views)
assert len(result) == 10
assert (result["run_id"] == "apple-1").all()
- assert (result["run_date"] == date(2025, 6, 1)).all()
+ assert (result["run_date"] == pd.Timestamp("2025-06-01")).all()
def test_current_embeddings_view_multiple_runs(timdex_dataset_for_embeddings_views):
@@ -317,6 +300,9 @@ def test_current_embeddings_view_multiple_runs(timdex_dataset_for_embeddings_vie
# write embeddings for runs "orange-1" and "orange-2"
td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="orange-1"))
td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="orange-2"))
+
+ # rebuild metadata to include embeddings, then refresh
+ td.metadata.rebuild_dataset_metadata()
td.refresh()
# query current_embeddings for orange source using read_dataframe
@@ -329,18 +315,190 @@ def test_current_embeddings_view_multiple_runs(timdex_dataset_for_embeddings_vie
# verify 5 from orange-1 (records not in orange-2, run_date 2025-07-01)
orange_1_records = result[result["run_id"] == "orange-1"]
assert len(orange_1_records) == 5
- assert (orange_1_records["run_date"] == date(2025, 7, 1)).all()
+ assert (orange_1_records["run_date"] == pd.Timestamp("2025-07-01")).all()
# verify 5 from orange-2 (newer records, run_date 2025-07-02)
orange_2_records = result[result["run_id"] == "orange-2"]
assert len(orange_2_records) == 5
- assert (orange_2_records["run_date"] == date(2025, 7, 2)).all()
+ assert (orange_2_records["run_date"] == pd.Timestamp("2025-07-02")).all()
+
+
+def test_current_embeddings_prefers_record_recency_over_embedding_recency(
+ tmp_path,
+):
+ td = TIMDEXDataset(str(tmp_path / "record_recency_wins_dataset/"))
+
+ td.records.write(
+ generate_sample_records(
+ num_records=10,
+ source="pear",
+ run_date="2025-09-01",
+ run_type="full",
+ run_id="pear-1",
+ ),
+ write_append_deltas=False,
+ )
+ td.records.write(
+ generate_sample_records(
+ num_records=5,
+ source="pear",
+ run_date="2025-09-02",
+ run_type="daily",
+ run_id="pear-2",
+ ),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+ td.refresh()
+
+ # older record version gets a later embedding event
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(
+ td,
+ run_id="pear-1",
+ embedding_timestamp="2025-09-04T00:00:00+00:00",
+ ),
+ write_append_deltas=False,
+ )
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(
+ td,
+ run_id="pear-2",
+ embedding_timestamp="2025-09-03T00:00:00+00:00",
+ ),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+ td.refresh()
+
+ result = td.embeddings.read_dataframe(table="current_embeddings", source="pear")
+
+ assert len(result) == 10
+
+ overlap = result[result["timdex_record_id"].isin([f"pear:{i}" for i in range(5)])]
+ non_overlap = result[
+ result["timdex_record_id"].isin([f"pear:{i}" for i in range(5, 10)])
+ ]
+
+ assert (overlap["run_id"] == "pear-2").all()
+ assert (non_overlap["run_id"] == "pear-1").all()
+ assert (
+ overlap["embedding_timestamp"] == pd.Timestamp("2025-09-03T00:00:00+00:00")
+ ).all()
+
+
+def test_current_embeddings_excludes_superseded_record_versions(tmp_path):
+ td = TIMDEXDataset(str(tmp_path / "current_embeddings_current_records_only/"))
+
+ td.records.write(
+ generate_sample_records(
+ num_records=10,
+ source="grape",
+ run_date="2025-09-01",
+ run_type="full",
+ run_id="grape-1",
+ ),
+ write_append_deltas=False,
+ )
+ td.records.write(
+ generate_sample_records(
+ num_records=5,
+ source="grape",
+ run_date="2025-09-02",
+ run_type="daily",
+ run_id="grape-2",
+ ),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(td, run_id="grape-1"),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+ td.refresh()
+
+ result = td.embeddings.read_dataframe(table="current_embeddings", source="grape")
+
+ assert len(result) == 5
+ assert set(result["timdex_record_id"]) == {f"grape:{i}" for i in range(5, 10)}
+ assert (result["run_id"] == "grape-1").all()
+
+
+def test_current_embeddings_view_keeps_models_separate(tmp_path):
+ td = TIMDEXDataset(str(tmp_path / "multiple_models_current_embeddings/"))
+
+ td.records.write(
+ generate_sample_records(
+ num_records=10,
+ source="plum",
+ run_date="2025-10-01",
+ run_type="full",
+ run_id="plum-1",
+ ),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+ td.refresh()
+
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(
+ td,
+ run_id="plum-1",
+ embedding_model="model-a",
+ embedding_strategy="full_record",
+ ),
+ write_append_deltas=False,
+ )
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(
+ td,
+ run_id="plum-1",
+ embedding_model="model-b",
+ embedding_strategy="full_record",
+ ),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+ td.refresh()
+
+ result = td.embeddings.read_dataframe(table="current_embeddings", source="plum")
+
+ assert len(result) == 20
+ assert set(result["embedding_model"].unique()) == {"model-a", "model-b"}
+ assert result.groupby("timdex_record_id")["embedding_model"].nunique().eq(2).all()
def test_current_embeddings_view_handles_duplicate_run_embeddings(
- timdex_dataset_for_embeddings_views,
+ tmp_path,
):
- td = timdex_dataset_for_embeddings_views
+ """Test that duplicate embeddings for the same run are handled correctly."""
+ td = TIMDEXDataset(str(tmp_path / "dup_run_dataset/"))
+
+ # scenario: lemon - full run + daily run (daily will be embedded twice)
+ td.records.write(
+ generate_sample_records(
+ num_records=10,
+ source="lemon",
+ run_date="2025-08-01",
+ run_type="full",
+ run_id="lemon-1",
+ ),
+ write_append_deltas=False,
+ )
+ td.records.write(
+ generate_sample_records(
+ num_records=5,
+ source="lemon",
+ run_date="2025-08-02",
+ run_type="daily",
+ run_id="lemon-2",
+ ),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+ td = TIMDEXDataset(td.location)
# write embeddings for run "lemon-1"
td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="lemon-1"))
@@ -358,6 +516,9 @@ def test_current_embeddings_view_handles_duplicate_run_embeddings(
td, run_id="lemon-2", embedding_timestamp="2025-08-03T00:00:00+00:00"
)
)
+
+ # rebuild metadata to include embeddings, then refresh
+ td.metadata.rebuild_dataset_metadata()
td.refresh()
# check all embeddings for lemon-2 to verify both writes exist
@@ -378,20 +539,45 @@ def test_current_embeddings_view_handles_duplicate_run_embeddings(
# verify lemon-1 embeddings (run_date 2025-08-01)
lemon_1_result = result[result["run_id"] == "lemon-1"]
assert len(lemon_1_result) == 5
- assert (lemon_1_result["run_date"] == date(2025, 8, 1)).all()
+ assert (lemon_1_result["run_date"] == pd.Timestamp("2025-08-01")).all()
# verify lemon-2 embeddings have the later embedding timestamp (run_date 2025-08-02)
lemon_2_result = result[result["run_id"] == "lemon-2"]
assert len(lemon_2_result) == 5
- assert (lemon_2_result["run_date"] == date(2025, 8, 2)).all()
+ assert (lemon_2_result["run_date"] == pd.Timestamp("2025-08-02")).all()
# all lemon-2 current embeddings should have the later embedding timestamp
max_timestamp = all_lemon_2["embedding_timestamp"].max()
assert (lemon_2_result["embedding_timestamp"] == max_timestamp).all()
-def test_embeddings_view_includes_all_embeddings(timdex_dataset_for_embeddings_views):
- td = timdex_dataset_for_embeddings_views
+def test_embeddings_view_includes_all_embeddings(tmp_path):
+ """Test that the embeddings view includes all embeddings from multiple writes."""
+ td = TIMDEXDataset(str(tmp_path / "all_embeddings_dataset/"))
+
+ # scenario: lemon - full run + daily run (daily will be embedded twice)
+ td.records.write(
+ generate_sample_records(
+ num_records=10,
+ source="lemon",
+ run_date="2025-08-01",
+ run_type="full",
+ run_id="lemon-1",
+ ),
+ write_append_deltas=False,
+ )
+ td.records.write(
+ generate_sample_records(
+ num_records=5,
+ source="lemon",
+ run_date="2025-08-02",
+ run_type="daily",
+ run_id="lemon-2",
+ ),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+ td = TIMDEXDataset(td.location)
# write embeddings for lemon-1
td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="lemon-1"))
@@ -409,6 +595,9 @@ def test_embeddings_view_includes_all_embeddings(timdex_dataset_for_embeddings_v
td, run_id="lemon-2", embedding_timestamp="2025-08-03T00:00:00+00:00"
)
)
+
+ # rebuild metadata to include embeddings, then refresh
+ td.metadata.rebuild_dataset_metadata()
td.refresh()
# query all embeddings for lemon source
@@ -421,22 +610,21 @@ def test_embeddings_view_includes_all_embeddings(timdex_dataset_for_embeddings_v
# verify run_date distribution
lemon_1_embeddings = result[result["run_id"] == "lemon-1"]
assert len(lemon_1_embeddings) == 10
- assert (lemon_1_embeddings["run_date"] == date(2025, 8, 1)).all()
+ assert (lemon_1_embeddings["run_date"] == pd.Timestamp("2025-08-01")).all()
lemon_2_embeddings = result[result["run_id"] == "lemon-2"]
assert len(lemon_2_embeddings) == 10 # 5 from each write
- assert (lemon_2_embeddings["run_date"] == date(2025, 8, 2)).all()
+ assert (lemon_2_embeddings["run_date"] == pd.Timestamp("2025-08-02")).all()
def test_embeddings_read_batches_iter_returns_empty_when_embeddings_missing(
timdex_dataset_empty, caplog
):
- result = list(timdex_dataset_empty.embeddings.read_batches_iter())
- assert result == []
- assert (
- "Table 'embeddings' not found in DuckDB context. Embeddings may not yet exist "
- "or TIMDEXDataset.refresh() may be required." in caplog.text
- )
+ with pytest.raises(
+ ValueError,
+ match=r"Table 'embeddings' not found in DuckDB context.*rebuild_dataset_metadata",
+ ):
+ list(timdex_dataset_empty.embeddings.read_batches_iter())
def test_embeddings_read_batches_iter_returns_empty_for_invalid_table(
diff --git a/tests/test_metadata.py b/tests/test_metadata.py
index 74cd3fd..a9306d8 100644
--- a/tests/test_metadata.py
+++ b/tests/test_metadata.py
@@ -4,21 +4,15 @@
import os
from pathlib import Path
+import pytest
from duckdb import DuckDBPyConnection
+from tests.utils import generate_sample_embeddings_for_run, generate_sample_records
from timdex_dataset_api import TIMDEXDataset
-
-ORDERED_METADATA_COLUMN_NAMES = [
- "timdex_record_id",
- "source",
- "run_date",
- "run_type",
- "action",
- "run_id",
- "run_record_offset",
- "run_timestamp",
- "filename",
-]
+from timdex_dataset_api.data_source import TIMDEXDataSource
+from timdex_dataset_api.embeddings import TIMDEXEmbeddings
+from timdex_dataset_api.metadata import TIMDEXDatasetMetadata
+from timdex_dataset_api.records import TIMDEXRecords
def test_tdm_init_no_metadata_file_warning_success(caplog, tmp_path):
@@ -31,14 +25,63 @@ def test_tdm_init_no_metadata_file_warning_success(caplog, tmp_path):
def test_tdm_local_dataset_structure_properties(tmp_path):
local_root = str(Path(tmp_path) / "path/to/nothing")
td_local = TIMDEXDataset(local_root)
- assert td_local.metadata.location == local_root
- assert td_local.metadata.location_scheme == "file"
+ assert td_local.location == local_root
+ assert td_local.location_scheme == "file"
def test_tdm_s3_dataset_structure_properties(timdex_dataset_empty):
# test that location_scheme property works correctly for local paths
# S3 tests require full mocking and are covered in other tests
- assert timdex_dataset_empty.metadata.location_scheme == "file"
+ assert timdex_dataset_empty.location_scheme == "file"
+
+
+def test_data_source_metadata_columns_are_derived_from_base_class():
+ assert (
+ TIMDEXRecords.SOURCE_METADATA_COLUMNS
+ == TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS
+ )
+ assert TIMDEXRecords.METADATA_COLUMNS == TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS
+
+ assert TIMDEXEmbeddings.SOURCE_METADATA_COLUMNS == [
+ "timdex_record_id",
+ "run_id",
+ "run_record_offset",
+ "filename",
+ "embedding_timestamp",
+ "embedding_model",
+ "embedding_strategy",
+ ]
+ assert [
+ *TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS,
+ "embedding_timestamp",
+ "embedding_model",
+ "embedding_strategy",
+ ] == TIMDEXEmbeddings.METADATA_COLUMNS
+
+
+def test_data_source_subclass_requires_contract_vars():
+ with pytest.raises(
+ TypeError,
+ match=(
+ "InvalidDataSource must define required class vars: "
+ "SCHEMA, DATA_COLUMNS, DATA_PATH"
+ ),
+ ):
+
+ class InvalidDataSource(TIMDEXDataSource):
+ NAME = "invalid"
+
+
+def test_dataset_registers_table_configs_from_data_sources(tmp_path):
+ td = TIMDEXDataset(str(tmp_path / "register_table_configs"))
+
+ expected_table_names = [
+ table_config.name
+ for table_config in (TIMDEXRecords.TABLES + TIMDEXEmbeddings.TABLES)
+ ]
+ assert [
+ table_config.name for table_config in td.table_configs
+ ] == expected_table_names
def test_tdm_create_metadata_database_file_success(
@@ -51,12 +94,12 @@ def test_tdm_create_metadata_database_file_success(
def test_tdm_init_metadata_file_found_success(timdex_metadata):
- assert isinstance(timdex_metadata.conn, DuckDBPyConnection)
+ assert isinstance(timdex_metadata.timdex_dataset.conn, DuckDBPyConnection)
def test_tdm_duckdb_context_creates_metadata_schema(timdex_metadata):
assert (
- timdex_metadata.conn.query("""
+ timdex_metadata.timdex_dataset.conn.query("""
select count(*)
from information_schema.schemata
where catalog_name = 'memory'
@@ -68,12 +111,14 @@ def test_tdm_duckdb_context_creates_metadata_schema(timdex_metadata):
def test_tdm_connection_has_static_database_attached(timdex_metadata):
assert set(
- timdex_metadata.conn.query("""show databases;""").to_df().database_name
+ timdex_metadata.timdex_dataset.conn.query("""show databases;""")
+ .to_df()
+ .database_name
) == {"memory", "static_db"}
def test_tdm_connection_static_database_records_table_exists(timdex_metadata):
- records_df = timdex_metadata.conn.query(
+ records_df = timdex_metadata.timdex_dataset.conn.query(
"""select * from static_db.records;"""
).to_df()
assert len(records_df) > 0
@@ -91,17 +136,69 @@ def test_dataset_metadata_structure_is_idempotent(timdex_metadata):
def test_tdm_views_created_on_init(timdex_metadata):
- views = timdex_metadata.conn.query(
+ views = timdex_metadata.timdex_dataset.conn.query(
"""select table_name from information_schema.tables where table_type = 'VIEW';"""
).to_df()
- expected_views = {"append_deltas", "records", "current_records"}
+ expected_views = {"records_append_deltas", "records", "current_records"}
actual_views = set(views.table_name)
assert expected_views <= actual_views
+def test_tdm_custom_tables_missing_dependencies_are_skipped_generically(caplog, tmp_path):
+ dataset_path = str(tmp_path / "current_view_missing_dependencies")
+
+ td = TIMDEXDataset(dataset_path)
+ td.records.write(
+ generate_sample_records(
+ num_records=10,
+ source="alma",
+ run_date="2025-03-01",
+ run_type="full",
+ run_id="missing-deps-run",
+ ),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+
+ caplog.set_level("WARNING")
+ caplog.clear()
+
+ td_with_metadata = TIMDEXDataset(dataset_path)
+
+ metadata_objects = td_with_metadata.conn.query("""
+ select table_name
+ from information_schema.tables
+ where table_schema = 'metadata'
+ """).to_df()
+ metadata_names = set(metadata_objects.table_name)
+
+ missing_tables = []
+ for table_config in td_with_metadata.table_configs:
+ if table_config.kind != "custom":
+ continue
+
+ missing_required_tables = [
+ table_name
+ for table_name in table_config.required_metadata_tables
+ if table_name not in metadata_names
+ ]
+ if not missing_required_tables:
+ continue
+
+ missing_tables.append(table_config.name)
+ assert table_config.name not in metadata_names
+ assert (
+ "Skipping metadata."
+ f"{table_config.name} view creation because missing dependencies: "
+ f"{', '.join(missing_required_tables)}"
+ ) in caplog.text
+
+ assert missing_tables
+
+
def test_tdm_records_view_structure(timdex_metadata):
- records_df = timdex_metadata.conn.query(
+ records_df = timdex_metadata.timdex_dataset.conn.query(
"""select * from metadata.records limit 1;"""
).to_df()
expected_columns = {
@@ -119,7 +216,7 @@ def test_tdm_records_view_structure(timdex_metadata):
def test_tdm_current_records_view_structure(timdex_metadata):
- current_records_df = timdex_metadata.conn.query(
+ current_records_df = timdex_metadata.timdex_dataset.conn.query(
"""select * from metadata.current_records limit 1;"""
).to_df()
expected_columns = {
@@ -137,8 +234,8 @@ def test_tdm_current_records_view_structure(timdex_metadata):
def test_tdm_append_deltas_view_empty_structure(timdex_metadata):
- append_deltas_df = timdex_metadata.conn.query(
- """select * from metadata.append_deltas;"""
+ append_deltas_df = timdex_metadata.timdex_dataset.conn.query(
+ """select * from metadata.records_append_deltas;"""
).to_df()
expected_columns = {
"timdex_record_id",
@@ -150,6 +247,7 @@ def test_tdm_append_deltas_view_empty_structure(timdex_metadata):
"run_record_offset",
"run_timestamp",
"filename",
+ "append_delta_filename",
}
assert set(append_deltas_df.columns) == expected_columns
assert len(append_deltas_df) == 0
@@ -158,7 +256,7 @@ def test_tdm_append_deltas_view_empty_structure(timdex_metadata):
def test_tdm_records_count_property(timdex_metadata):
assert timdex_metadata.records_count > 0
- manual_count = timdex_metadata.conn.query(
+ manual_count = timdex_metadata.timdex_dataset.conn.query(
"""select count(*) from metadata.records;"""
).fetchone()[0]
assert timdex_metadata.records_count == manual_count
@@ -167,7 +265,7 @@ def test_tdm_records_count_property(timdex_metadata):
def test_tdm_current_records_count_property(timdex_metadata):
assert timdex_metadata.current_records_count > 0
- manual_count = timdex_metadata.conn.query(
+ manual_count = timdex_metadata.timdex_dataset.conn.query(
"""select count(*) from metadata.current_records;"""
).fetchone()[0]
assert timdex_metadata.current_records_count == manual_count
@@ -178,10 +276,10 @@ def test_tdm_append_deltas_count_property_empty(timdex_metadata):
def test_tdm_records_equals_static_without_deltas(timdex_metadata):
- static_count = timdex_metadata.conn.query(
+ static_count = timdex_metadata.timdex_dataset.conn.query(
"""select count(*) from static_db.records;"""
).fetchone()[0]
- records_count = timdex_metadata.conn.query(
+ records_count = timdex_metadata.timdex_dataset.conn.query(
"""select count(*) from metadata.records;"""
).fetchone()[0]
assert static_count == records_count
@@ -196,11 +294,11 @@ def test_tdm_current_records_filtering_logic(timdex_metadata):
def test_tdm_views_with_append_deltas(timdex_metadata_with_deltas):
- views = timdex_metadata_with_deltas.conn.query(
+ views = timdex_metadata_with_deltas.timdex_dataset.conn.query(
"""select table_name from information_schema.tables where table_type = 'VIEW';"""
).to_df()
- expected_views = {"append_deltas", "records", "current_records"}
+ expected_views = {"records_append_deltas", "records", "current_records"}
actual_views = set(views.table_name)
assert expected_views.issubset(actual_views)
@@ -211,7 +309,7 @@ def test_tdm_append_deltas_view_has_data(timdex_metadata_with_deltas):
def test_tdm_records_includes_deltas(timdex_metadata_with_deltas):
- static_count = timdex_metadata_with_deltas.conn.query(
+ static_count = timdex_metadata_with_deltas.timdex_dataset.conn.query(
"""select count(*) from static_db.records;"""
).fetchone()[0]
deltas_count = timdex_metadata_with_deltas.append_deltas_count
@@ -229,7 +327,7 @@ def test_tdm_current_records_with_deltas_logic(timdex_metadata_with_deltas):
assert current_count > 0
# verify current records view returns unique timdex_record_id values
- current_records_df = timdex_metadata_with_deltas.conn.query(
+ current_records_df = timdex_metadata_with_deltas.timdex_dataset.conn.query(
"""select timdex_record_id from metadata.current_records;"""
).to_df()
@@ -239,7 +337,7 @@ def test_tdm_current_records_with_deltas_logic(timdex_metadata_with_deltas):
def test_tdm_current_records_most_recent_version(timdex_metadata_with_deltas):
# check that for records with multiple versions, only the most recent is returned
- multi_version_records = timdex_metadata_with_deltas.conn.query("""
+ multi_version_records = timdex_metadata_with_deltas.timdex_dataset.conn.query("""
select timdex_record_id, count(*) as version_count
from metadata.records
group by timdex_record_id
@@ -251,7 +349,7 @@ def test_tdm_current_records_most_recent_version(timdex_metadata_with_deltas):
record_id = multi_version_records.iloc[0]["timdex_record_id"]
# get most recent timestamp for this record
- most_recent = timdex_metadata_with_deltas.conn.query(f"""
+ most_recent = timdex_metadata_with_deltas.timdex_dataset.conn.query(f"""
select run_timestamp, run_id
from metadata.records
where timdex_record_id = '{record_id}'
@@ -260,7 +358,7 @@ def test_tdm_current_records_most_recent_version(timdex_metadata_with_deltas):
""").to_df()
# verify current_records contains this version
- current_version = timdex_metadata_with_deltas.conn.query(f"""
+ current_version = timdex_metadata_with_deltas.timdex_dataset.conn.query(f"""
select run_timestamp, run_id
from metadata.current_records
where timdex_record_id = '{record_id}';
@@ -277,7 +375,7 @@ def test_tdm_current_records_most_recent_version(timdex_metadata_with_deltas):
def test_tdm_merge_append_deltas_static_counts_match_records_count_before_merge(
timdex_metadata_with_deltas, timdex_metadata_merged_deltas
):
- static_count_merged_deltas = timdex_metadata_merged_deltas.conn.query(
+ static_count_merged_deltas = timdex_metadata_merged_deltas.timdex_dataset.conn.query(
"""select count(*) as count from static_db.records;"""
).fetchone()[0]
assert static_count_merged_deltas == timdex_metadata_with_deltas.records_count
@@ -286,15 +384,16 @@ def test_tdm_merge_append_deltas_static_counts_match_records_count_before_merge(
def test_tdm_merge_append_deltas_adds_records_to_static_db(
timdex_metadata_with_deltas, timdex_metadata_merged_deltas
):
- append_deltas = timdex_metadata_with_deltas.conn.query(f"""
+ columns = ",".join(TIMDEXRecords.SOURCE_METADATA_COLUMNS)
+ append_deltas = timdex_metadata_with_deltas.timdex_dataset.conn.query(f"""
select
- {",".join(ORDERED_METADATA_COLUMN_NAMES)}
- from metadata.append_deltas
+ {columns}
+ from metadata.records_append_deltas
""").to_df()
- merged_static_db = timdex_metadata_merged_deltas.conn.query(f"""
+ merged_static_db = timdex_metadata_merged_deltas.timdex_dataset.conn.query(f"""
select
- {",".join(ORDERED_METADATA_COLUMN_NAMES)}
+ {columns}
from static_db.records
""").to_df()
@@ -306,11 +405,475 @@ def test_tdm_merge_append_deltas_adds_records_to_static_db(
def test_tdm_merge_append_deltas_deletes_append_deltas(
timdex_metadata_with_deltas, timdex_metadata_merged_deltas
):
+ records_deltas_path_before = timdex_metadata_with_deltas.append_deltas_path_for(
+ TIMDEXRecords
+ )
+ records_deltas_path_after = timdex_metadata_merged_deltas.append_deltas_path_for(
+ TIMDEXRecords
+ )
+
assert timdex_metadata_with_deltas.append_deltas_count != 0
- assert os.listdir(timdex_metadata_with_deltas.append_deltas_path)
+ assert os.listdir(records_deltas_path_before)
assert timdex_metadata_merged_deltas.append_deltas_count == 0
- assert not os.listdir(timdex_metadata_merged_deltas.append_deltas_path)
+ assert not os.listdir(records_deltas_path_after)
+
+
+def test_tdm_embeddings_metadata_view_structure(tmp_path):
+ td = TIMDEXDataset(str(tmp_path / "embeddings_metadata_structure"))
+
+ td.records.write(
+ generate_sample_records(
+ num_records=25,
+ source="alma",
+ run_date="2025-03-01",
+ run_type="full",
+ run_id="emb-structure-run",
+ ),
+ write_append_deltas=False,
+ )
+
+ td.metadata.rebuild_dataset_metadata()
+
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(td, run_id="emb-structure-run"),
+ write_append_deltas=False,
+ )
+
+ td.metadata.rebuild_dataset_metadata()
+
+ embeddings_df = td.conn.query(
+ """select * from metadata.embeddings limit 1;"""
+ ).to_df()
+ assert len(embeddings_df) == 1
+ expected_columns = set(TIMDEXEmbeddings.METADATA_COLUMNS)
+ assert set(embeddings_df.columns) == expected_columns
+
+
+def test_tdm_current_embeddings_view_structure(tmp_path):
+ td = TIMDEXDataset(str(tmp_path / "current_embeddings_structure"))
+
+ td.records.write(
+ generate_sample_records(
+ num_records=25,
+ source="alma",
+ run_date="2025-03-01",
+ run_type="full",
+ run_id="emb-current-structure-run",
+ ),
+ write_append_deltas=False,
+ )
+
+ td.metadata.rebuild_dataset_metadata()
+
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(td, run_id="emb-current-structure-run"),
+ write_append_deltas=False,
+ )
+
+ td.metadata.rebuild_dataset_metadata()
+
+ current_embeddings_df = td.conn.query(
+ """select * from metadata.current_embeddings limit 1;"""
+ ).to_df()
+
+ assert len(current_embeddings_df) == 1
+ expected_columns = set(TIMDEXEmbeddings.METADATA_COLUMNS)
+ assert set(current_embeddings_df.columns) == expected_columns
+
+
+def test_tdm_current_embeddings_latest_per_record_strategy(tmp_path):
+ td = TIMDEXDataset(str(tmp_path / "current_embeddings_latest"))
+
+ td.records.write(
+ generate_sample_records(
+ num_records=10,
+ source="alma",
+ run_date="2025-03-01",
+ run_type="full",
+ run_id="emb-current-latest-run-1",
+ ),
+ write_append_deltas=False,
+ )
+ td.records.write(
+ generate_sample_records(
+ num_records=5,
+ source="alma",
+ run_date="2025-03-02",
+ run_type="daily",
+ run_id="emb-current-latest-run-2",
+ ),
+ write_append_deltas=False,
+ )
+
+ td.metadata.rebuild_dataset_metadata()
+
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(
+ td,
+ run_id="emb-current-latest-run-1",
+ embedding_timestamp="2025-03-10T00:00:00+00:00",
+ ),
+ write_append_deltas=False,
+ )
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(
+ td,
+ run_id="emb-current-latest-run-2",
+ embedding_timestamp="2025-03-11T00:00:00+00:00",
+ ),
+ write_append_deltas=False,
+ )
+
+ td.metadata.rebuild_dataset_metadata()
+
+ current_embeddings_df = td.conn.query("""
+ select
+ timdex_record_id,
+ run_id,
+ embedding_strategy
+ from metadata.current_embeddings
+ """).to_df()
+
+ expected_total_rows = 10
+ expected_run_1_rows = 5
+ expected_run_2_rows = 5
+
+ assert len(current_embeddings_df) == expected_total_rows
+ assert (
+ len(
+ current_embeddings_df[
+ current_embeddings_df.run_id == "emb-current-latest-run-1"
+ ]
+ )
+ == expected_run_1_rows
+ )
+ assert (
+ len(
+ current_embeddings_df[
+ current_embeddings_df.run_id == "emb-current-latest-run-2"
+ ]
+ )
+ == expected_run_2_rows
+ )
+
+
+def test_tdm_current_run_embeddings_view_structure(tmp_path):
+ td = TIMDEXDataset(str(tmp_path / "current_run_embeddings_structure"))
+
+ td.records.write(
+ generate_sample_records(
+ num_records=25,
+ source="alma",
+ run_date="2025-03-01",
+ run_type="full",
+ run_id="emb-current-run-structure-run",
+ ),
+ write_append_deltas=False,
+ )
+
+ td.metadata.rebuild_dataset_metadata()
+
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(td, run_id="emb-current-run-structure-run"),
+ write_append_deltas=False,
+ )
+
+ td.metadata.rebuild_dataset_metadata()
+
+ current_run_embeddings_df = td.conn.query(
+ """select * from metadata.current_run_embeddings limit 1;"""
+ ).to_df()
+
+ assert len(current_run_embeddings_df) == 1
+ expected_columns = set(TIMDEXEmbeddings.METADATA_COLUMNS)
+ assert set(current_run_embeddings_df.columns) == expected_columns
+
+
+def test_tdm_prejoined_embeddings_view_has_correct_source_values(tmp_path):
+ """Verify pre-joined source column matches the underlying records."""
+ td = TIMDEXDataset(str(tmp_path / "prejoin_source_values"))
+
+ td.records.write(
+ generate_sample_records(
+ num_records=10,
+ source="alma",
+ run_date="2025-03-01",
+ run_type="full",
+ run_id="prejoin-run-1",
+ ),
+ write_append_deltas=False,
+ )
+ td.records.write(
+ generate_sample_records(
+ num_records=10,
+ source="dspace",
+ run_date="2025-03-02",
+ run_type="full",
+ run_id="prejoin-run-2",
+ ),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(td, run_id="prejoin-run-1"),
+ write_append_deltas=False,
+ )
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(td, run_id="prejoin-run-2"),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+
+ # all embeddings from run-1 should have source='alma'
+ alma_embeddings = td.conn.query("""
+ select count(*) from metadata.embeddings
+ where run_id = 'prejoin-run-1' and source = 'alma'
+ """).fetchone()[0]
+ assert alma_embeddings == 10 # noqa: PLR2004
+
+ # all embeddings from run-2 should have source='dspace'
+ dspace_embeddings = td.conn.query("""
+ select count(*) from metadata.embeddings
+ where run_id = 'prejoin-run-2' and source = 'dspace'
+ """).fetchone()[0]
+ assert dspace_embeddings == 10 # noqa: PLR2004
+
+ # verify current_embeddings also has pre-joined source column
+ alma_current = td.conn.query("""
+ select count(*) from metadata.current_embeddings
+ where source = 'alma'
+ """).fetchone()[0]
+ dspace_current = td.conn.query("""
+ select count(*) from metadata.current_embeddings
+ where source = 'dspace'
+ """).fetchone()[0]
+ assert alma_current == 10 # noqa: PLR2004
+ assert dspace_current == 10 # noqa: PLR2004
+
+
+def test_tdm_prejoined_embeddings_filterable_by_run_date(tmp_path):
+ """Verify pre-joined run_date column is usable for filtering."""
+ td = TIMDEXDataset(str(tmp_path / "prejoin_run_date_filter"))
+
+ td.records.write(
+ generate_sample_records(
+ num_records=10,
+ source="alma",
+ run_date="2025-03-01",
+ run_type="full",
+ run_id="filter-run-1",
+ ),
+ write_append_deltas=False,
+ )
+ td.records.write(
+ generate_sample_records(
+ num_records=10,
+ source="alma",
+ run_date="2025-04-01",
+ run_type="full",
+ run_id="filter-run-2",
+ ),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(td, run_id="filter-run-1"),
+ write_append_deltas=False,
+ )
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(td, run_id="filter-run-2"),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+
+ # filter embeddings by run_date
+ march_embeddings = td.conn.query("""
+ select count(*) from metadata.embeddings
+ where run_date = cast('2025-03-01' as date)
+ """).fetchone()[0]
+ assert march_embeddings == 10 # noqa: PLR2004
+
+ april_embeddings = td.conn.query("""
+ select count(*) from metadata.embeddings
+ where run_date = cast('2025-04-01' as date)
+ """).fetchone()[0]
+ assert april_embeddings == 10 # noqa: PLR2004
+
+
+def test_tdm_keyset_paginated_query_on_prejoined_embeddings_view(tmp_path):
+ """Verify build_keyset_paginated_metadata_query works on pre-joined embeddings."""
+ td = TIMDEXDataset(str(tmp_path / "keyset_prejoin_embeddings"))
+
+ td.records.write(
+ generate_sample_records(
+ num_records=25,
+ source="alma",
+ run_date="2025-03-01",
+ run_type="full",
+ run_id="keyset-prejoin-run",
+ ),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(td, run_id="keyset-prejoin-run"),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+ td.reflect_sa_tables()
+
+ # build a keyset pagination query against the pre-joined embeddings view
+ query = td.metadata.build_keyset_paginated_metadata_query(
+ "embeddings",
+ limit=10,
+ keyset_value=(0, 0, 0),
+ )
+
+ # execute and verify results
+ result_df = td.conn.query(query).to_df()
+ assert len(result_df) == 10 # noqa: PLR2004
+ expected_cols = {*TIMDEXEmbeddings.METADATA_COLUMNS, "run_id_hash", "filename_hash"}
+ assert set(result_df.columns) == expected_cols
+
+
+def test_tdm_records_bootstrap_from_append_deltas_without_static_db(tmp_path):
+ record_count = 20
+ td = TIMDEXDataset(str(tmp_path / "records_append_deltas_bootstrap"))
+
+ td.records.write(
+ generate_sample_records(
+ num_records=record_count,
+ source="alma",
+ run_date="2025-03-01",
+ run_type="full",
+ run_id="records-bootstrap-run",
+ )
+ )
+
+ assert td.metadata.database_exists() is False
+ assert len(td.records.read_dataframe()) == record_count
+ assert len(td.records.read_dataframe(table="current_records")) == record_count
+
+
+def test_tdm_embeddings_bootstrap_from_append_deltas_without_static_db(tmp_path):
+ record_count = 20
+ td = TIMDEXDataset(str(tmp_path / "embeddings_append_deltas_bootstrap"))
+
+ td.records.write(
+ generate_sample_records(
+ num_records=record_count,
+ source="alma",
+ run_date="2025-03-02",
+ run_type="full",
+ run_id="emb-delta-run",
+ )
+ )
+ td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="emb-delta-run"))
+
+ assert td.metadata.database_exists() is False
+ assert len(td.embeddings.read_dataframe()) == record_count
+ assert len(td.embeddings.read_dataframe(table="current_embeddings")) == record_count
+
+
+def test_tdm_embeddings_write_append_deltas_without_static_embeddings_table(tmp_path):
+ record_count = 20
+ td = TIMDEXDataset(str(tmp_path / "embeddings_append_deltas_only"))
+
+ # build records metadata only
+ td.records.write(
+ generate_sample_records(
+ num_records=record_count,
+ source="alma",
+ run_date="2025-03-02",
+ run_type="full",
+ run_id="emb-delta-run",
+ ),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+
+ # write embeddings with append deltas (without rebuilding static metadata first)
+ td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="emb-delta-run"))
+
+ # embeddings metadata views should still exist and include append deltas
+ embeddings_count = td.conn.query(
+ """select count(*) from metadata.embeddings;"""
+ ).fetchone()[0]
+ embeddings_deltas_count = td.conn.query(
+ """select count(*) from metadata.embeddings_append_deltas;"""
+ ).fetchone()[0]
+
+ embeddings_deltas_path = td.metadata.append_deltas_path_for(TIMDEXEmbeddings)
+ assert embeddings_count == record_count
+ assert embeddings_deltas_count == record_count
+ assert os.listdir(embeddings_deltas_path)
+
+
+def test_tdm_merge_append_deltas_merges_embeddings(tmp_path):
+ run_1_count = 30
+ run_2_count = 10
+ td = TIMDEXDataset(str(tmp_path / "embeddings_merge"))
+
+ # write records + initial embeddings and rebuild so static_db.embeddings exists
+ td.records.write(
+ generate_sample_records(
+ num_records=run_1_count,
+ source="alma",
+ run_date="2025-03-03",
+ run_type="full",
+ run_id="emb-merge-run-1",
+ ),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+
+ td.embeddings.write(
+ generate_sample_embeddings_for_run(td, run_id="emb-merge-run-1"),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+
+ # write second embeddings run with append deltas
+ td.records.write(
+ generate_sample_records(
+ num_records=run_2_count,
+ source="alma",
+ run_date="2025-03-04",
+ run_type="daily",
+ run_id="emb-merge-run-2",
+ ),
+ write_append_deltas=False,
+ )
+ td.metadata.rebuild_dataset_metadata()
+
+ td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="emb-merge-run-2"))
+
+ embeddings_count_before_merge = td.conn.query(
+ """select count(*) from metadata.embeddings;"""
+ ).fetchone()[0]
+ assert (
+ td.conn.query(
+ """select count(*) from metadata.embeddings_append_deltas;"""
+ ).fetchone()[0]
+ == run_2_count
+ )
+
+ td.metadata.merge_append_deltas()
+ td.refresh()
+
+ embeddings_static_after_merge = td.conn.query(
+ """select count(*) from static_db.embeddings;"""
+ ).fetchone()[0]
+ embeddings_deltas_after_merge = td.conn.query(
+ """select count(*) from metadata.embeddings_append_deltas;"""
+ ).fetchone()[0]
+
+ assert embeddings_static_after_merge == embeddings_count_before_merge
+ assert embeddings_deltas_after_merge == 0
def test_td_prepare_duckdb_secret_and_extensions_home_env_var_set_and_valid(
@@ -379,13 +942,13 @@ def test_td_prepare_duckdb_secret_and_extensions_home_env_var_set_but_empty(
def test_td_preload_current_records_default_false(tmp_path):
td = TIMDEXDataset(str(tmp_path))
assert td.preload_current_records is False
- assert td.metadata.preload_current_records is False
+ assert td.preload_current_records is False
def test_td_preload_current_records_flag_true(tmp_path):
td = TIMDEXDataset(str(tmp_path), preload_current_records=True)
assert td.preload_current_records is True
- assert td.metadata.preload_current_records is True
+ assert td.preload_current_records is True
def test_tdm_preload_false_no_temp_table(timdex_dataset_with_runs):
@@ -393,7 +956,7 @@ def test_tdm_preload_false_no_temp_table(timdex_dataset_with_runs):
td = TIMDEXDataset(timdex_dataset_with_runs.location)
# assert that materialized, temporary table "temp.current_records" does not exist
- temp_table_count = td.metadata.conn.query("""
+ temp_table_count = td.conn.query("""
select count(*)
from information_schema.tables
where table_catalog = 'temp'
@@ -410,7 +973,7 @@ def test_tdm_preload_true_has_temp_table(timdex_dataset_with_runs):
td = TIMDEXDataset(timdex_dataset_with_runs.location, preload_current_records=True)
# assert that materialized, temporary table "temp.current_records" does exist
- temp_table_count = td.metadata.conn.query("""
+ temp_table_count = td.conn.query("""
select count(*)
from information_schema.tables
where table_catalog = 'temp'
diff --git a/tests/test_read.py b/tests/test_read.py
index 9941522..bf7b2d9 100644
--- a/tests/test_read.py
+++ b/tests/test_read.py
@@ -6,38 +6,50 @@
import pytest
from duckdb import ParserException
-from timdex_dataset_api.dataset import TIMDEX_DATASET_SCHEMA
+from timdex_dataset_api.records import TIMDEXRecords
-DATASET_COLUMNS_SET = set(TIMDEX_DATASET_SCHEMA.names)
+DATASET_COLUMNS_SET = set(TIMDEXRecords.AVAILABLE_READ_COLUMNS)
+
+
+def _count_rows_via_duckdb_parquet(timdex_dataset) -> int:
+ return timdex_dataset.conn.query(f"""
+ select count(*)
+ from read_parquet(
+ '{timdex_dataset.records.data_root}/**/*.parquet',
+ hive_partitioning=true
+ )
+ """).fetchone()[0]
def test_read_batches_yields_pyarrow_record_batches(timdex_dataset_multi_source):
- batches = timdex_dataset_multi_source.read_batches_iter()
+ batches = timdex_dataset_multi_source.records.read_batches_iter()
batch = next(batches)
assert isinstance(batch, pa.RecordBatch)
def test_read_batches_all_columns_by_default(timdex_dataset_multi_source):
- batches = timdex_dataset_multi_source.read_batches_iter()
+ batches = timdex_dataset_multi_source.records.read_batches_iter()
batch = next(batches)
assert set(batch.column_names) == DATASET_COLUMNS_SET
def test_read_batches_filter_columns(timdex_dataset_multi_source):
columns_subset = ["source", "transformed_record"]
- batches = timdex_dataset_multi_source.read_batches_iter(columns=columns_subset)
+ batches = timdex_dataset_multi_source.records.read_batches_iter(
+ columns=columns_subset
+ )
batch = next(batches)
assert set(batch.column_names) == set(columns_subset)
def test_read_batches_no_filters_gets_full_dataset(timdex_dataset_multi_source):
- batches = timdex_dataset_multi_source.read_batches_iter()
+ batches = timdex_dataset_multi_source.records.read_batches_iter()
table = pa.Table.from_batches(batches)
- assert len(table) == timdex_dataset_multi_source.dataset.count_rows()
+ assert len(table) == _count_rows_via_duckdb_parquet(timdex_dataset_multi_source)
def test_read_batches_with_filters_gets_subset_of_dataset(timdex_dataset_multi_source):
- batches = timdex_dataset_multi_source.read_batches_iter(
+ batches = timdex_dataset_multi_source.records.read_batches_iter(
source="libguides",
run_date="2024-12-01",
run_type="daily",
@@ -45,15 +57,17 @@ def test_read_batches_with_filters_gets_subset_of_dataset(timdex_dataset_multi_s
)
table = pa.Table.from_batches(batches)
+ total_rows = _count_rows_via_duckdb_parquet(timdex_dataset_multi_source)
+
assert len(table) == 1_000
- assert len(table) < timdex_dataset_multi_source.dataset.count_rows()
+ assert len(table) < total_rows
- # assert loaded dataset is unchanged by filtering for a read method
- assert timdex_dataset_multi_source.dataset.count_rows() == 5_000
+ # assert loaded parquet data is unchanged by filtering for a read method
+ assert total_rows == 5_000
def test_read_dataframes_yields_dataframes(timdex_dataset_multi_source):
- df_iter = timdex_dataset_multi_source.read_dataframes_iter()
+ df_iter = timdex_dataset_multi_source.records.read_dataframes_iter()
df_batch = next(df_iter)
assert isinstance(df_batch, pd.DataFrame)
assert len(df_batch) == 1_000
@@ -62,47 +76,51 @@ def test_read_dataframes_yields_dataframes(timdex_dataset_multi_source):
def test_read_dataframe_gets_full_dataset(
timdex_dataset_multi_source,
):
- df = timdex_dataset_multi_source.read_dataframe()
+ df = timdex_dataset_multi_source.records.read_dataframe()
assert isinstance(df, pd.DataFrame)
- assert len(df) == timdex_dataset_multi_source.dataset.count_rows()
+ assert len(df) == _count_rows_via_duckdb_parquet(timdex_dataset_multi_source)
def test_read_dicts_yields_dictionary_for_each_dataset_record(
timdex_dataset_multi_source,
):
- records = timdex_dataset_multi_source.read_dicts_iter()
+ records = timdex_dataset_multi_source.records.read_dicts_iter()
record = next(records)
assert isinstance(record, dict)
assert set(record.keys()) == DATASET_COLUMNS_SET
def test_read_batches_filter_to_none_returns_empty_list(timdex_dataset_multi_source):
- batches = timdex_dataset_multi_source.read_batches_iter(source="not-gonna-find-me")
+ batches = timdex_dataset_multi_source.records.read_batches_iter(
+ source="not-gonna-find-me"
+ )
assert list(batches) == []
def test_read_dicts_filter_to_none_stopiteration_immediately(timdex_dataset_multi_source):
- batches = timdex_dataset_multi_source.read_dicts_iter(source="not-gonna-find-me")
+ batches = timdex_dataset_multi_source.records.read_dicts_iter(
+ source="not-gonna-find-me"
+ )
with pytest.raises(StopIteration):
next(batches)
def test_read_transformed_records_yields_parsed_dictionary(timdex_dataset_multi_source):
- batches = timdex_dataset_multi_source.read_transformed_records_iter()
+ batches = timdex_dataset_multi_source.records.read_transformed_records_iter()
transformed_record = next(batches)
assert isinstance(transformed_record, dict)
assert transformed_record == {"title": ["Hello World."]}
def test_read_batches_where_filters_response(timdex_dataset_multi_source):
- df_all = timdex_dataset_multi_source.read_dataframe()
+ df_all = timdex_dataset_multi_source.records.read_dataframe()
total_count = len(df_all)
where = (
"source = 'libguides' AND run_date = '2024-12-01' AND "
"run_type = 'daily' AND action = 'index'"
)
- df_where = timdex_dataset_multi_source.read_dataframe(where=where)
+ df_where = timdex_dataset_multi_source.records.read_dataframe(where=where)
assert len(df_where) == 1_000
assert len(df_where) < total_count
@@ -112,7 +130,7 @@ def test_read_batches_where_and_dataset_filters_are_combined(timdex_dataset_mult
"""Test that when key/value DatasetFilters AND a SQL where clause is provided, they
are combined in the final DuckDB SQL query."""
where = "run_date = '2024-12-01' AND run_type = 'daily'"
- df = timdex_dataset_multi_source.read_dataframe(
+ df = timdex_dataset_multi_source.records.read_dataframe(
where=where, source="libguides", action="index"
)
assert len(df) == 1_000
@@ -133,12 +151,12 @@ def test_read_batches_where_rejects_non_predicate_sql(
timdex_dataset_multi_source, bad_where
):
with pytest.raises(ParserException):
- next(timdex_dataset_multi_source.read_batches_iter(where=bad_where))
+ next(timdex_dataset_multi_source.records.read_batches_iter(where=bad_where))
def test_read_dataframe_respects_where(timdex_dataset_multi_source):
where = "source = 'libguides' AND action = 'index'"
- df = timdex_dataset_multi_source.read_dataframe(where=where)
+ df = timdex_dataset_multi_source.records.read_dataframe(where=where)
assert len(df) > 0
assert set(df["source"].unique().tolist()) == {"libguides"}
assert set(df["action"].unique().tolist()) == {"index"}
@@ -146,14 +164,16 @@ def test_read_dataframe_respects_where(timdex_dataset_multi_source):
def test_read_dicts_iter_respects_where_and_filters(timdex_dataset_multi_source):
where = "run_type = 'daily'"
- it = timdex_dataset_multi_source.read_dicts_iter(where=where, source="libguides")
+ it = timdex_dataset_multi_source.records.read_dicts_iter(
+ where=where, source="libguides"
+ )
first = next(it)
assert first["run_type"] == "daily"
assert first["source"] == "libguides"
def test_dataset_all_current_records_deduped(timdex_dataset_with_runs_with_metadata):
- df = timdex_dataset_with_runs_with_metadata.read_dataframe(
+ df = timdex_dataset_with_runs_with_metadata.records.read_dataframe(
table="current_records",
columns=["timdex_record_id"],
)
@@ -162,7 +182,7 @@ def test_dataset_all_current_records_deduped(timdex_dataset_with_runs_with_metad
def test_dataset_source_current_records_deduped(timdex_dataset_with_runs_with_metadata):
- df = timdex_dataset_with_runs_with_metadata.read_dataframe(
+ df = timdex_dataset_with_runs_with_metadata.records.read_dataframe(
table="current_records", source="alma"
)
assert df is not None
@@ -174,17 +194,17 @@ def test_dataset_all_read_methods_get_deduplication(
timdex_dataset_with_runs_with_metadata,
):
batch_rows = 0
- for b in timdex_dataset_with_runs_with_metadata.read_batches_iter(
+ for b in timdex_dataset_with_runs_with_metadata.records.read_batches_iter(
table="current_records", columns=["timdex_record_id"]
):
batch_rows += len(b)
dict_rows = sum(
1
- for _ in timdex_dataset_with_runs_with_metadata.read_dicts_iter(
+ for _ in timdex_dataset_with_runs_with_metadata.records.read_dicts_iter(
table="current_records", columns=["timdex_record_id"]
)
)
- df = timdex_dataset_with_runs_with_metadata.read_dataframe(
+ df = timdex_dataset_with_runs_with_metadata.records.read_dataframe(
table="current_records", columns=["timdex_record_id"]
)
assert df is not None
@@ -195,11 +215,11 @@ def test_dataset_all_read_methods_get_deduplication(
def test_dataset_current_records_no_additional_filtering_accurate_records_yielded(
timdex_dataset_with_runs_with_metadata,
):
- df_all = timdex_dataset_with_runs_with_metadata.read_dataframe(
+ df_all = timdex_dataset_with_runs_with_metadata.records.read_dataframe(
table="current_records"
)
assert df_all is not None
- df_total = timdex_dataset_with_runs_with_metadata.read_dataframe()
+ df_total = timdex_dataset_with_runs_with_metadata.records.read_dataframe()
assert df_total is not None
assert len(df_all) <= len(df_total)
assert df_all["timdex_record_id"].nunique() == len(df_all)
@@ -208,7 +228,7 @@ def test_dataset_current_records_no_additional_filtering_accurate_records_yielde
def test_dataset_current_records_action_filtering_accurate_records_yielded(
timdex_dataset_with_runs_with_metadata,
):
- df = timdex_dataset_with_runs_with_metadata.read_dataframe(
+ df = timdex_dataset_with_runs_with_metadata.records.read_dataframe(
table="current_records", action="index"
)
assert df is not None
@@ -219,14 +239,14 @@ def test_dataset_current_records_index_filtering_accurate_records_yielded(
timdex_dataset_with_runs_with_metadata,
):
# with all records, run-5 has 25 rows
- df_all = timdex_dataset_with_runs_with_metadata.read_dataframe(
+ df_all = timdex_dataset_with_runs_with_metadata.records.read_dataframe(
source="alma", run_id="run-5"
)
assert df_all is not None
assert len(df_all) == 25
# within current_records, only 15 remain due to later deletes
- df_current = timdex_dataset_with_runs_with_metadata.read_dataframe(
+ df_current = timdex_dataset_with_runs_with_metadata.records.read_dataframe(
table="current_records", source="alma", run_id="run-5"
)
assert df_current is not None
@@ -255,7 +275,7 @@ def test_dataset_load_current_records_gets_correct_same_day_full_run(
):
# ensure metadata exists for this dataset
timdex_dataset_same_day_runs.metadata.rebuild_dataset_metadata()
- df = timdex_dataset_same_day_runs.read_dataframe(
+ df = timdex_dataset_same_day_runs.records.read_dataframe(
table="current_records", run_type="full"
)
assert list(df.run_id.unique()) == ["run-2"]
@@ -266,7 +286,7 @@ def test_dataset_load_current_records_gets_correct_same_day_daily_runs_ordering(
):
timdex_dataset_same_day_runs.metadata.rebuild_dataset_metadata()
first_record = next(
- timdex_dataset_same_day_runs.read_dicts_iter(
+ timdex_dataset_same_day_runs.records.read_dicts_iter(
table="current_records", run_type="daily"
)
)
@@ -277,13 +297,13 @@ def test_dataset_load_current_records_gets_correct_same_day_daily_runs_ordering(
def test_read_batches_iter_limit_returns_n_rows(timdex_dataset_multi_source):
- batches = timdex_dataset_multi_source.read_batches_iter(limit=10)
+ batches = timdex_dataset_multi_source.records.read_batches_iter(limit=10)
table = pa.Table.from_batches(batches)
assert len(table) == 10
-def test_read_batches_iter_returns_empty_when_metadata_missing(
- timdex_dataset_empty, caplog
+def test_read_batches_iter_raises_when_metadata_missing(
+ timdex_dataset_empty,
):
with pytest.raises(
ValueError,
@@ -293,15 +313,14 @@ def test_read_batches_iter_returns_empty_when_metadata_missing(
"TIMDEXDataset.metadata.rebuild_dataset_metadata() may be required."
),
):
- list(timdex_dataset_empty.read_batches_iter())
+ list(timdex_dataset_empty.records.read_batches_iter())
-def test_read_batches_iter_returns_empty_for_invalid_table(
- timdex_dataset_multi_source, caplog
+def test_read_batches_iter_raises_for_invalid_table(
+ timdex_dataset_multi_source,
):
- """read_batches_iter returns empty iterator for nonexistent table name."""
with pytest.raises(
ValueError,
match="Invalid table: 'nonexistent'",
):
- list(timdex_dataset_multi_source.read_batches_iter(table="nonexistent"))
+ list(timdex_dataset_multi_source.records.read_batches_iter(table="nonexistent"))
diff --git a/tests/test_records.py b/tests/test_records.py
index 3e3d4db..c960cd4 100644
--- a/tests/test_records.py
+++ b/tests/test_records.py
@@ -3,7 +3,7 @@
import pytest
-from timdex_dataset_api.record import DatasetRecord
+from timdex_dataset_api.records import DatasetRecord
def test_dataset_record_init_with_valid_run_date_parses_year_month_day():
diff --git a/tests/test_write.py b/tests/test_write.py
index 3710989..0cd5839 100644
--- a/tests/test_write.py
+++ b/tests/test_write.py
@@ -1,4 +1,5 @@
# ruff: noqa: PLR2004, D209, D205
+import glob
import math
import os
from pathlib import Path
@@ -8,20 +9,47 @@
import pyarrow.parquet as pq
from tests.utils import generate_sample_records
-from timdex_dataset_api.dataset import (
- TIMDEX_DATASET_SCHEMA,
-)
-from timdex_dataset_api.metadata import ORDERED_METADATA_COLUMN_NAMES
+from timdex_dataset_api.records import TIMDEXRecords
+
+
+def _count_rows_via_duckdb_parquet(timdex_dataset) -> int:
+ return timdex_dataset.conn.query(f"""
+ select count(*)
+ from read_parquet(
+ '{timdex_dataset.records.data_root}/**/*.parquet',
+ hive_partitioning=true
+ )
+ """).fetchone()[0]
+
+
+def _count_parquet_files(timdex_dataset) -> int:
+ return len(
+ glob.glob(
+ f"{timdex_dataset.records.data_root}/**/*.parquet",
+ recursive=True,
+ )
+ )
+
+
+def test_records_data_root_created_on_init(timdex_dataset_empty):
+ expected = f"{timdex_dataset_empty.location.removesuffix('/')}/data/records"
+ assert os.path.exists(expected)
+
+
+def test_embeddings_data_root_created_on_init(timdex_dataset_empty):
+ expected = f"{timdex_dataset_empty.location.removesuffix('/')}/data/embeddings"
+ assert os.path.exists(expected)
+ assert timdex_dataset_empty.embeddings.data_root == expected
def test_dataset_write_records_to_timdex_dataset_empty(
timdex_dataset_empty, sample_records_generator
):
- written_files = timdex_dataset_empty.write(sample_records_generator(10_000))
+ written_files = timdex_dataset_empty.records.write(sample_records_generator(10_000))
assert len(written_files) == 1
assert os.path.exists(timdex_dataset_empty.location)
- assert timdex_dataset_empty.dataset.count_rows() == 10_000
+ assert _count_rows_via_duckdb_parquet(timdex_dataset_empty) == 10_000
def test_dataset_write_default_max_rows_per_file(
@@ -32,33 +60,18 @@ def test_dataset_write_default_max_rows_per_file(
default_max_rows_per_file = timdex_dataset_empty.config.max_rows_per_file
total_records = 200_033
- timdex_dataset_empty.write(sample_records_generator(total_records))
+ timdex_dataset_empty.records.write(sample_records_generator(total_records))
- assert timdex_dataset_empty.dataset.count_rows() == total_records
- assert len(timdex_dataset_empty.dataset.files) == math.ceil(
+ assert _count_rows_via_duckdb_parquet(timdex_dataset_empty) == total_records
+ assert _count_parquet_files(timdex_dataset_empty) == math.ceil(
total_records / default_max_rows_per_file
)
-def test_dataset_write_record_batches_uses_batch_size(
- timdex_dataset_empty, sample_records_generator
-):
- total_records = 101
- timdex_dataset_empty.config.write_batch_size = 50
- batches = list(
- timdex_dataset_empty.create_record_batches(
- sample_records_generator(total_records)
- )
- )
- assert len(batches) == math.ceil(
- total_records / timdex_dataset_empty.config.write_batch_size
- )
-
-
def test_dataset_write_schema_applied_to_dataset(
timdex_dataset_empty, sample_records_generator
):
- timdex_dataset_empty.write(sample_records_generator(10))
+ timdex_dataset_empty.records.write(sample_records_generator(10))
# manually load dataset to confirm schema without TIMDEXDataset projecting schema
# during load
@@ -68,13 +81,13 @@ def test_dataset_write_schema_applied_to_dataset(
partitioning="hive",
)
- assert set(dataset.schema.names) == set(TIMDEX_DATASET_SCHEMA.names)
+ assert set(dataset.schema.names) == set(TIMDEXRecords.SCHEMA.names)
def test_dataset_write_partition_for_single_source(
timdex_dataset_empty, sample_records_generator
):
- written_files = timdex_dataset_empty.write(sample_records_generator(10))
+ written_files = timdex_dataset_empty.records.write(sample_records_generator(10))
assert len(written_files) == 1
assert os.path.exists(timdex_dataset_empty.location)
assert "year=2024/month=12/day=01" in written_files[0].path
@@ -84,35 +97,41 @@ def test_dataset_write_partition_for_multiple_sources(
timdex_dataset_empty, sample_records_generator
):
# perform write for source="alma" and run_date="2024-12-01"
- written_files_source_a = timdex_dataset_empty.write(sample_records_generator(10))
+ written_files_source_a = timdex_dataset_empty.records.write(
+ sample_records_generator(10)
+ )
assert os.path.exists(written_files_source_a[0].path)
- assert timdex_dataset_empty.dataset.count_rows() == 10
+ assert _count_rows_via_duckdb_parquet(timdex_dataset_empty) == 10
# perform write for source="libguides" and run_date="2024-12-01"
- written_files_source_b = timdex_dataset_empty.write(
+ written_files_source_b = timdex_dataset_empty.records.write(
generate_sample_records(num_records=7, source="libguides")
)
assert os.path.exists(written_files_source_b[0].path)
assert os.path.exists(written_files_source_a[0].path)
- assert timdex_dataset_empty.dataset.count_rows() == 17
+ assert _count_rows_via_duckdb_parquet(timdex_dataset_empty) == 17
def test_dataset_write_partition_ignore_existing_data(
timdex_dataset_empty, sample_records_generator
):
# perform two (2) writes for source="alma" and run_date="2024-12-01"
- written_files_source_a0 = timdex_dataset_empty.write(sample_records_generator(10))
- written_files_source_a1 = timdex_dataset_empty.write(sample_records_generator(10))
+ written_files_source_a0 = timdex_dataset_empty.records.write(
+ sample_records_generator(10)
+ )
+ written_files_source_a1 = timdex_dataset_empty.records.write(
+ sample_records_generator(10)
+ )
# assert that both files exist and no overwriting occurs
assert os.path.exists(written_files_source_a0[0].path)
assert os.path.exists(written_files_source_a1[0].path)
- assert timdex_dataset_empty.dataset.count_rows() == 20
+ assert _count_rows_via_duckdb_parquet(timdex_dataset_empty) == 20
-@patch("timdex_dataset_api.dataset.uuid.uuid4")
+@patch("timdex_dataset_api.data_source.uuid.uuid4")
def test_dataset_write_partition_overwrite_files_with_same_name(
mock_uuid, timdex_dataset_empty, sample_records_generator
):
@@ -125,19 +144,24 @@ def test_dataset_write_partition_overwrite_files_with_same_name(
mock_uuid.return_value = "abc"
# perform two (2) writes for source="alma" and run_date="2024-12-01"
- _ = timdex_dataset_empty.write(sample_records_generator(10))
- written_files_source_a1 = timdex_dataset_empty.write(sample_records_generator(7))
+ _ = timdex_dataset_empty.records.write(sample_records_generator(10))
+ written_files_source_a1 = timdex_dataset_empty.records.write(
+ sample_records_generator(7)
+ )
# assert that only the second file exists and overwriting occurs
assert os.path.exists(written_files_source_a1[0].path)
- assert timdex_dataset_empty.dataset.count_rows() == 7
+ assert _count_rows_via_duckdb_parquet(timdex_dataset_empty) == 7
def test_dataset_write_single_append_delta_success(
timdex_dataset_empty, sample_records_generator
):
- written_files = timdex_dataset_empty.write(sample_records_generator(1_000))
- append_deltas = os.listdir(timdex_dataset_empty.metadata.append_deltas_path)
+ written_files = timdex_dataset_empty.records.write(sample_records_generator(1_000))
+ records_deltas_path = timdex_dataset_empty.metadata.append_deltas_path_for(
+ TIMDEXRecords
+ )
+ append_deltas = os.listdir(records_deltas_path)
assert len(append_deltas) == len(written_files)
@@ -149,8 +173,11 @@ def test_dataset_write_multiple_append_deltas_success(
timdex_dataset_empty.config.max_rows_per_file = 100
timdex_dataset_empty.config.max_rows_per_group = 100
- written_files = timdex_dataset_empty.write(sample_records_generator(1_000))
- append_deltas = os.listdir(timdex_dataset_empty.metadata.append_deltas_path)
+ written_files = timdex_dataset_empty.records.write(sample_records_generator(1_000))
+ records_deltas_path = timdex_dataset_empty.metadata.append_deltas_path_for(
+ TIMDEXRecords
+ )
+ append_deltas = os.listdir(records_deltas_path)
assert len(written_files) == 10
assert len(append_deltas) == len(written_files)
@@ -159,12 +186,11 @@ def test_dataset_write_multiple_append_deltas_success(
def test_dataset_write_append_delta_expected_metadata_columns(
timdex_dataset_empty, sample_records_generator
):
- timdex_dataset_empty.write(sample_records_generator(1_000))
- append_delta_filepath = os.listdir(timdex_dataset_empty.metadata.append_deltas_path)[
- 0
- ]
-
- append_delta = pq.ParquetFile(
- timdex_dataset_empty.metadata.append_deltas_path / Path(append_delta_filepath)
+ timdex_dataset_empty.records.write(sample_records_generator(1_000))
+ records_deltas_path = timdex_dataset_empty.metadata.append_deltas_path_for(
+ TIMDEXRecords
)
- assert append_delta.schema.names == ORDERED_METADATA_COLUMN_NAMES
+ append_delta_filepath = os.listdir(records_deltas_path)[0]
+
+ append_delta = pq.ParquetFile(Path(records_deltas_path) / append_delta_filepath)
+ assert append_delta.schema.names == TIMDEXRecords.SOURCE_METADATA_COLUMNS
diff --git a/timdex_dataset_api/__init__.py b/timdex_dataset_api/__init__.py
index a3aed52..67a794f 100644
--- a/timdex_dataset_api/__init__.py
+++ b/timdex_dataset_api/__init__.py
@@ -2,17 +2,21 @@
from importlib.metadata import version
+from timdex_dataset_api.data_source import DataSourceTableConfig, TIMDEXDataSource
from timdex_dataset_api.dataset import TIMDEXDataset
from timdex_dataset_api.embeddings import DatasetEmbedding, TIMDEXEmbeddings
from timdex_dataset_api.metadata import TIMDEXDatasetMetadata
-from timdex_dataset_api.record import DatasetRecord
+from timdex_dataset_api.records import DatasetRecord, TIMDEXRecords
__version__ = version("timdex_dataset_api")
__all__ = [
+ "DataSourceTableConfig",
"DatasetEmbedding",
"DatasetRecord",
+ "TIMDEXDataSource",
"TIMDEXDataset",
"TIMDEXDatasetMetadata",
"TIMDEXEmbeddings",
+ "TIMDEXRecords",
]
diff --git a/timdex_dataset_api/data_source.py b/timdex_dataset_api/data_source.py
new file mode 100644
index 0000000..77e5b1d
--- /dev/null
+++ b/timdex_dataset_api/data_source.py
@@ -0,0 +1,548 @@
+"""timdex_dataset_api/data_source.py
+
+Abstract base class for TIMDEX data sources (records, embeddings, etc.).
+
+Shared read/write orchestration lives here; subclasses provide schema definitions,
+column contracts, and domain-specific hooks.
+"""
+
+import itertools
+import logging
+import time
+import uuid
+from abc import ABC
+from collections.abc import Iterator
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import TYPE_CHECKING, Any, ClassVar, Literal, Protocol, runtime_checkable
+
+import pandas as pd
+import pyarrow as pa
+import pyarrow.dataset as ds
+
+from timdex_dataset_api.metadata import TIMDEXDatasetMetadata
+
+if TYPE_CHECKING:
+ from timdex_dataset_api.dataset import TIMDEXDataset
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class DataSourceTableConfig:
+ """Unified definition of a readable metadata-backed table or view."""
+
+ # DuckDB table or view name, e.g. 'current_records'.
+ name: str
+
+ # Human-readable explanation of what this table contains.
+ description: str
+
+ # Whether this is a base source table or a custom metadata view.
+ kind: Literal["base", "custom"]
+
+ # DuckDB SQL to define custom projection
+ query_sql: str | None = None
+
+ # List of metadata schema tables this relies on
+ required_metadata_tables: list[str] = field(default_factory=list)
+
+ # Deprecated: to be removed
+ preload_setting_attribute: str | None = None
+
+
+@runtime_checkable
+class DataSourceRow(Protocol):
+ """Protocol for row objects that can be written to a data source."""
+
+ def to_dict(self) -> dict: ...
+
+
+class TIMDEXDataSource(ABC):
+ """Abstract base class for TIMDEX data sources.
+
+ Provides shared write, read, and column-contract logic. Subclasses must
+ define their schema and column contract class variables; metadata
+ configuration is derived in ``__init_subclass__``.
+ """
+
+ # ------------------------------------------------------------------ #
+ # Required sub-class class vars
+ # ------------------------------------------------------------------ #
+
+ # Short identifier, e.g. "records", "embeddings", etc.
+ NAME: ClassVar[str]
+
+ # Full pyarrow schema for parquet files of this data source
+ SCHEMA: ClassVar[pa.Schema]
+
+ # Location of data parquet files, e.g. "data/records"
+ DATA_PATH: ClassVar[str]
+
+ # Heavy/data columns read from parquet data files
+ DATA_COLUMNS: ClassVar[list[str]]
+
+ # Tables this data source exposes for reading.
+ TABLES: ClassVar[list[DataSourceTableConfig]] = []
+
+ # ------------------------------------------------------------------ #
+ # Optional sub-class class vars
+ # ------------------------------------------------------------------ #
+
+ # Hive-style partition columns (e.g. ``["year", "month", "day"]``)
+ PARTITION_COLUMNS: ClassVar[list[str]] = [
+ "year",
+ "month",
+ "day",
+ ]
+
+ # Composite key columns used when joining metadata to parquet data.
+ # filename is always included to physically disambiguate rows that share
+ # the same logical key but reside in different parquet files (common for
+ # bolt-on data sources like embeddings).
+ JOIN_KEYS: ClassVar[list[str]] = [
+ "timdex_record_id",
+ "run_id",
+ "run_record_offset",
+ "filename",
+ ]
+
+ # If True, metadata views are pre-joined to records for base record columns
+ PREJOIN_RECORDS: ClassVar[bool] = True
+
+ # ------------------------------------------------------------------ #
+ # Derived class vars
+ # ------------------------------------------------------------------ #
+ METADATA_COLUMNS: ClassVar[list[str]]
+ SOURCE_METADATA_COLUMNS: ClassVar[list[str]]
+ AVAILABLE_READ_COLUMNS: ClassVar[list[str]]
+
+ def __init_subclass__(cls, **kwargs: object) -> None:
+ """Build dynamic class variables for class."""
+ super().__init_subclass__(**kwargs)
+
+ # validate that child class satisfies TIMDEXDataSource requirements
+ required_class_vars = [
+ "NAME",
+ "SCHEMA",
+ "PARTITION_COLUMNS",
+ "DATA_COLUMNS",
+ "DATA_PATH",
+ "TABLES",
+ ]
+ missing_class_vars = [
+ var_name for var_name in required_class_vars if not hasattr(cls, var_name)
+ ]
+ if missing_class_vars:
+ missing = ", ".join(missing_class_vars)
+ raise TypeError(f"{cls.__name__} must define required class vars: {missing}")
+
+ schema_metadata_columns = [
+ column_name
+ for column_name in cls.SCHEMA.names
+ if column_name not in cls.DATA_COLUMNS
+ and column_name not in cls.PARTITION_COLUMNS
+ ]
+
+ cls.METADATA_COLUMNS = list(
+ dict.fromkeys(
+ TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS + schema_metadata_columns
+ )
+ )
+
+ cls.AVAILABLE_READ_COLUMNS = list(
+ dict.fromkeys(cls.METADATA_COLUMNS + cls.DATA_COLUMNS)
+ )
+
+ if cls.PREJOIN_RECORDS:
+ cls.SOURCE_METADATA_COLUMNS = [
+ column_name
+ for column_name in cls.METADATA_COLUMNS
+ if column_name not in TIMDEXDatasetMetadata.PREJOIN_RECORDS_COLUMNS
+ ]
+ else:
+ cls.SOURCE_METADATA_COLUMNS = cls.METADATA_COLUMNS
+
+ def __init__(self, timdex_dataset: "TIMDEXDataset") -> None:
+ """Instance instantiation; runs after sub-class instantiation."""
+ self.timdex_dataset = timdex_dataset
+ self.schema = self.SCHEMA
+ self.partition_columns = self.PARTITION_COLUMNS
+ self._ensure_data_root_exists()
+
+ @property
+ def data_root(self) -> str:
+ """Root path for this source's parquet data."""
+ return f"{self.timdex_dataset.location.removesuffix('/')}/{self.DATA_PATH}"
+
+ @property
+ def default_table(self) -> str:
+ """Default table name for read methods."""
+ return self.NAME
+
+ def create_data_structure(self) -> None:
+ """Ensure source data root exists (idempotent for local datasets)."""
+ self._ensure_data_root_exists()
+
+ def _ensure_data_root_exists(self) -> None:
+ """Ensure local data root directory exists for this source."""
+ if self.timdex_dataset.location_scheme != "file":
+ return
+ Path(self.data_root).mkdir(parents=True, exist_ok=True)
+
+ # ------------------------------------------------------------------ #
+ # Write pipeline
+ # ------------------------------------------------------------------ #
+
+ def write(
+ self,
+ rows_iter: Iterator[DataSourceRow],
+ *,
+ use_threads: bool = True,
+ write_append_deltas: bool = True,
+ ) -> list[ds.WrittenFile]:
+ """Write rows to this data source's parquet dataset.
+
+ Args:
+ rows_iter: iterator of row objects (DatasetRecord, DatasetEmbedding, etc.)
+ each must implement ``.to_dict()``
+ use_threads: use threads for writing
+ write_append_deltas: write append deltas for metadata tracking
+ """
+ start_time = time.perf_counter()
+ written_files: list[ds.WrittenFile] = []
+
+ filesystem, path = self.timdex_dataset.parse_location(self.data_root)
+
+ batches_iter = self._create_batches(rows_iter)
+ ds.write_dataset(
+ batches_iter,
+ base_dir=path,
+ basename_template="%s-{i}.parquet" % (str(uuid.uuid4())), # noqa: UP031
+ existing_data_behavior="overwrite_or_ignore",
+ filesystem=filesystem,
+ file_visitor=lambda written_file: written_files.append(written_file), # type: ignore[arg-type] # noqa: PLW0108
+ format="parquet",
+ max_open_files=500,
+ max_rows_per_file=self.timdex_dataset.config.max_rows_per_file,
+ max_rows_per_group=self.timdex_dataset.config.max_rows_per_group,
+ partitioning=self.partition_columns,
+ partitioning_flavor="hive",
+ schema=self.schema,
+ use_threads=use_threads,
+ )
+
+ # write metadata append deltas
+ if write_append_deltas:
+ for written_file in written_files:
+ self.timdex_dataset.metadata.write_append_delta(
+ written_file.path, # type: ignore[attr-defined]
+ type(self),
+ )
+ self.timdex_dataset.refresh()
+
+ self.log_write_statistics(start_time, written_files)
+
+ return written_files
+
+ def _create_batches(
+ self,
+ rows_iter: Iterator[DataSourceRow],
+ ) -> Iterator[pa.RecordBatch]:
+ """Yield ``pyarrow.RecordBatch`` objects from an iterator of row objects."""
+ for i, batch in enumerate(
+ itertools.batched(rows_iter, self.timdex_dataset.config.write_batch_size)
+ ):
+ row_dicts = [row.to_dict() for row in batch]
+ record_batch = pa.RecordBatch.from_pylist(row_dicts)
+ logger.debug(f"Yielding batch {i + 1} for dataset writing.")
+ yield record_batch
+
+ def log_write_statistics(
+ self,
+ start_time: float,
+ written_files: list[ds.WrittenFile],
+ ) -> None:
+ """Parse written files from write and log statistics."""
+ total_time = round(time.perf_counter() - start_time, 2)
+ total_files = len(written_files)
+ total_rows = sum(
+ [wf.metadata.num_rows for wf in written_files] # type: ignore[attr-defined]
+ )
+ total_size = sum([wf.size for wf in written_files]) # type: ignore[attr-defined]
+ logger.info(
+ f"Dataset write complete - elapsed: "
+ f"{total_time}s, "
+ f"total files: {total_files}, "
+ f"total rows: {total_rows}, "
+ f"total size: {total_size}"
+ )
+
+ # ------------------------------------------------------------------ #
+ # Read pipeline
+ # ------------------------------------------------------------------ #
+
+ def read_batches_iter(
+ self,
+ table: str | None = None,
+ columns: list[str] | None = None,
+ limit: int | None = None,
+ where: str | None = None,
+ **filters: Any, # noqa: ANN401
+ ) -> Iterator[pa.RecordBatch]:
+ """Yield rows as ``pyarrow.RecordBatch`` via metadata-driven two-step reads.
+
+ Args:
+ table: DuckDB table/view name (defaults to ``self.default_table``)
+ columns: columns to return (defaults to ``AVAILABLE_READ_COLUMNS``)
+ limit: max rows to yield
+ where: raw SQL WHERE predicate
+ **filters: key/value filter pairs
+ """
+ start_time = time.perf_counter()
+ table = table or self.default_table
+
+ valid_table_names = {table_config.name for table_config in self.TABLES}
+ if table not in valid_table_names:
+ valid = ", ".join(
+ f"'{table_config.name}' ({table_config.description})"
+ for table_config in self.TABLES
+ )
+ raise ValueError(f"Invalid table: '{table}'. Valid tables: {valid}")
+
+ try:
+ self.timdex_dataset.get_sa_table("metadata", table)
+ except ValueError as exc:
+ raise ValueError(
+ f"Table '{table}' not found in DuckDB context. If this is a new "
+ f"dataset, either {self.NAME} do not yet exist or a "
+ "TIMDEXDataset.metadata.rebuild_dataset_metadata() may be required."
+ ) from exc
+
+ temp_table_name = "read_meta_chunk"
+ total_yield_count = 0
+ metadata_columns = self.timdex_dataset.metadata.get_metadata_columns_for_table(
+ table
+ )
+
+ meta_chunks = self._iter_meta_chunks(
+ table,
+ limit=limit,
+ where=where,
+ **filters,
+ )
+ for i, meta_chunk_df in enumerate(meta_chunks):
+ batch_time = time.perf_counter()
+ batch_yield_count = len(meta_chunk_df)
+ total_yield_count += batch_yield_count
+
+ self.timdex_dataset.conn.register(
+ temp_table_name,
+ meta_chunk_df[metadata_columns],
+ )
+
+ try:
+ data_query = self._build_data_query_for_chunk(
+ columns,
+ meta_chunk_df,
+ registered_metadata_chunk=temp_table_name,
+ )
+ yield from self._iter_data_chunks(data_query)
+ finally:
+ self.timdex_dataset.conn.unregister(temp_table_name)
+
+ batch_rps = int(batch_yield_count / (time.perf_counter() - batch_time))
+ logger.debug(
+ f"read_batches_iter batch {i + 1}, "
+ f"yielded: {batch_yield_count} "
+ f"@ {batch_rps} records/second, "
+ f"total yielded: {total_yield_count}"
+ )
+
+ logger.debug(
+ f"read_batches_iter() elapsed: {round(time.perf_counter() - start_time, 2)}s"
+ )
+
+ def _iter_meta_chunks(
+ self,
+ table: str | None = None,
+ limit: int | None = None,
+ where: str | None = None,
+ **filters: Any, # noqa: ANN401
+ ) -> Iterator[pd.DataFrame]:
+ """Yield pandas DataFrames of metadata query results via keyset pagination."""
+ table = table or self.default_table
+ chunk_size = self.timdex_dataset.config.duckdb_join_batch_size
+
+ keyset_value = (0, 0, 0)
+
+ total_yielded = 0
+ while True:
+ if limit is not None:
+ remaining = limit - total_yielded
+ if remaining <= 0:
+ break
+ chunk_limit = min(chunk_size, remaining)
+ else:
+ chunk_limit = chunk_size
+
+ meta_query = (
+ self.timdex_dataset.metadata.build_keyset_paginated_metadata_query(
+ table,
+ limit=chunk_limit,
+ where=where,
+ keyset_value=keyset_value,
+ **filters,
+ )
+ )
+ meta_chunk_df = self.timdex_dataset.conn.query(meta_query).to_df()
+
+ meta_chunk_count = len(meta_chunk_df)
+
+ if meta_chunk_count == 0:
+ break
+
+ total_yielded += meta_chunk_count
+ yield meta_chunk_df
+
+ last_row = meta_chunk_df.iloc[-1]
+ keyset_value = (
+ int(last_row.filename_hash),
+ int(last_row.run_id_hash),
+ int(last_row.run_record_offset),
+ )
+
+ def _build_data_query_for_chunk(
+ self,
+ columns: list[str] | None,
+ meta_chunk_df: pd.DataFrame,
+ registered_metadata_chunk: str = "meta_chunk",
+ ) -> str:
+ """Build SQL query for data retrieval, joining metadata chunk to parquet."""
+ metadata_columns = self.METADATA_COLUMNS
+
+ requested_columns = columns or self.AVAILABLE_READ_COLUMNS
+ invalid_columns = set(requested_columns) - set(self.AVAILABLE_READ_COLUMNS)
+ if invalid_columns:
+ invalid = ", ".join(sorted(invalid_columns))
+ raise ValueError(f"Invalid column: {invalid}")
+
+ select_parts: list[str] = []
+ for column_name in requested_columns:
+ if column_name in metadata_columns:
+ select_parts.append(f"mc.{column_name}")
+ continue
+ if column_name in self.DATA_COLUMNS:
+ select_parts.append(f"ds.{column_name}")
+
+ select_cols = ",".join(select_parts)
+
+ filenames = list(meta_chunk_df["filename"].unique())
+ if self.timdex_dataset.location_scheme == "s3":
+ filenames = [
+ f"s3://{f.removeprefix('s3://')}"
+ for f in filenames # type: ignore[union-attr]
+ ]
+ parquet_list_sql = "[" + ",".join(f"'{f}'" for f in filenames) + "]"
+
+ rro_values = meta_chunk_df["run_record_offset"].unique()
+ rro_values.sort()
+ if len(rro_values) <= 1_000: # noqa: PLR2004
+ rro_clause = (
+ f"and run_record_offset in ({','.join(str(rro) for rro in rro_values)})"
+ )
+ else:
+ rro_clause = (
+ f"and run_record_offset between {rro_values[0]} and {rro_values[-1]}"
+ )
+
+ join_keys = ", ".join(self.JOIN_KEYS)
+
+ return f"""
+ select
+ {select_cols}
+ from read_parquet(
+ {parquet_list_sql},
+ hive_partitioning=true,
+ filename=true
+ ) as ds
+ inner join {registered_metadata_chunk} mc using (
+ {join_keys}
+ )
+ where true
+ {rro_clause};
+ """
+
+ def _iter_data_chunks(self, data_query: str) -> Iterator[pa.RecordBatch]:
+ """Execute data query and stream ``pyarrow.RecordBatch`` results."""
+ if self.timdex_dataset.location_scheme == "s3":
+ self.timdex_dataset.conn.execute("""set threads=16;""")
+ try:
+ cursor = self.timdex_dataset.conn.execute(data_query)
+ yield from cursor.to_arrow_reader(
+ batch_size=self.timdex_dataset.config.read_batch_size
+ )
+ finally:
+ if self.timdex_dataset.location_scheme == "s3":
+ self.timdex_dataset.conn.execute(
+ f"""set threads={self.timdex_dataset.conn_factory.threads};"""
+ )
+
+ def read_dataframes_iter(
+ self,
+ table: str | None = None,
+ columns: list[str] | None = None,
+ limit: int | None = None,
+ where: str | None = None,
+ **filters: Any, # noqa: ANN401
+ ) -> Iterator[pd.DataFrame]:
+ """Yield rows as pandas DataFrames."""
+ for record_batch in self.read_batches_iter(
+ table=table or self.default_table,
+ columns=columns,
+ limit=limit,
+ where=where,
+ **filters,
+ ):
+ yield record_batch.to_pandas()
+
+ def read_dataframe(
+ self,
+ table: str | None = None,
+ columns: list[str] | None = None,
+ limit: int | None = None,
+ where: str | None = None,
+ **filters: Any, # noqa: ANN401
+ ) -> pd.DataFrame | None:
+ """Read all matching rows into a single pandas DataFrame."""
+ df_batches = [
+ record_batch.to_pandas()
+ for record_batch in self.read_batches_iter(
+ table=table or self.default_table,
+ columns=columns,
+ limit=limit,
+ where=where,
+ **filters,
+ )
+ ]
+ if not df_batches:
+ return None
+ return pd.concat(df_batches)
+
+ def read_dicts_iter(
+ self,
+ table: str | None = None,
+ columns: list[str] | None = None,
+ limit: int | None = None,
+ where: str | None = None,
+ **filters: Any, # noqa: ANN401
+ ) -> Iterator[dict]:
+ """Yield rows as Python dicts."""
+ for record_batch in self.read_batches_iter(
+ table=table or self.default_table,
+ columns=columns,
+ limit=limit,
+ where=where,
+ **filters,
+ ):
+ yield from record_batch.to_pylist()
diff --git a/timdex_dataset_api/dataset.py b/timdex_dataset_api/dataset.py
index 66da101..564fdaa 100644
--- a/timdex_dataset_api/dataset.py
+++ b/timdex_dataset_api/dataset.py
@@ -1,72 +1,24 @@
"""timdex_dataset_api/dataset.py"""
-import itertools
-import json
import os
import time
-import uuid
-from collections.abc import Iterator
from dataclasses import dataclass, field
-from datetime import date, datetime
-from pathlib import Path
-from typing import TYPE_CHECKING, Literal, TypedDict, Unpack
+from typing import Literal
from urllib.parse import urlparse
import boto3
-import pandas as pd
-import pyarrow as pa
-import pyarrow.dataset as ds
from duckdb_engine import ConnectionWrapper
from pyarrow import fs
from sqlalchemy import MetaData, Table, create_engine
-from sqlalchemy.types import ARRAY, FLOAT
from timdex_dataset_api.config import configure_logger
from timdex_dataset_api.embeddings import TIMDEXEmbeddings
from timdex_dataset_api.metadata import TIMDEXDatasetMetadata
+from timdex_dataset_api.records import TIMDEXRecords
from timdex_dataset_api.utils import DuckDBConnectionFactory
-if TYPE_CHECKING:
- from timdex_dataset_api.record import DatasetRecord # pragma: nocover
-
-
logger = configure_logger(__name__)
-TIMDEX_DATASET_SCHEMA = pa.schema(
- (
- pa.field("timdex_record_id", pa.string()),
- pa.field("source_record", pa.binary()),
- pa.field("transformed_record", pa.binary()),
- pa.field("source", pa.string()),
- pa.field("run_date", pa.date32()),
- pa.field("run_type", pa.string()),
- pa.field("action", pa.string()),
- pa.field("run_id", pa.string()),
- pa.field("run_record_offset", pa.int32()),
- pa.field("year", pa.string()),
- pa.field("month", pa.string()),
- pa.field("day", pa.string()),
- pa.field("run_timestamp", pa.timestamp("us", tz="UTC")),
- )
-)
-
-TIMDEX_DATASET_PARTITION_COLUMNS = [
- "year",
- "month",
- "day",
-]
-
-
-class DatasetFilters(TypedDict, total=False):
- timdex_record_id: str | list[str] | None
- source: str | list[str] | None
- run_date: str | date | list[str | date] | None
- run_type: str | list[str] | None
- action: str | list[str] | None
- run_id: str | list[str] | None
- run_record_offset: int | list[int] | None
- run_timestamp: str | datetime | list[str | datetime] | None
-
@dataclass
class TIMDEXDatasetConfig:
@@ -111,6 +63,8 @@ class TIMDEXDatasetConfig:
class TIMDEXDataset:
+ """Class to represent the TIMDEXDataset."""
+
def __init__(
self,
location: str,
@@ -131,13 +85,6 @@ def __init__(
self.location = location
self.preload_current_records = preload_current_records
- self.create_data_structure()
-
- # pyarrow dataset
- self.schema = TIMDEX_DATASET_SCHEMA
- self.partition_columns = TIMDEX_DATASET_PARTITION_COLUMNS
- self.dataset = self.load_pyarrow_dataset()
-
# create DuckDB connection used by all classes
self.conn_factory = DuckDBConnectionFactory(location_scheme=self.location_scheme)
self.conn = self.conn_factory.create_connection()
@@ -145,7 +92,17 @@ def __init__(
# create schemas
self._create_duckdb_schemas()
+ self.source_classes = [TIMDEXRecords, TIMDEXEmbeddings]
+
+ # define readable metadata-backed tables contributed by data sources
+ self.table_configs = [
+ table_config
+ for source_class in self.source_classes
+ for table_config in source_class.TABLES
+ ]
+
# composed components receive self
+ self.records = TIMDEXRecords(self)
self.metadata = TIMDEXDatasetMetadata(self)
self.embeddings = TIMDEXEmbeddings(self)
@@ -162,10 +119,6 @@ def location_scheme(self) -> Literal["file", "s3"]:
return "s3"
raise ValueError(f"Location with scheme type '{scheme}' not supported.")
- @property
- def data_records_root(self) -> str:
- return f"{self.location.removesuffix('/')}/data/records" # type: ignore[union-attr]
-
def refresh(self) -> None:
"""Refresh dataset by fully reinitializing."""
self.__init__( # type: ignore[misc]
@@ -174,47 +127,6 @@ def refresh(self) -> None:
preload_current_records=self.preload_current_records,
)
- def create_data_structure(self) -> None:
- """Ensure ETL records data structure exists in TIMDEX dataset."""
- if self.location_scheme == "file":
- Path(self.data_records_root).mkdir(
- parents=True,
- exist_ok=True,
- )
-
- def load_pyarrow_dataset(self, parquet_files: list[str] | None = None) -> ds.Dataset:
- """Lazy load a pyarrow.dataset.Dataset.
-
- The dataset is loaded via the expected schema as defined by module constant
- TIMDEX_DATASET_SCHEMA. If the target dataset differs in any way, errors may be
- raised when reading or writing data.
-
- Args:
- parquet_files: explicit list of parquet files to construct pyarrow dataset
- """
- start_time = time.perf_counter()
-
- # get pyarrow filesystem and dataset path basesd on self.location
- filesystem, path = self.parse_location(self.data_records_root)
-
- # set source for pyarrow dataset
- source: str | list[str] = parquet_files or path
-
- dataset = ds.dataset(
- source,
- schema=self.schema,
- format="parquet",
- partitioning="hive",
- filesystem=filesystem,
- )
-
- logger.info(
- f"Dataset successfully loaded: '{self.data_records_root}', "
- f"{round(time.perf_counter() - start_time, 2)}s"
- )
-
- return dataset
-
def parse_location(
self,
location: str,
@@ -257,7 +169,6 @@ def get_s3_filesystem() -> fs.FileSystem:
def _create_duckdb_schemas(self) -> None:
"""Create DuckDB schemas used by all components."""
self.conn.execute("create schema metadata;")
- self.conn.execute("create schema data;")
def reflect_sa_tables(self, schemas: list[str] | None = None) -> None:
"""Reflect SQLAlchemy metadata for DuckDB schemas.
@@ -266,10 +177,10 @@ def reflect_sa_tables(self, schemas: list[str] | None = None) -> None:
are stored in self.sa_tables as {schema: {table_name: Table}}.
Args:
- schemas: list of schemas to reflect; defaults to ["metadata", "data"]
+ schemas: list of schemas to reflect; defaults to ["metadata"]
"""
start_time = time.perf_counter()
- schemas = schemas or ["metadata", "data"]
+ schemas = schemas or ["metadata"]
engine = create_engine(
"duckdb://",
@@ -280,16 +191,11 @@ def reflect_sa_tables(self, schemas: list[str] | None = None) -> None:
db_metadata = MetaData()
db_metadata.reflect(bind=engine, schema=schema, views=True)
- # store tables in flat dict keyed by table name (without schema prefix)
self.sa_tables[schema] = {
table_name.removeprefix(f"{schema}."): table
for table_name, table in db_metadata.tables.items()
}
- # type fixup for embedding_vector column (DuckDB LIST -> SA ARRAY)
- if "embeddings" in self.sa_tables.get("data", {}):
- self.sa_tables["data"]["embeddings"].c.embedding_vector.type = ARRAY(FLOAT)
-
logger.debug(
f"SQLAlchemy reflection complete for schemas {schemas}, "
f"{round(time.perf_counter() - start_time, 3)}s"
@@ -302,400 +208,3 @@ def get_sa_table(self, schema: str, table: str) -> Table:
if table not in self.sa_tables[schema]:
raise ValueError(f"Table '{table}' not found in schema '{schema}'.")
return self.sa_tables[schema][table]
-
- def write(
- self,
- records_iter: Iterator["DatasetRecord"],
- *,
- use_threads: bool = True,
- write_append_deltas: bool = True,
- ) -> list[ds.WrittenFile]:
- """Write records to the TIMDEX parquet dataset.
-
- This method expects an iterator of DatasetRecord instances.
-
- This method encapsulates all dataset writing mechanics and performance
- optimizations (e.g. batching) so that the calling context can focus on yielding
- data.
-
- This method uses the configuration existing_data_behavior="overwrite_or_ignore",
- which will ignore any existing data and will overwrite files with the same name
- as the parquet file. Since a UUID is generated for each write via the
- basename_template, this effectively makes a write idempotent to the
- TIMDEX dataset.
-
- A max_open_files=500 configuration is set to avoid AWS S3 503 error "SLOW_DOWN"
- if too many PutObject calls are made in parallel. Testing suggests this does not
- substantially slow down the overall write.
-
- Args:
- - records_iter: Iterator of DatasetRecord instances
- - use_threads: boolean if threads should be used for writing
- - write_append_deltas: boolean if append deltas should be written for records
- written during write
- """
- start_time = time.perf_counter()
- written_files: list[ds.WrittenFile] = []
-
- filesystem, path = self.parse_location(self.data_records_root)
-
- # write ETL parquet records
- record_batches_iter = self.create_record_batches(records_iter)
- ds.write_dataset(
- record_batches_iter,
- base_dir=path,
- basename_template="%s-{i}.parquet" % (str(uuid.uuid4())), # noqa: UP031
- existing_data_behavior="overwrite_or_ignore",
- filesystem=filesystem,
- file_visitor=lambda written_file: written_files.append(written_file), # type: ignore[arg-type] # noqa: PLW0108
- format="parquet",
- max_open_files=500,
- max_rows_per_file=self.config.max_rows_per_file,
- max_rows_per_group=self.config.max_rows_per_group,
- partitioning=self.partition_columns,
- partitioning_flavor="hive",
- schema=self.schema,
- use_threads=use_threads,
- )
-
- # refresh dataset files
- self.dataset = self.load_pyarrow_dataset()
-
- # write metadata append deltas
- if write_append_deltas:
- for written_file in written_files:
- self.metadata.write_append_delta_duckdb(written_file.path) # type: ignore[attr-defined]
- self.refresh()
-
- self.log_write_statistics(start_time, written_files)
-
- return written_files
-
- def create_record_batches(
- self, records_iter: Iterator["DatasetRecord"]
- ) -> Iterator[pa.RecordBatch]:
- """Yield pyarrow.RecordBatches for writing.
-
- This method expects an iterator of DatasetRecord instances.
-
- Each DatasetRecord is serialized to a dictionary, any column data shared by all
- rows is added to the record, and then added to a pyarrow.RecordBatch for writing.
-
- Args:
- - records_iter: Iterator of DatasetRecord instances
- """
- for i, record_batch in enumerate(
- itertools.batched(records_iter, self.config.write_batch_size)
- ):
- record_dicts = [record.to_dict() for record in record_batch]
- batch = pa.RecordBatch.from_pylist(record_dicts)
- logger.debug(f"Yielding batch {i + 1} for dataset writing.")
- yield batch
-
- def log_write_statistics(
- self,
- start_time: float,
- written_files: list[ds.WrittenFile],
- ) -> None:
- """Parse written files from write and log statistics."""
- total_time = round(time.perf_counter() - start_time, 2)
- total_files = len(written_files)
- total_rows = sum(
- [wf.metadata.num_rows for wf in written_files] # type: ignore[attr-defined]
- )
- total_size = sum([wf.size for wf in written_files]) # type: ignore[attr-defined]
- logger.info(
- f"Dataset write complete - elapsed: "
- f"{total_time}s, "
- f"total files: {total_files}, "
- f"total rows: {total_rows}, "
- f"total size: {total_size}"
- )
-
- def read_batches_iter(
- self,
- table: str = "records",
- columns: list[str] | None = None,
- limit: int | None = None,
- where: str | None = None,
- **filters: Unpack[DatasetFilters],
- ) -> Iterator[pa.RecordBatch]:
- """Yield ETL records as pyarrow.RecordBatches.
-
- This is the base read method. All read methods eventually drop down and use this
- for streaming batches of records. This method performs a two-step process:
-
- 1. Perform a "metadata" query that narrows down records and physical parquet
- files to read from.
- 2. Perform a "data" query that retrieves actual rows, joining the metadata
- information to increase efficiency.
-
- More detail can be found here: docs/reading.md
-
- Args:
- - table: an available DuckDB view or table
- - columns: list of columns to return
- - limit: limit number of records yielded
- - where: raw SQL WHERE clause that can be used alone, or in combination with
- key/value DatasetFilters
- - filters: simple filtering based on key/value pairs from DatasetFilters
- """
- start_time = time.perf_counter()
-
- # ensure valid table
- if table not in ["records", "current_records"]:
- raise ValueError(f"Invalid table: '{table}'")
-
- # ensure table exists
- try:
- self.get_sa_table("metadata", table)
- except ValueError as exc:
- raise ValueError(
- f"Table '{table}' not found in DuckDB context. If this is a new "
- "dataset, either records do not yet exist or a "
- "TIMDEXDataset.metadata.rebuild_dataset_metadata() may be required."
- ) from exc
-
- temp_table_name = "read_meta_chunk"
- total_yield_count = 0
-
- meta_chunks = self._iter_meta_chunks(
- table,
- limit=limit,
- where=where,
- **filters,
- )
- for i, meta_chunk_df in enumerate(meta_chunks):
- batch_time = time.perf_counter()
- batch_yield_count = len(meta_chunk_df)
- total_yield_count += batch_yield_count
-
- self.conn.register(
- temp_table_name,
- meta_chunk_df[
- [
- "timdex_record_id",
- "run_id",
- "run_record_offset",
- ]
- ],
- )
-
- # build and perform data query, yield records
- # set in try/finally block to ensure we always deregister the meta table
- try:
- data_query = self._build_data_query_for_chunk(
- columns,
- meta_chunk_df,
- registered_metadata_chunk=temp_table_name,
- )
- yield from self._iter_data_chunks(data_query)
- finally:
- self.conn.unregister(temp_table_name)
-
- batch_rps = int(batch_yield_count / (time.perf_counter() - batch_time))
- logger.debug(
- f"read_batches_iter batch {i + 1}, yielded: {batch_yield_count} "
- f"@ {batch_rps} records/second, total yielded: {total_yield_count}"
- )
-
- logger.debug(
- f"read_batches_iter() elapsed: {round(time.perf_counter() - start_time, 2)}s"
- )
-
- def _iter_meta_chunks(
- self,
- table: str = "records",
- limit: int | None = None,
- where: str | None = None,
- **filters: Unpack[DatasetFilters],
- ) -> Iterator[pd.DataFrame]:
- """Utility method to yield pandas Dataframe chunks of metadata query results.
-
- The approach here is to use "keyset" pagination, which means each paged result
- is a greater-than (>) check against a tuple of ordered values from the previous
- chunk. This is more performant than a LIMIT + OFFSET.
- """
- # use duckdb_join_batch_size as the chunk size for keyset pagination
- chunk_size = self.config.duckdb_join_batch_size
-
- # init keyset value of zeros to begin with
- keyset_value = (0, 0, 0)
-
- total_yielded = 0
- while True:
- # enforce limit if passed
- if limit is not None:
- remaining = limit - total_yielded
- if remaining <= 0:
- break
- chunk_limit = min(chunk_size, remaining)
- else:
- chunk_limit = chunk_size
-
- # perform chunk query and convert to pyarrow Table
- meta_query = self.metadata.build_keyset_paginated_metadata_query(
- table,
- limit=chunk_limit, # pass chunk_limit instead of limit
- where=where,
- keyset_value=keyset_value,
- **filters,
- )
- meta_chunk_df = self.metadata.conn.query(meta_query).to_df()
-
- meta_chunk_count = len(meta_chunk_df)
-
- # an empty chunk signals end of pagination
- if meta_chunk_count == 0:
- break
-
- # yield this chunk of data
- total_yielded += meta_chunk_count
- yield meta_chunk_df[
- [
- "timdex_record_id",
- "run_id",
- "run_record_offset",
- "filename",
- ]
- ]
-
- # update keyset value using the last row from this chunk
- last_row = meta_chunk_df.iloc[-1]
- keyset_value = (
- int(last_row.filename_hash),
- int(last_row.run_id_hash),
- int(last_row.run_record_offset),
- )
-
- def _build_data_query_for_chunk(
- self,
- columns: list[str] | None,
- meta_chunk_df: pd.DataFrame,
- registered_metadata_chunk: str = "meta_chunk",
- ) -> str:
- """Build SQL query used for data retrieval, joining on passed metadata data."""
- # build select columns
- select_cols = ",".join(
- [f"ds.{col}" for col in (columns or TIMDEX_DATASET_SCHEMA.names)]
- )
-
- # build list of explicit parquet files to read from
- filenames = list(meta_chunk_df["filename"].unique())
- if self.location_scheme == "s3":
- filenames = [
- f"s3://{f.removeprefix('s3://')}"
- for f in filenames # type: ignore[union-attr]
- ]
- parquet_list_sql = "[" + ",".join(f"'{f}'" for f in filenames) + "]"
-
- # build run_record_offset WHERE clause to leverage row group pruning
- rro_values = meta_chunk_df["run_record_offset"].unique()
- rro_values.sort()
- if len(rro_values) <= 1_000: # noqa: PLR2004
- rro_clause = (
- f"and run_record_offset in ({','.join(str(rro) for rro in rro_values)})"
- )
- else:
- rro_clause = (
- f"and run_record_offset between {rro_values[0]} and {rro_values[-1]}"
- )
-
- return f"""
- select
- {select_cols}
- from read_parquet(
- {parquet_list_sql},
- hive_partitioning=true,
- filename=true
- ) as ds
- inner join {registered_metadata_chunk} mc using (
- timdex_record_id, run_id, run_record_offset
- )
- where true
- {rro_clause};
- """
-
- def _iter_data_chunks(self, data_query: str) -> Iterator[pa.RecordBatch]:
- """Perform a query to retrieve data and stream chunks."""
- if self.location_scheme == "s3":
- self.conn.execute("""set threads=16;""")
- try:
- cursor = self.conn.execute(data_query)
- yield from cursor.to_arrow_reader(batch_size=self.config.read_batch_size)
- finally:
- if self.location_scheme == "s3":
- self.conn.execute(f"""set threads={self.conn_factory.threads};""")
-
- def read_dataframes_iter(
- self,
- table: str = "records",
- columns: list[str] | None = None,
- limit: int | None = None,
- where: str | None = None,
- **filters: Unpack[DatasetFilters],
- ) -> Iterator[pd.DataFrame]:
- for record_batch in self.read_batches_iter(
- table=table,
- columns=columns,
- limit=limit,
- where=where,
- **filters,
- ):
- yield record_batch.to_pandas()
-
- def read_dataframe(
- self,
- table: str = "records",
- columns: list[str] | None = None,
- limit: int | None = None,
- where: str | None = None,
- **filters: Unpack[DatasetFilters],
- ) -> pd.DataFrame | None:
- df_batches = [
- record_batch.to_pandas()
- for record_batch in self.read_batches_iter(
- table=table,
- columns=columns,
- limit=limit,
- where=where,
- **filters,
- )
- ]
- if not df_batches:
- return None
- return pd.concat(df_batches)
-
- def read_dicts_iter(
- self,
- table: str = "records",
- columns: list[str] | None = None,
- limit: int | None = None,
- where: str | None = None,
- **filters: Unpack[DatasetFilters],
- ) -> Iterator[dict]:
- for record_batch in self.read_batches_iter(
- table=table,
- columns=columns,
- limit=limit,
- where=where,
- **filters,
- ):
- yield from record_batch.to_pylist()
-
- def read_transformed_records_iter(
- self,
- table: str = "records",
- limit: int | None = None,
- where: str | None = None,
- **filters: Unpack[DatasetFilters],
- ) -> Iterator[dict]:
- for record_dict in self.read_dicts_iter(
- table=table,
- columns=["transformed_record"],
- limit=limit,
- where=where,
- **filters,
- ):
- if transformed_record := record_dict["transformed_record"]:
- yield json.loads(transformed_record)
diff --git a/timdex_dataset_api/embeddings.py b/timdex_dataset_api/embeddings.py
index b91df56..509a444 100644
--- a/timdex_dataset_api/embeddings.py
+++ b/timdex_dataset_api/embeddings.py
@@ -1,80 +1,12 @@
-import itertools
-import logging
-import time
-import uuid
-from collections.abc import Iterator
-from datetime import UTC, date, datetime
-from typing import TYPE_CHECKING, TypedDict, Unpack, cast
+from datetime import UTC, datetime
+from typing import ClassVar
import attrs
-import pandas as pd
import pyarrow as pa
-import pyarrow.dataset as ds
from attrs import asdict, define, field
-from duckdb import DuckDBPyConnection
-from duckdb import IOException as DuckDBIOException
-from duckdb_engine import Dialect as DuckDBDialect
-from sqlalchemy import and_, select, text
-from timdex_dataset_api.record import datetime_iso_parse
-from timdex_dataset_api.utils import build_filter_expr_sa
-
-if TYPE_CHECKING:
- from timdex_dataset_api import TIMDEXDataset
-
-
-logger = logging.getLogger(__name__)
-
-TIMDEX_DATASET_EMBEDDINGS_SCHEMA = pa.schema(
- (
- pa.field("timdex_record_id", pa.string()),
- pa.field("run_id", pa.string()),
- pa.field("run_record_offset", pa.int32()),
- pa.field("embedding_timestamp", pa.timestamp("us", tz="UTC")),
- pa.field("embedding_model", pa.string()),
- pa.field("embedding_strategy", pa.string()),
- pa.field("embedding_vector", pa.list_(pa.float32())),
- pa.field("embedding_object", pa.binary()),
- pa.field("year", pa.string()),
- pa.field("month", pa.string()),
- pa.field("day", pa.string()),
- )
-)
-
-
-EMBEDDINGS_FILTER_COLUMNS = {
- "timdex_record_id",
- "run_id",
- "run_record_offset",
- "embedding_timestamp",
- "embedding_model",
- "embedding_strategy",
-}
-
-# subset of record metadata columns for filtering and selecting
-METADATA_SELECT_FILTER_COLUMNS = {
- "source",
- "run_date",
- "run_type",
- "action",
- "run_timestamp",
-}
-
-
-class EmbeddingsFilters(TypedDict, total=False):
- # embeddings columns
- timdex_record_id: str
- run_id: str
- run_record_offset: int
- embedding_timestamp: str | datetime
- embedding_model: str
- embedding_strategy: str
- # record metadata columns
- source: str | list[str]
- run_date: str | date | list[str | date]
- run_type: str | list[str]
- action: str | list[str]
- run_timestamp: str | datetime | list[str | datetime]
+from timdex_dataset_api.data_source import DataSourceTableConfig, TIMDEXDataSource
+from timdex_dataset_api.utils import datetime_iso_parse
@define
@@ -135,382 +67,130 @@ def to_dict(
}
-class TIMDEXEmbeddings:
- def __init__(self, timdex_dataset: "TIMDEXDataset"):
- """Init TIMDEXEmbeddings.
-
- Class to handle the writing and readings of embeddings associated with TIMDEX
- records.
-
- Args:
- - timdex_dataset: instance of TIMDEXDataset
- """
- self.timdex_dataset = timdex_dataset
- self.conn = timdex_dataset.conn
-
- self.schema = TIMDEX_DATASET_EMBEDDINGS_SCHEMA
- self.partition_columns = ["year", "month", "day"]
-
- # set up embeddings views
- self._setup_embeddings_views()
-
- @property
- def data_embeddings_root(self) -> str:
- return f"{self.timdex_dataset.location.removesuffix('/')}/data/embeddings"
-
- def _setup_embeddings_views(self) -> None:
- """Set up embeddings views in the 'data' schema."""
- start_time = time.perf_counter()
-
- try:
- self._create_embeddings_view(self.conn)
- self._create_current_embeddings_view(self.conn)
- self._create_current_run_embeddings_view(self.conn)
- except DuckDBIOException:
- logger.debug("No embeddings parquet files found")
- except Exception as exception: # noqa: BLE001
- logger.warning(f"Error creating embeddings views: {exception}")
-
- logger.debug(
- "Embeddings views setup for TIMDEXEmbeddings, "
- f"{round(time.perf_counter() - start_time, 2)}s"
+class TIMDEXEmbeddings(TIMDEXDataSource):
+ """Class to handle record embeddings in the TIMDEXDataset."""
+
+ NAME: ClassVar[str] = "embeddings"
+
+ SCHEMA: ClassVar[pa.Schema] = pa.schema(
+ (
+ pa.field("timdex_record_id", pa.string()),
+ pa.field("run_id", pa.string()),
+ pa.field("run_record_offset", pa.int32()),
+ pa.field("embedding_timestamp", pa.timestamp("us", tz="UTC")),
+ pa.field("embedding_model", pa.string()),
+ pa.field("embedding_strategy", pa.string()),
+ pa.field("embedding_vector", pa.list_(pa.float32())),
+ pa.field("embedding_object", pa.binary()),
+ pa.field("year", pa.string()),
+ pa.field("month", pa.string()),
+ pa.field("day", pa.string()),
)
+ )
- def _create_embeddings_view(self, conn: DuckDBPyConnection) -> None:
- """Create a view that projects over embeddings parquet files."""
- logger.debug("creating view data.embeddings")
-
- conn.execute(f"""
- create or replace view data.embeddings as
- (
- select *
- from read_parquet(
- '{self.data_embeddings_root}/**/*.parquet',
- hive_partitioning=true,
- filename=true
- )
- );
- """)
-
- def _create_current_embeddings_view(self, conn: DuckDBPyConnection) -> None:
- """Create a view of current embedding records.
+ DATA_COLUMNS: ClassVar[list[str]] = [
+ "embedding_vector",
+ "embedding_object",
+ ]
- This builds on the 'data.embeddings' view. This view includes only
- the most current version of each embedding grouped by
- [timdex_record_id, embedding_strategy].
- """
- logger.debug("creating view data.current_embeddings")
+ DATA_PATH: ClassVar[str] = "data/embeddings"
- # SQL for the current records logic (CTEs)
- conn.execute("""
- create or replace view data.current_embeddings as
+ CURRENT_METADATA_VIEW_QUERY: ClassVar[str] = """
+ with
+ -- CTE of embeddings attached to current record versions only
+ ce_current_record_embeddings as
(
- with
- -- CTE of embeddings ranked by embedding_timestamp
- ce_ranked_embeddings as
- (
- select
- *,
- row_number() over (
- partition by timdex_record_id, embedding_strategy
- order by
- embedding_timestamp desc nulls last,
- run_record_offset desc nulls last
- ) as rn
- from data.embeddings
- )
- -- final select for current records (rn = 1)
select
- * exclude (rn)
- from ce_ranked_embeddings
- where rn = 1
- );
- """)
-
- def _create_current_run_embeddings_view(self, conn: DuckDBPyConnection) -> None:
- """Create a view of current embedding records per run.
-
- This builds on the 'data.embeddings' view. This view includes only
- the most current version of each embedding per run grouped by
- [timdex_record_id, run_id, embedding_strategy,].
- """
- logger.debug("creating view data.current_run_embeddings")
+ e.*
+ from metadata.embeddings e
+ join metadata.current_records r using (
+ source,
+ timdex_record_id,
+ run_id,
+ run_record_offset
+ )
+ ),
- # SQL for the current records logic (CTEs)
- conn.execute("""
- create or replace view data.current_run_embeddings as
+ -- CTE of current-record embeddings ranked by embedding recency
+ ce_ranked_embeddings as
(
- with
- -- CTE of embeddings ranked by embedding_timestamp
- ce_ranked_embeddings as
- (
- select
- *,
- row_number() over (
- partition by timdex_record_id, run_id, embedding_strategy
- order by
- embedding_timestamp desc nulls last,
- run_id desc nulls last,
- run_record_offset desc nulls last
- ) as rn
- from data.embeddings
- )
- -- final select for current records (rn = 1)
select
- * exclude (rn)
- from ce_ranked_embeddings
- where rn = 1
- );
- """)
-
- def write(
- self,
- embeddings_iter: Iterator[DatasetEmbedding],
- *,
- use_threads: bool = True,
- ) -> list[ds.WrittenFile]:
- """Write embeddings as parquet files to /data/embeddings.
-
- Approach is similar to TIMDEXDataset.write() for Records:
- - use self.data_embeddings_root for location of embeddings parquet files
- - use pyarrow Dataset to write rows
- """
- start_time = time.perf_counter()
- written_files: list[ds.WrittenFile] = []
-
- filesystem, path = self.timdex_dataset.parse_location(self.data_embeddings_root)
-
- embedding_batches_iter = self.create_embedding_batches(embeddings_iter)
- ds.write_dataset(
- embedding_batches_iter,
- base_dir=path,
- basename_template="%s-{i}.parquet" % (str(uuid.uuid4())), # noqa: UP031
- existing_data_behavior="overwrite_or_ignore",
- filesystem=filesystem,
- file_visitor=lambda written_file: written_files.append(written_file), # type: ignore[arg-type] # noqa: PLW0108
- format="parquet",
- max_open_files=500,
- max_rows_per_file=self.timdex_dataset.config.max_rows_per_file,
- max_rows_per_group=self.timdex_dataset.config.max_rows_per_group,
- partitioning=self.partition_columns,
- partitioning_flavor="hive",
- schema=self.schema,
- use_threads=use_threads,
- )
-
- self.log_write_statistics(start_time, written_files)
-
- return written_files
-
- def create_embedding_batches(
- self, embeddings_iter: Iterator["DatasetEmbedding"]
- ) -> Iterator[pa.RecordBatch]:
- for i, embedding_batch in enumerate(
- itertools.batched(
- embeddings_iter, self.timdex_dataset.config.write_batch_size
+ e.*,
+ row_number() over (
+ partition by
+ e.timdex_record_id,
+ e.embedding_model,
+ e.embedding_strategy
+ order by
+ e.embedding_timestamp desc nulls last,
+ e.filename desc nulls last
+ ) as rn
+ from ce_current_record_embeddings e
)
- ):
- embedding_dicts = [embedding.to_dict() for embedding in embedding_batch]
- batch = pa.RecordBatch.from_pylist(embedding_dicts)
- logger.debug(f"Yielding batch {i + 1} for dataset writing.")
- yield batch
-
- def log_write_statistics(
- self,
- start_time: float,
- written_files: list[ds.WrittenFile],
- ) -> None:
- """Parse written files from write and log statistics."""
- total_time = round(time.perf_counter() - start_time, 2)
- total_files = len(written_files)
- total_rows = sum(
- [wf.metadata.num_rows for wf in written_files] # type: ignore[attr-defined]
- )
- total_size = sum([wf.size for wf in written_files]) # type: ignore[attr-defined]
- logger.info(
- f"Dataset write complete - elapsed: "
- f"{total_time}s, "
- f"total files: {total_files}, "
- f"total rows: {total_rows}, "
- f"total size: {total_size}"
- )
-
- def read_batches_iter(
- self,
- table: str = "embeddings",
- columns: list[str] | None = None,
- limit: int | None = None,
- where: str | None = None,
- **filters: Unpack[EmbeddingsFilters],
- ) -> Iterator[pa.RecordBatch]:
- """Yield ETL records as pyarrow.RecordBatches.
-
- This is the base read method. All read methods use this for streaming
- batches of records. This method relies on DuckDB to project over all
- embeddings parquet files (i.e., no "metadata layer") and filter data.
- """
- start_time = time.perf_counter()
-
- if table not in ["embeddings", "current_embeddings", "current_run_embeddings"]:
- raise ValueError(f"Invalid table: '{table}'")
-
- # ensure table exists
- try:
- self.timdex_dataset.get_sa_table("data", table)
- except ValueError:
- logger.warning(
- f"Table '{table}' not found in DuckDB context. Embeddings may not yet "
- "exist or TIMDEXDataset.refresh() may be required."
- )
- return
-
- data_query = self._build_query(
- table,
- columns,
- limit,
- where,
- **filters,
- )
- cursor = self.conn.execute(data_query)
- yield from cursor.to_arrow_reader(
- batch_size=self.timdex_dataset.config.read_batch_size
- )
-
- logger.debug(f"read() elapsed: {round(time.perf_counter() - start_time, 2)}s")
-
- def _build_query(
- self,
- table: str = "embeddings",
- columns: list[str] | None = None,
- limit: int | None = None,
- where: str | None = None,
- **filters: Unpack[EmbeddingsFilters],
- ) -> str:
- """Build SQL query using SQLAlchemy.
-
- The method returns a SQL query string, which SQLAlchemy executes to
- fetch results. Always joins to metadata.records to enable filtering
- by metadata columns (source, run_date, run_type, action, run_timestamp).
- """
- embeddings_table = self.timdex_dataset.get_sa_table("data", table)
- metadata_table = self.timdex_dataset.get_sa_table("metadata", "records")
-
- # select specific columns or default to all from embeddings + metadata
- if columns:
- embeddings_cols = []
- metadata_cols = []
-
- for col_name in columns:
- if col_name in TIMDEX_DATASET_EMBEDDINGS_SCHEMA.names:
- embeddings_cols.append(embeddings_table.c[col_name])
- elif col_name in METADATA_SELECT_FILTER_COLUMNS:
- metadata_cols.append(metadata_table.c[col_name])
- else:
- raise ValueError(f"Invalid column: {col_name}")
-
- stmt = select(*embeddings_cols, *metadata_cols)
- else:
- embeddings_cols = [
- embeddings_table.c[col] for col in TIMDEX_DATASET_EMBEDDINGS_SCHEMA.names
- ]
- metadata_cols = [
- metadata_table.c[col] for col in METADATA_SELECT_FILTER_COLUMNS
- ]
- stmt = select(*embeddings_cols, *metadata_cols)
-
- # create SQL statement with join to metadata.records
- join_condition = and_(
- embeddings_table.c.timdex_record_id == metadata_table.c.timdex_record_id,
- embeddings_table.c.run_id == metadata_table.c.run_id,
- embeddings_table.c.run_record_offset == metadata_table.c.run_record_offset,
- )
- stmt = stmt.select_from(embeddings_table.join(metadata_table, join_condition))
-
- # split filters between embeddings and metadata tables
- embeddings_filters = {
- k: v for k, v in filters.items() if k in EMBEDDINGS_FILTER_COLUMNS
- }
- record_metadata_filters = {
- k: v for k, v in filters.items() if k in METADATA_SELECT_FILTER_COLUMNS
- }
-
- # apply embeddings filters
- embeddings_filter_expr = build_filter_expr_sa(
- embeddings_table, **cast("dict", embeddings_filters)
- )
- if embeddings_filter_expr is not None:
- stmt = stmt.where(embeddings_filter_expr)
-
- # apply metadata filters
- record_metadata_filter_expr = build_filter_expr_sa(
- metadata_table, **cast("dict", record_metadata_filters)
- )
- if record_metadata_filter_expr is not None:
- stmt = stmt.where(record_metadata_filter_expr)
-
- # explicit raw WHERE string
- if where is not None and where.strip():
- stmt = stmt.where(text(where))
-
- # apply limit if present
- if limit:
- stmt = stmt.limit(limit)
-
- # using DuckDB dialect, compile to SQL string
- compiled = stmt.compile(
- dialect=DuckDBDialect(),
- compile_kwargs={"literal_binds": True},
- )
- return str(compiled)
-
- def read_dataframes_iter(
- self,
- table: str = "embeddings",
- columns: list[str] | None = None,
- limit: int | None = None,
- where: str | None = None,
- **filters: Unpack[EmbeddingsFilters],
- ) -> Iterator[pd.DataFrame]:
- for record_batch in self.read_batches_iter(
- table=table, columns=columns, limit=limit, where=where, **filters
- ):
- yield record_batch.to_pandas()
+ -- final select for current embeddings (rn = 1)
+ select
+ * exclude (rn)
+ from ce_ranked_embeddings
+ where rn = 1
+ """
- def read_dataframe(
- self,
- table: str = "embeddings",
- columns: list[str] | None = None,
- limit: int | None = None,
- where: str | None = None,
- **filters: Unpack[EmbeddingsFilters],
- ) -> pd.DataFrame | None:
- df_batches = [
- record_batch.to_pandas()
- for record_batch in self.read_batches_iter(
- table=table,
- columns=columns,
- limit=limit,
- where=where,
- **filters,
+ CURRENT_RUN_METADATA_VIEW_QUERY: ClassVar[str] = """
+ with
+ -- CTE of embeddings ranked by embedding recency within a run and family
+ -- keep run_timestamp because the same run_id can be written more than once
+ -- keep run_record_offset as an intra-run tie-break when the same logical
+ -- record appears more than once within a run
+ crce_ranked_embeddings as
+ (
+ select
+ e.*,
+ row_number() over (
+ partition by
+ e.timdex_record_id,
+ e.run_id,
+ e.embedding_model,
+ e.embedding_strategy
+ order by
+ e.run_timestamp desc nulls last,
+ e.embedding_timestamp desc nulls last,
+ e.run_record_offset desc nulls last,
+ e.filename desc nulls last
+ ) as rn
+ from metadata.embeddings e
)
- ]
- if not df_batches:
- return None
- return pd.concat(df_batches)
+ -- final select for current run embeddings (rn = 1)
+ select
+ * exclude (rn)
+ from crce_ranked_embeddings
+ where rn = 1
+ """
- def read_dicts_iter(
- self,
- table: str = "embeddings",
- columns: list[str] | None = None,
- limit: int | None = None,
- where: str | None = None,
- **filters: Unpack[EmbeddingsFilters],
- ) -> Iterator[dict]:
- for record_batch in self.read_batches_iter(
- table=table,
- columns=columns,
- limit=limit,
- where=where,
- **filters,
- ):
- yield from record_batch.to_pylist()
+ TABLES: ClassVar[list[DataSourceTableConfig]] = [
+ DataSourceTableConfig(
+ name="embeddings",
+ description="All embedding versions across all runs.",
+ kind="base",
+ ),
+ DataSourceTableConfig(
+ name="current_embeddings",
+ description=(
+ "One row per (timdex_record_id, embedding_model, "
+ "embedding_strategy) representing the most recent embedding "
+ "for each current record."
+ ),
+ kind="custom",
+ query_sql=CURRENT_METADATA_VIEW_QUERY,
+ required_metadata_tables=["embeddings", "current_records"],
+ ),
+ DataSourceTableConfig(
+ name="current_run_embeddings",
+ description=(
+ "One row per (timdex_record_id, run_id, embedding_model, "
+ "embedding_strategy) representing the most recent embedding "
+ "within each run, regardless of whether the record is current."
+ ),
+ kind="custom",
+ query_sql=CURRENT_RUN_METADATA_VIEW_QUERY,
+ required_metadata_tables=["embeddings", "records"],
+ ),
+ ]
diff --git a/timdex_dataset_api/metadata.py b/timdex_dataset_api/metadata.py
index 11da914..cf38e9e 100644
--- a/timdex_dataset_api/metadata.py
+++ b/timdex_dataset_api/metadata.py
@@ -5,9 +5,13 @@
import tempfile
import time
from pathlib import Path
-from typing import TYPE_CHECKING, Literal, Unpack, cast
+from typing import TYPE_CHECKING, Any, ClassVar, Unpack, cast
+from duckdb import BinderException as DuckDBBinderException
+from duckdb import CatalogException as DuckDBCatalogException
from duckdb import DuckDBPyConnection
+from duckdb import HTTPException as DuckDBHTTPException
+from duckdb import IOException as DuckDBIOException
from duckdb_engine import Dialect as DuckDBDialect
from sqlalchemy import func, literal, select, text, tuple_
@@ -19,24 +23,28 @@
)
if TYPE_CHECKING:
- from timdex_dataset_api.dataset import DatasetFilters, TIMDEXDataset
+ from timdex_dataset_api.data_source import DataSourceTableConfig, TIMDEXDataSource
+ from timdex_dataset_api.dataset import TIMDEXDataset
+ from timdex_dataset_api.records import RecordsFilters
logger = configure_logger(__name__)
-ORDERED_METADATA_COLUMN_NAMES = [
- "timdex_record_id",
- "source",
- "run_date",
- "run_type",
- "action",
- "run_id",
- "run_record_offset",
- "run_timestamp",
- "filename",
-]
-
class TIMDEXDatasetMetadata:
+ """Class to handle metadata for all data sources in the TIMDEXDataset."""
+
+ BASE_METADATA_COLUMNS: ClassVar[list[str]] = [
+ "timdex_record_id",
+ "source",
+ "run_date",
+ "run_type",
+ "action",
+ "run_id",
+ "run_record_offset",
+ "run_timestamp",
+ "filename",
+ ]
+
def __init__(self, timdex_dataset: "TIMDEXDataset") -> None:
"""Init TIMDEXDatasetMetadata.
@@ -44,30 +52,15 @@ def __init__(self, timdex_dataset: "TIMDEXDataset") -> None:
timdex_dataset: parent TIMDEXDataset instance
"""
self.timdex_dataset = timdex_dataset
- self.conn = timdex_dataset.conn
+ self.source_classes = timdex_dataset.source_classes
+ self.table_configs = timdex_dataset.table_configs
self.create_metadata_structure()
self._setup_metadata_schema()
- @property
- def location(self) -> str:
- return self.timdex_dataset.location
-
- @property
- def location_scheme(self) -> Literal["file", "s3"]:
- return self.timdex_dataset.location_scheme
-
- @property
- def config(self) -> "TIMDEXDataset.config": # type: ignore[name-defined]
- return self.timdex_dataset.config
-
- @property
- def preload_current_records(self) -> bool:
- return self.timdex_dataset.preload_current_records
-
@property
def metadata_root(self) -> str:
- return f"{self.location.removesuffix('/')}/metadata"
+ return f"{self.timdex_dataset.location.removesuffix('/')}/metadata"
@property
def metadata_database_filename(self) -> str:
@@ -77,46 +70,94 @@ def metadata_database_filename(self) -> str:
def metadata_database_path(self) -> str:
return f"{self.metadata_root}/{self.metadata_database_filename}"
- @property
- def append_deltas_path(self) -> str:
- return f"{self.metadata_root}/append_deltas"
+ def append_deltas_path_for(self, source_class: type["TIMDEXDataSource"]) -> str:
+ """Return the append deltas path for a specific data source."""
+ return f"{self.metadata_root}/append_deltas/{source_class.NAME}"
+
+ def resolve_source_class_for_table(self, table: str) -> type["TIMDEXDataSource"]:
+ """Resolve a metadata table/view name to its owning data source class."""
+ for source_class in self.source_classes:
+ if table == source_class.NAME or table.endswith(f"_{source_class.NAME}"):
+ return source_class
+
+ raise ValueError(f"Could not resolve data source for metadata table '{table}'.")
+
+ def data_source_metadata_columns_for(
+ self, source_class: type["TIMDEXDataSource"]
+ ) -> list[str]:
+ """Return the full metadata column surface for a data source."""
+ return source_class.METADATA_COLUMNS
+
+ def get_metadata_columns_for_table(self, table: str) -> list[str]:
+ """Return canonical metadata columns projected by read keyset queries.
+
+ The returned columns are derived from the owning data source's metadata surface
+ and filtered to columns actually available on the requested table or view.
+ """
+ sa_table = self.timdex_dataset.get_sa_table("metadata", table)
+ available_columns = set(sa_table.c.keys())
+
+ source_class = self.resolve_source_class_for_table(table)
+ expected_columns = self.data_source_metadata_columns_for(source_class)
+
+ projected_columns: list[str] = []
+ for column_name in expected_columns:
+ if column_name not in available_columns:
+ continue
+ if column_name in projected_columns:
+ continue
+ projected_columns.append(column_name)
+
+ return projected_columns
@property
def records_count(self) -> int:
"""Count of all records in dataset."""
- return self.conn.query("""
+ return self.timdex_dataset.conn.query("""
select count(*) from metadata.records;
""").fetchone()[0] # type: ignore[index]
@property
def current_records_count(self) -> int:
"""Count of all current records in dataset."""
- return self.conn.query("""
+ return self.timdex_dataset.conn.query("""
select count(*) from metadata.current_records;
""").fetchone()[0] # type: ignore[index]
+ def append_deltas_count_for(self, source_class: type["TIMDEXDataSource"]) -> int:
+ """Count append deltas rows for a single data source."""
+ view_name = f"{source_class.NAME}_append_deltas"
+ return self.timdex_dataset.conn.query(f"""
+ select count(*) from metadata.{view_name};
+ """).fetchone()[0] # type: ignore[index]
+
@property
def append_deltas_count(self) -> int:
- """Count of all append deltas."""
- return self.conn.query("""
- select count(*) from metadata.append_deltas;
- """).fetchone()[0] # type: ignore[index]
+ """Count of append deltas rows across all registered data sources."""
+ total = 0
+ for source_class in self.source_classes:
+ try:
+ total += self.append_deltas_count_for(source_class)
+ except (DuckDBCatalogException, DuckDBBinderException):
+ continue
+ return total
def create_metadata_structure(self) -> None:
"""Ensure metadata structure exists in TIMDEX dataset."""
- if self.location_scheme == "file":
+ if self.timdex_dataset.location_scheme == "file":
Path(self.metadata_database_path).parent.mkdir(
parents=True,
exist_ok=True,
)
- Path(self.append_deltas_path).mkdir(
- parents=True,
- exist_ok=True,
- )
+ for source_class in self.source_classes:
+ Path(self.append_deltas_path_for(source_class)).mkdir(
+ parents=True,
+ exist_ok=True,
+ )
def database_exists(self) -> bool:
"""Check if static metadata database file exists."""
- if self.location_scheme == "s3":
+ if self.timdex_dataset.location_scheme == "s3":
s3_client = S3Client()
return s3_client.object_exists(self.metadata_database_path)
return os.path.exists(self.metadata_database_path)
@@ -130,22 +171,26 @@ def rebuild_dataset_metadata(self) -> None:
- build a local, temporary static metadata database file, then overwrite the
canonical version in the dataset (e.g. in S3)
"""
- if self.location_scheme == "s3":
- s3_client = S3Client()
- s3_client.delete_folder(self.append_deltas_path)
- else:
- shutil.rmtree(self.append_deltas_path, ignore_errors=True)
+ for source_class in self.source_classes:
+ deltas_path = self.append_deltas_path_for(source_class)
+ if self.timdex_dataset.location_scheme == "s3":
+ s3_client = S3Client()
+ s3_client.delete_folder(deltas_path)
+ else:
+ shutil.rmtree(deltas_path, ignore_errors=True)
# build database locally
with tempfile.TemporaryDirectory() as temp_dir:
local_db_path = str(Path(temp_dir) / self.metadata_database_filename)
- factory = DuckDBConnectionFactory(location_scheme=self.location_scheme)
+ factory = DuckDBConnectionFactory(
+ location_scheme=self.timdex_dataset.location_scheme
+ )
with factory.create_connection(local_db_path) as conn:
self._create_full_dataset_table(conn)
# copy local database file to remote location
- if self.location_scheme == "s3":
+ if self.timdex_dataset.location_scheme == "s3":
s3_client = S3Client()
s3_client.upload_file(
local_db_path,
@@ -158,37 +203,53 @@ def rebuild_dataset_metadata(self) -> None:
self.timdex_dataset.refresh()
def _create_full_dataset_table(self, conn: DuckDBPyConnection) -> None:
- """Create a table of metadata for all records in the ETL parquet dataset.
+ """Create metadata tables for all data sources in the static database.
- This is one of the few times we fully materialize data in a DuckDB connection.
- This is most commonly used when recreating the baseline static metadata database
- file.
+ Iterates over registered data source classes and creates one table per source.
+ Gracefully skips data sources whose parquet data does not yet exist.
"""
- start_time = time.perf_counter()
- logger.debug("creating table static_db.main.records")
+ for source_class in self.source_classes:
+ self._create_metadata_table(conn, source_class)
- # temporarily increase thread count
- conn.execute("""SET threads = 64;""")
+ def _create_metadata_table(
+ self, conn: DuckDBPyConnection, source_class: type["TIMDEXDataSource"]
+ ) -> None:
+ """Create a metadata table for a single data source in the static database."""
+ start_time = time.perf_counter()
+ data_path = (
+ f"{self.timdex_dataset.location.removesuffix('/')}/{source_class.DATA_PATH}"
+ )
- query = f"""
- create or replace table records as (
- select
- {",".join(ORDERED_METADATA_COLUMN_NAMES)}
- from read_parquet(
- '{self.location}/data/records/**/*.parquet',
- hive_partitioning=true,
- filename=true
- )
- );
+ logger.debug(f"creating table static_db.main.{source_class.NAME}")
+
+ # temporarily increase thread count for parallel parquet file scanning
+ conn.execute("SET threads = 64;")
+
+ try:
+ sql_query = f"""
+ create or replace table {source_class.NAME} as (
+ select {",".join(source_class.SOURCE_METADATA_COLUMNS)}
+ from read_parquet(
+ '{data_path}/**/*.parquet',
+ hive_partitioning=true,
+ filename=true
+ )
+ );
"""
- conn.execute(query)
-
- # reset thread count
- conn.execute(f"""SET threads = {self.timdex_dataset.conn_factory.threads};""")
+ conn.execute(sql_query)
+ except DuckDBIOException:
+ logger.warning(
+ f"Could not create metadata table for '{source_class.NAME}' "
+ f"(no parquet data at '{data_path}'). Skipping."
+ )
+ return
+ finally:
+ # always reset thread count, even if parquet data is missing
+ conn.execute(f"""SET threads = {self.timdex_dataset.conn_factory.threads};""")
- row_count = conn.query("""select count(*) from records;""").fetchone()[0] # type: ignore[index]
+ row_count = conn.query(f"select count(*) from {source_class.NAME};").fetchone()[0] # type: ignore[index]
logger.info(
- f"'records' table created - rows: {row_count}, "
+ f"'{source_class.NAME}' table created - rows: {row_count}, "
f"elapsed: {time.perf_counter() - start_time}"
)
@@ -196,21 +257,44 @@ def _setup_metadata_schema(self) -> None:
"""Set up metadata schema views in the DuckDB connection.
Creates views for accessing static metadata DB and append deltas.
- If static DB doesn't exist, logs warning but doesn't fail.
+ If the static DB does not exist yet, bootstrap metadata views from append
+ deltas when available.
"""
start_time = time.perf_counter()
- if not self.database_exists():
- logger.warning(
- f"Static metadata database not found @ '{self.metadata_database_path}'. "
- "Consider rebuild via TIMDEXDataset.metadata.rebuild_dataset_metadata()."
- )
- return
+ if self.database_exists():
+ self._attach_database_file(self.timdex_dataset.conn)
+ else:
+ bootstrap_sources = [
+ source_class.NAME
+ for source_class in self.source_classes
+ if self._append_delta_count(self.timdex_dataset.conn, source_class) > 0
+ ]
+ if bootstrap_sources:
+ logger.warning(
+ "Static metadata database not found @ "
+ f"'{self.metadata_database_path}'. "
+ "Bootstrapping metadata views from append deltas for: "
+ f"{', '.join(bootstrap_sources)}. "
+ "Consider rebuild via "
+ "TIMDEXDataset.metadata.rebuild_dataset_metadata()."
+ )
+ else:
+ logger.warning(
+ "Static metadata database not found @ "
+ f"'{self.metadata_database_path}'. "
+ "Consider rebuild via "
+ "TIMDEXDataset.metadata.rebuild_dataset_metadata()."
+ )
+
+ for source_class in self.source_classes:
+ self._create_append_deltas_view(self.timdex_dataset.conn, source_class)
+ self._create_union_view(self.timdex_dataset.conn, source_class)
- self._attach_database_file(self.conn)
- self._create_append_deltas_view(self.conn)
- self._create_records_union_view(self.conn)
- self._create_current_records_view(self.conn)
+ for table_config in self.table_configs:
+ if table_config.kind != "custom":
+ continue
+ self._create_custom_metadata_table(self.timdex_dataset.conn, table_config)
logger.debug(
"Metadata schema setup for TIMDEXDatasetMetadata, "
@@ -229,175 +313,292 @@ def _attach_database_file(self, conn: DuckDBPyConnection) -> None:
f"""attach '{self.metadata_database_path}' AS static_db (READ_ONLY);"""
)
- def _create_append_deltas_view(self, conn: DuckDBPyConnection) -> None:
- """Create a view that projects over append delta parquet files.
+ def _create_append_deltas_view(
+ self, conn: DuckDBPyConnection, source_class: type["TIMDEXDataSource"]
+ ) -> None:
+ """Create a view that projects over append delta parquet files for a data source.
- If when run there are NO append deltas, which could be true immediately after a
- metadata base create/recreate or append delta merge, we still create a view by
- utilizing the schema from static_db.records but without any rows. This allows us
- to build additional downstream views on top of *this* view. Also noting that a
- call to .refresh() will recreate this view.
+ If there are NO append deltas (e.g. after a rebuild or merge), we still create a
+ view by utilizing the schema from the static DB table but without any rows. This
+ allows downstream views to be built on top of this view.
+
+ The view is named ``metadata.{source_class.NAME}_append_deltas``.
"""
- logger.debug("creating view metadata.append_deltas")
+ view_name = f"{source_class.NAME}_append_deltas"
+ deltas_path = self.append_deltas_path_for(source_class)
+ static_table = f"static_db.{source_class.NAME}"
+
+ logger.debug(f"creating view metadata.{view_name}")
# get current append delta count
- append_delta_count = conn.execute(f"""
- select count(*) as file_count
- from glob('{self.append_deltas_path}/*.parquet')
- """).fetchone()[0] # type: ignore[index]
- logger.debug(f"{append_delta_count} append deltas found")
+ append_delta_count = self._append_delta_count(conn, source_class)
+ logger.debug(
+ f"{append_delta_count} append deltas found for '{source_class.NAME}'"
+ )
- # if deltas, create view projecting over those parquet files
+ # if deltas exist, always create this view from parquet files
if append_delta_count > 0:
- query = f"""
- create or replace view metadata.append_deltas as (
- select *
- from read_parquet(
- '{self.append_deltas_path}/*.parquet',
- filename = 'append_delta_filename'
- )
- );
- """
+ conn.execute(f"""
+ create or replace view metadata.{view_name} as (
+ select *
+ from read_parquet(
+ '{deltas_path}/*.parquet',
+ filename = 'append_delta_filename'
+ )
+ );
+ """)
+ return
- # if not, create a view that mirrors the structure of static_db.records
- else:
- query = """
- create or replace view metadata.append_deltas as (
- select *
- from static_db.records
- where 1 = 0
- );"""
+ # no deltas: if static table exists, create zero-row mirror
+ table_exists = conn.execute(f"""
+ select count(*) from information_schema.tables
+ where table_catalog = 'static_db'
+ and table_name = '{source_class.NAME}'
+ """).fetchone()[0] # type: ignore[index]
- conn.execute(query)
+ if table_exists:
+ conn.execute(f"""
+ create or replace view metadata.{view_name} as (
+ select *,
+ null::varchar as append_delta_filename
+ from {static_table}
+ where 1 = 0
+ );
+ """)
+ return
- def _create_records_union_view(self, conn: DuckDBPyConnection) -> None:
- logger.debug("creating view metadata.records")
+ # no static table and no deltas, so no view to create
+ logger.debug(
+ f"No static table or append deltas found for '{source_class.NAME}'; "
+ f"skipping append deltas view for '{source_class.NAME}'."
+ )
- conn.execute(f"""
- create or replace view metadata.records as
- (
- select
- {",".join(ORDERED_METADATA_COLUMN_NAMES)}
- from static_db.records
- union all
- select
- {",".join(ORDERED_METADATA_COLUMN_NAMES)}
- from metadata.append_deltas
- );
- """)
+ # columns added to bolt-on types via pre-join to metadata.records
+ PREJOIN_RECORDS_COLUMNS: ClassVar[list[str]] = [
+ "source",
+ "run_date",
+ "run_type",
+ "action",
+ "run_timestamp",
+ ]
+
+ def _create_union_view(
+ self, conn: DuckDBPyConnection, source_class: type["TIMDEXDataSource"]
+ ) -> None:
+ """Create a union view combining static DB and append deltas for a data source.
+
+ The view is named ``metadata.{source_class.NAME}`` and unions
+ `static_db.{source_class.NAME}` with `metadata.{source_class.NAME}_append_deltas`.
+
+ For bolt-on data sources (`source_class.PREJOIN_RECORDS=True`), the view
+ pre-joins to `metadata.records` so that `source`, `run_date`, `run_type`,
+ `action`, and `run_timestamp` are available as filterable columns.
+ """
+ view_name = source_class.NAME
+ static_table = f"static_db.{source_class.NAME}"
+ deltas_view = f"metadata.{source_class.NAME}_append_deltas"
+ columns = ",".join(source_class.SOURCE_METADATA_COLUMNS)
+
+ logger.debug(f"creating view metadata.{view_name}")
+
+ static_table_exists = conn.execute(f"""
+ select count(*) from information_schema.tables
+ where table_catalog = 'static_db'
+ and table_name = '{source_class.NAME}'
+ """).fetchone()[0] # type: ignore[index]
+
+ deltas_view_exists = conn.execute(f"""
+ select count(*) from information_schema.tables
+ where table_schema = 'metadata'
+ and table_name = '{source_class.NAME}_append_deltas'
+ and table_type = 'VIEW'
+ """).fetchone()[0] # type: ignore[index]
+
+ # build the base union (or single-source) subquery
+ base_subquery = self._build_base_union_sql(
+ static_table,
+ deltas_view,
+ columns,
+ static_table_exists,
+ deltas_view_exists,
+ )
- def _create_current_records_view(self, conn: DuckDBPyConnection) -> None:
- """Create a view of current records.
+ if base_subquery is None:
+ logger.debug(
+ f"No static table or append deltas view found for '{source_class.NAME}'; "
+ f"skipping union view for '{source_class.NAME}'."
+ )
+ return
- This view builds on the table `records`.
+ if source_class.PREJOIN_RECORDS:
+ if not self._metadata_table_exists(conn, "records"):
+ logger.warning(
+ f"Skipping metadata.{view_name} view creation because missing "
+ "dependency: records"
+ )
+ return
- This metadata view includes only the most current version of each record in the
- dataset. With the metadata provided from this view, we can streamline data
- retrievals in TIMDEXDataset read methods.
+ prejoin_cols = ",".join(f"r.{c}" for c in self.PREJOIN_RECORDS_COLUMNS)
+ join_keys = "timdex_record_id, run_id, run_record_offset"
+ conn.execute(f"""
+ create or replace view metadata.{view_name} as
+ select
+ e.*,
+ {prejoin_cols}
+ from ({base_subquery}) e
+ join metadata.records r using ({join_keys});
+ """)
+ else:
+ conn.execute(f"""
+ create or replace view metadata.{view_name} as
+ {base_subquery};
+ """)
- By default, creates a view only (lazy evaluation). If
- preload_current_records=True, creates a temp table for better performance
- for repeated queries.
+ @staticmethod
+ def _build_base_union_sql(
+ static_table: str,
+ deltas_view: str,
+ columns: str,
+ static_table_exists: int,
+ deltas_view_exists: int,
+ ) -> str | None:
+ """Return base union SQL or None if neither source exists."""
+ if static_table_exists and deltas_view_exists:
+ return f"""
+ select {columns} from {static_table}
+ union all
+ select {columns} from {deltas_view}
+ """
+ if static_table_exists:
+ return f"select {columns} from {static_table}"
+ if deltas_view_exists:
+ return f"select {columns} from {deltas_view}"
+ return None
+
+ def _create_custom_metadata_table(
+ self, conn: DuckDBPyConnection, table_config: "DataSourceTableConfig"
+ ) -> None:
+ """Create a custom metadata view from a data-source table config."""
+ missing_tables = [
+ table_name
+ for table_name in table_config.required_metadata_tables
+ if not self._metadata_table_exists(conn, table_name)
+ ]
+
+ if missing_tables:
+ logger.warning(
+ f"Skipping metadata.{table_config.name} view creation because missing "
+ f"dependencies: {', '.join(missing_tables)}"
+ )
+ return
- For temp table mode, the data is mostly in memory but has the ability to spill to
- disk if we risk getting too close to our memory constraints. We explicitly set the
- temporary location on disk for DuckDB at "/tmp" to play nice with contexts like
- AWS ECS or Lambda, where sometimes the $HOME env var is missing; DuckDB often
- tries to utilize the user's home directory and this works around that.
- """
- logger.debug("creating view metadata.current_records")
-
- # SQL for the current records logic (CTEs)
- current_records_query = """
- with
- -- CTE of run_timestamp for last source full run
- cr_source_last_full as (
- select
- source,
- max(run_timestamp) as last_full_ts
- from metadata.records
- where run_type = 'full'
- group by source
- ),
-
- -- CTE of all records, per source, on or after last full run
- cr_since_last_full as (
- select
- r.*
- from metadata.records r
- join cr_source_last_full f using (source)
- where r.run_timestamp >= f.last_full_ts
- ),
-
- -- CTE of records ranked by run_timestamp
- cr_ranked_records as (
- select
- r.*,
- row_number() over (
- partition by r.source, r.timdex_record_id
- order by
- r.run_timestamp desc nulls last,
- r.run_id desc nulls last,
- r.run_record_offset desc nulls last
- ) as rn
- from cr_since_last_full r
- )
+ if table_config.query_sql is None:
+ raise ValueError(
+ f"Custom metadata table '{table_config.name}' must define query_sql."
+ )
- -- final select for current records (rn = 1)
- select
- * exclude (rn)
- from cr_ranked_records
- where rn = 1
- """
+ logger.debug(f"creating view metadata.{table_config.name}")
- # create temp table (materializes in memory)
- if self.preload_current_records:
- logger.debug("creating temp table temp.main.current_records")
+ if self._should_preload_table(table_config):
+ logger.debug(f"creating temp table temp.main.{table_config.name}")
conn.execute("set temp_directory = '/tmp';")
conn.execute(f"""
- create or replace temp table temp.main.current_records as
- {current_records_query};
+ create or replace temp table temp.main.{table_config.name} as
+ {table_config.query_sql};
- -- create view in metadata schema that points to temp table
- create or replace view metadata.current_records as
- select * from temp.main.current_records;
- """)
+ create or replace view metadata.{table_config.name} as
+ select * from temp.main.{table_config.name};
+ """)
+ return
- # create view only (lazy evaluation)
- else:
- conn.execute(f"""
- create or replace view metadata.current_records as
- {current_records_query};
- """)
+ conn.execute(f"""
+ create or replace view metadata.{table_config.name} as
+ {table_config.query_sql};
+ """)
+
+ def _should_preload_table(self, table_config: "DataSourceTableConfig") -> bool:
+ """Return True when a table config is configured for temp-table preloading."""
+ if table_config.preload_setting_attribute is None:
+ return False
+ return bool(
+ getattr(self.timdex_dataset, table_config.preload_setting_attribute, False)
+ )
+
+ def _append_delta_count(
+ self, conn: DuckDBPyConnection, source_class: type["TIMDEXDataSource"]
+ ) -> int:
+ """Return append delta parquet file count for a single data source."""
+ deltas_glob = f"{self.append_deltas_path_for(source_class)}/*.parquet"
+
+ try:
+ return cast(
+ "int",
+ conn.execute(f"""
+ select count(*) as file_count
+ from glob('{deltas_glob}')
+ """).fetchone()[0], # type: ignore[index]
+ )
+ except (DuckDBHTTPException, DuckDBIOException):
+ logger.debug(
+ "Could not inspect append deltas for "
+ f"'{source_class.NAME}' at '{deltas_glob}'; assuming none exist."
+ )
+ return 0
+
+ def _metadata_table_exists(self, conn: DuckDBPyConnection, table_name: str) -> bool:
+ """Return True if a metadata schema table or view exists by name."""
+ table_exists = conn.execute(f"""
+ select count(*) from information_schema.tables
+ where table_schema = 'metadata'
+ and table_name = '{table_name}'
+ """).fetchone()[0] # type: ignore[index]
+ return bool(table_exists)
def merge_append_deltas(self) -> None:
- """Merge append deltas into the static metadata database file."""
+ """Merge append deltas into the static metadata database file.
+
+ Iterates over all data source configs, merging each source's deltas into its
+ corresponding table in the static database.
+ """
logger.info("merging append deltas into static metadata database file")
start_time = time.perf_counter()
s3_client = S3Client()
- # get filenames of append deltas
- append_delta_filenames = (
- self.conn.query("""
- select distinct(append_delta_filename)
- from metadata.append_deltas
- """)
- .to_df()["append_delta_filename"]
- .to_list()
- )
-
- if len(append_delta_filenames) == 0:
+ # collect all append delta filenames across all sources
+ all_delta_filenames: dict[str, list[str]] = {}
+ has_any_deltas = False
+ for source_class in self.source_classes:
+ deltas_view = f"{source_class.NAME}_append_deltas"
+ try:
+ filenames = (
+ self.timdex_dataset.conn.query(f"""
+ select distinct(append_delta_filename)
+ from metadata.{deltas_view}
+ """)
+ .to_df()["append_delta_filename"]
+ .to_list()
+ )
+ except (
+ DuckDBIOException,
+ DuckDBCatalogException,
+ DuckDBBinderException,
+ KeyError,
+ ):
+ filenames = []
+ all_delta_filenames[source_class.NAME] = filenames
+ if filenames:
+ has_any_deltas = True
+
+ if not has_any_deltas:
logger.info("no append deltas found")
return
- logger.debug(f"{len(append_delta_filenames)} append deltas found")
-
with tempfile.TemporaryDirectory() as temp_dir:
# create local copy of the static metadata database (static db) file
local_db_path = str(Path(temp_dir) / self.metadata_database_filename)
- if self.location_scheme == "s3":
+ if self.timdex_dataset.location_scheme == "s3":
s3_client.download_file(
s3_uri=self.metadata_database_path, local_path=local_db_path
)
@@ -405,21 +606,21 @@ def merge_append_deltas(self) -> None:
shutil.copy(src=self.metadata_database_path, dst=local_db_path)
# attach to local static db
- self.conn.execute(f"""attach '{local_db_path}' AS local_static_db;""")
+ self.timdex_dataset.conn.execute(
+ f"""attach '{local_db_path}' AS local_static_db;"""
+ )
- # insert records from append deltas to local static db
- self.conn.execute(f"""
- insert into local_static_db.records
- select
- {",".join(ORDERED_METADATA_COLUMN_NAMES)}
- from metadata.append_deltas
- """)
+ # merge deltas for each data source
+ for source_class in self.source_classes:
+ if not all_delta_filenames[source_class.NAME]:
+ continue
+ self._merge_deltas_for_type(source_class)
# detach from local static db
- self.conn.execute("""detach local_static_db;""")
+ self.timdex_dataset.conn.execute("""detach local_static_db;""")
# overwrite static db file with local version
- if self.location_scheme == "s3":
+ if self.timdex_dataset.location_scheme == "s3":
s3_client.upload_file(
local_db_path,
self.metadata_database_path,
@@ -427,40 +628,77 @@ def merge_append_deltas(self) -> None:
else:
shutil.copy(src=local_db_path, dst=self.metadata_database_path)
- # delete append deltas
- for append_delta_filename in append_delta_filenames:
- if self.location_scheme == "s3":
- s3_client.delete_file(s3_uri=append_delta_filename)
- else:
- os.remove(append_delta_filename)
+ # delete append deltas for all sources
+ for source_class in self.source_classes:
+ for delta_filename in all_delta_filenames[source_class.NAME]:
+ if self.timdex_dataset.location_scheme == "s3":
+ s3_client.delete_file(s3_uri=delta_filename)
+ else:
+ os.remove(delta_filename)
logger.debug(
"append deltas merged into the static metadata database file: "
f"{self.metadata_database_path}, {time.perf_counter() - start_time}s"
)
- def write_append_delta_duckdb(self, filepath: str) -> None:
- """Write an append delta for an ETL parquet file.
+ def _merge_deltas_for_type(self, source_class: type["TIMDEXDataSource"]) -> None:
+ """Insert rows from append deltas into the local static DB for one data source."""
+ columns = ",".join(source_class.SOURCE_METADATA_COLUMNS)
+ deltas_view = f"metadata.{source_class.NAME}_append_deltas"
+
+ logger.debug(f"merging append deltas for '{source_class.NAME}'")
+
+ # if type table doesn't yet exist in static DB, initialize it from deltas schema
+ table_exists = self.timdex_dataset.conn.execute(f"""
+ select count(*) from information_schema.tables
+ where table_catalog = 'local_static_db'
+ and table_name = '{source_class.NAME}'
+ """).fetchone()[0] # type: ignore[index]
+
+ if not table_exists:
+ self.timdex_dataset.conn.execute(f"""
+ create table local_static_db.{source_class.NAME} as
+ select {columns}
+ from {deltas_view}
+ where 1 = 0
+ """)
+
+ self.timdex_dataset.conn.execute(f"""
+ insert into local_static_db.{source_class.NAME}
+ select {columns}
+ from {deltas_view}
+ """)
+
+ def write_append_delta(
+ self,
+ filepath: str,
+ source_class: type["TIMDEXDataSource"],
+ ) -> None:
+ """Write an append delta for a parquet file.
- A DuckDB context is used to both read metadata-only columns from the ETL parquet
- file, then write an append delta parquet file to /metadata/append_deltas. The
- write is performed by DuckDB's COPY function.
+ A DuckDB context is used to read metadata-only columns from the parquet
+ file, then write an append delta parquet file to
+ ``metadata/append_deltas/{source_class.NAME}/``.
Note: this operation is safe in parallel with other possible append delta writes.
+
+ Args:
+ filepath: path to the parquet file to extract metadata from
+ source_class: the data source class owning this parquet file
"""
start_time = time.perf_counter()
- output_path = f"{self.append_deltas_path}/append_delta-{filepath.split('/')[-1]}"
+ deltas_path = self.append_deltas_path_for(source_class)
+ output_path = f"{deltas_path}/append_delta-{filepath.split('/')[-1]}"
# ensure s3:// schema prefix is present
- if self.location_scheme == "s3":
+ if self.timdex_dataset.location_scheme == "s3":
filepath = f"s3://{filepath.removeprefix('s3://')}"
- # perform query + write as one SQL statement
sql = f"""
copy (
select
- {",".join(ORDERED_METADATA_COLUMN_NAMES)}
+ {",".join(source_class.SOURCE_METADATA_COLUMNS)}
from read_parquet(
'{filepath}',
hive_partitioning=true,
@@ -469,7 +707,7 @@ def write_append_delta_duckdb(self, filepath: str) -> None:
) to '{output_path}'
(FORMAT parquet);
"""
- self.conn.execute(sql)
+ self.timdex_dataset.conn.execute(sql)
logger.debug(
f"Append delta written: {output_path}, {time.perf_counter() - start_time}s"
@@ -482,20 +720,42 @@ def build_keyset_paginated_metadata_query(
limit: int | None = None,
where: str | None = None,
keyset_value: tuple[int, int, int] = (0, 0, 0),
- **filters: Unpack["DatasetFilters"],
+ **filters: Unpack["RecordsFilters"],
) -> str:
- """Build SQL query using SQLAlchemy against metadata schema tables and views."""
+ """Build SQL query using SQLAlchemy against metadata schema tables and views.
+
+ Args:
+ table: metadata table/view name
+ limit: max rows to return
+ where: raw SQL WHERE clause
+ keyset_value: tuple of (filename_hash, run_id_hash, run_record_offset)
+ for keyset pagination
+ **filters: key/value filter pairs
+ """
sa_table = self.timdex_dataset.get_sa_table("metadata", table)
+ required_keyset_columns = {"filename", "run_id", "run_record_offset"}
+ missing_keyset_columns = required_keyset_columns - set(sa_table.c.keys())
+ if missing_keyset_columns:
+ missing = ", ".join(sorted(missing_keyset_columns))
+ raise ValueError(
+ f"Table '{table}' missing required keyset column(s): {missing}"
+ )
+
+ metadata_columns = self.get_metadata_columns_for_table(table)
+
# create SQL statement object
- stmt = select(
- sa_table.c.timdex_record_id,
- sa_table.c.run_id,
- func.hash(sa_table.c.run_id).label("run_id_hash"),
- sa_table.c.run_record_offset,
- sa_table.c.filename,
- func.hash(sa_table.c.filename).label("filename_hash"),
- ).select_from(sa_table)
+ select_columns: list[Any] = [
+ sa_table.c[column_name] for column_name in metadata_columns
+ ]
+ select_columns.extend(
+ [
+ func.hash(sa_table.c.run_id).label("run_id_hash"),
+ func.hash(sa_table.c.filename).label("filename_hash"),
+ ]
+ )
+
+ stmt = select(*select_columns).select_from(sa_table)
# filter expressions from key/value filters (may return None)
filter_expr = build_filter_expr_sa(sa_table, **cast("dict", filters))
@@ -507,7 +767,7 @@ def build_keyset_paginated_metadata_query(
stmt = stmt.where(text(where))
# keyset pagination
- filename_has, run_id_hash, run_record_offset_ = keyset_value
+ filename_hash, run_id_hash, run_record_offset_ = keyset_value
stmt = stmt.where(
tuple_(
func.hash(sa_table.c.filename),
@@ -515,7 +775,7 @@ def build_keyset_paginated_metadata_query(
sa_table.c.run_record_offset,
)
> tuple_(
- literal(filename_has),
+ literal(filename_hash),
literal(run_id_hash),
literal(run_record_offset_),
)
diff --git a/timdex_dataset_api/record.py b/timdex_dataset_api/record.py
deleted file mode 100644
index 21a62c4..0000000
--- a/timdex_dataset_api/record.py
+++ /dev/null
@@ -1,69 +0,0 @@
-"""timdex_dataset_api/record.py"""
-
-from datetime import UTC, date, datetime
-
-import attrs
-from attrs import asdict, define, field
-
-
-def strict_date_parse(date_string: str) -> date:
- return datetime.strptime(date_string, "%Y-%m-%d").astimezone(UTC).date()
-
-
-def datetime_iso_parse(datetime_iso_string: str) -> datetime:
- parsed_datetime = datetime.fromisoformat(datetime_iso_string)
- # if timezone not present, set as UTC and return
- if parsed_datetime.tzinfo is None:
- return parsed_datetime.replace(tzinfo=UTC)
- # else, convert to / ensure UTC and return
- return parsed_datetime.astimezone(UTC)
-
-
-@define
-class DatasetRecord:
- """Container for single dataset record.
-
- An iterator of these are passed to the TIMDEXDataset.write() method, where they are
- first serialized into dictionaries, and then grouped into pyarrow.RecordBatches for
- writing.
- """
-
- timdex_record_id: str = field()
- source_record: bytes = field()
- transformed_record: bytes = field()
- source: str = field()
- run_date: date = field(converter=strict_date_parse)
- run_type: str = field()
- action: str = field()
- run_id: str = field()
- run_timestamp: datetime = field(
- converter=datetime_iso_parse,
- default=attrs.Factory(
- lambda self: self.run_date.isoformat(),
- takes_self=True,
- ),
- )
- run_record_offset: int = field(default=None)
-
- @property
- def year(self) -> str:
- return self.run_date.strftime("%Y")
-
- @property
- def month(self) -> str:
- return self.run_date.strftime("%m")
-
- @property
- def day(self) -> str:
- return self.run_date.strftime("%d")
-
- def to_dict(
- self,
- ) -> dict:
- """Serialize instance as dictionary."""
- return {
- **asdict(self),
- "year": self.year,
- "month": self.month,
- "day": self.day,
- }
diff --git a/timdex_dataset_api/records.py b/timdex_dataset_api/records.py
new file mode 100644
index 0000000..4c94a5b
--- /dev/null
+++ b/timdex_dataset_api/records.py
@@ -0,0 +1,189 @@
+"""timdex_dataset_api/records.py"""
+
+import json
+from collections.abc import Iterator
+from datetime import date, datetime
+from typing import ClassVar, TypedDict, Unpack
+
+import attrs
+import pyarrow as pa
+from attrs import asdict, define, field
+
+from timdex_dataset_api.data_source import DataSourceTableConfig, TIMDEXDataSource
+from timdex_dataset_api.utils import (
+ datetime_iso_parse,
+ strict_date_parse,
+)
+
+
+class RecordsFilters(TypedDict, total=False):
+ timdex_record_id: str | list[str] | None
+ source: str | list[str] | None
+ run_date: str | date | list[str | date] | None
+ run_type: str | list[str] | None
+ action: str | list[str] | None
+ run_id: str | list[str] | None
+ run_record_offset: int | list[int] | None
+ run_timestamp: str | datetime | list[str | datetime] | None
+
+
+@define
+class DatasetRecord:
+ """Container for single dataset record.
+
+ An iterator of these are passed to the TIMDEXRecords.write() method, where they are
+ first serialized into dictionaries, and then grouped into pyarrow.RecordBatches for
+ writing.
+ """
+
+ timdex_record_id: str = field()
+ source_record: bytes = field()
+ transformed_record: bytes = field()
+ source: str = field()
+ run_date: date = field(converter=strict_date_parse)
+ run_type: str = field()
+ action: str = field()
+ run_id: str = field()
+ run_timestamp: datetime = field(
+ converter=datetime_iso_parse,
+ default=attrs.Factory(
+ lambda self: self.run_date.isoformat(),
+ takes_self=True,
+ ),
+ )
+ run_record_offset: int = field(default=None)
+
+ @property
+ def year(self) -> str:
+ return self.run_date.strftime("%Y")
+
+ @property
+ def month(self) -> str:
+ return self.run_date.strftime("%m")
+
+ @property
+ def day(self) -> str:
+ return self.run_date.strftime("%d")
+
+ def to_dict(
+ self,
+ ) -> dict:
+ """Serialize instance as dictionary."""
+ return {
+ **asdict(self),
+ "year": self.year,
+ "month": self.month,
+ "day": self.day,
+ }
+
+
+class TIMDEXRecords(TIMDEXDataSource):
+ """Class to handle records in the TIMDEXDataset."""
+
+ NAME: ClassVar[str] = "records"
+
+ SCHEMA: ClassVar[pa.Schema] = pa.schema(
+ (
+ pa.field("timdex_record_id", pa.string()),
+ pa.field("source_record", pa.binary()),
+ pa.field("transformed_record", pa.binary()),
+ pa.field("source", pa.string()),
+ pa.field("run_date", pa.date32()),
+ pa.field("run_type", pa.string()),
+ pa.field("action", pa.string()),
+ pa.field("run_id", pa.string()),
+ pa.field("run_record_offset", pa.int32()),
+ pa.field("year", pa.string()),
+ pa.field("month", pa.string()),
+ pa.field("day", pa.string()),
+ pa.field("run_timestamp", pa.timestamp("us", tz="UTC")),
+ )
+ )
+
+ DATA_COLUMNS: ClassVar[list[str]] = [
+ "source_record",
+ "transformed_record",
+ ]
+
+ DATA_PATH: ClassVar[str] = "data/records"
+
+ PREJOIN_RECORDS: ClassVar[bool] = False
+
+ CURRENT_METADATA_VIEW_QUERY: ClassVar[str] = """
+ with
+ -- CTE of run_timestamp for last source full run
+ cr_source_last_full as (
+ select
+ source,
+ max(run_timestamp) as last_full_ts
+ from metadata.records
+ where run_type = 'full'
+ group by source
+ ),
+
+ -- CTE of all records, per source, on or after last full run
+ cr_since_last_full as (
+ select
+ r.*
+ from metadata.records r
+ join cr_source_last_full f using (source)
+ where r.run_timestamp >= f.last_full_ts
+ ),
+
+ -- CTE of records ranked by run_timestamp
+ cr_ranked_records as (
+ select
+ r.*,
+ row_number() over (
+ partition by r.source, r.timdex_record_id
+ order by
+ r.run_timestamp desc nulls last,
+ r.run_id desc nulls last,
+ r.run_record_offset desc nulls last
+ ) as rn
+ from cr_since_last_full r
+ )
+
+ -- final select for current records (rn = 1)
+ select
+ * exclude (rn)
+ from cr_ranked_records
+ where rn = 1
+ """
+
+ TABLES: ClassVar[list[DataSourceTableConfig]] = [
+ DataSourceTableConfig(
+ name="records",
+ description="All record versions across all runs.",
+ kind="base",
+ ),
+ DataSourceTableConfig(
+ name="current_records",
+ description=(
+ "One row per (source, timdex_record_id) representing the "
+ "most recent version of each record since the last full run."
+ ),
+ kind="custom",
+ query_sql=CURRENT_METADATA_VIEW_QUERY,
+ required_metadata_tables=["records"],
+ preload_setting_attribute="preload_current_records",
+ ),
+ ]
+
+ def read_transformed_records_iter(
+ self,
+ table: str = "records",
+ limit: int | None = None,
+ where: str | None = None,
+ **filters: Unpack[RecordsFilters],
+ ) -> Iterator[dict]:
+ """Custom read method to yield parsed transformed TIMDEX JSON records only."""
+ for record_dict in self.read_dicts_iter(
+ table=table,
+ columns=["transformed_record"],
+ limit=limit,
+ where=where,
+ **filters,
+ ):
+ if transformed_record := record_dict["transformed_record"]:
+ yield json.loads(transformed_record)
diff --git a/timdex_dataset_api/utils.py b/timdex_dataset_api/utils.py
index 0d7c16e..627fb8d 100644
--- a/timdex_dataset_api/utils.py
+++ b/timdex_dataset_api/utils.py
@@ -289,3 +289,14 @@ def build_filter_expr_sa(
if predicates:
return and_(*predicates)
return None
+
+
+def strict_date_parse(date_string: str) -> date:
+ return datetime.strptime(date_string, "%Y-%m-%d").astimezone(UTC).date()
+
+
+def datetime_iso_parse(datetime_iso_string: str) -> datetime:
+ parsed_datetime = datetime.fromisoformat(datetime_iso_string)
+ if parsed_datetime.tzinfo is None:
+ return parsed_datetime.replace(tzinfo=UTC)
+ return parsed_datetime.astimezone(UTC)
diff --git a/uv.lock b/uv.lock
index 7f451a5..14e13d4 100644
--- a/uv.lock
+++ b/uv.lock
@@ -22,11 +22,11 @@ wheels = [
[[package]]
name = "attrs"
-version = "25.4.0"
+version = "26.1.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
+ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
]
[[package]]
@@ -40,29 +40,29 @@ wheels = [
[[package]]
name = "boto3"
-version = "1.42.68"
+version = "1.42.89"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/06/ae/60c642aa5413e560b671da825329f510b29a77274ed0f580bde77562294d/boto3-1.42.68.tar.gz", hash = "sha256:3f349f967ab38c23425626d130962bcb363e75f042734fe856ea8c5a00eef03c", size = 112761, upload-time = "2026-03-13T19:32:17.137Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/bb/0c/f7bccb22b245cabf392816baba20f9e95f78ace7dbc580fd40136e80e732/boto3-1.42.89.tar.gz", hash = "sha256:3e43aacc0801bba9bcd23a8c271c089af297a69565f783fcdd357ae0e330bf1e", size = 113165, upload-time = "2026-04-13T19:36:17.516Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fb/f6/dc6e993479dbb597d68223fbf61cb026511737696b15bd7d2a33e9b2c24f/boto3-1.42.68-py3-none-any.whl", hash = "sha256:dbff353eb7dc93cbddd7926ed24793e0174c04adbe88860dfa639568442e4962", size = 140556, upload-time = "2026-03-13T19:32:14.951Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/33/55103ba5ef9975ea54b8d39e69b76eb6e9fded3beae5f01065e26951a3a1/boto3-1.42.89-py3-none-any.whl", hash = "sha256:6204b189f4d0c655535f43d7eaa57ff4e8d965b8463c97e45952291211162932", size = 140556, upload-time = "2026-04-13T19:36:13.894Z" },
]
[[package]]
name = "boto3-stubs"
-version = "1.42.68"
+version = "1.42.89"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore-stubs" },
{ name = "types-s3transfer" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/4c/8c/dd4b0c95ff008bed5a35ab411452ece121b355539d2a0b6dcd62a0c47be5/boto3_stubs-1.42.68.tar.gz", hash = "sha256:96ad1020735619483fb9b4da7a5e694b460bf2e18f84a34d5d175d0ffe8c4653", size = 101372, upload-time = "2026-03-13T19:49:54.867Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/7d/2ea3bb15bba37e6d70f9297dccd1ce769b7b92f6179baa890293995f359b/boto3_stubs-1.42.89.tar.gz", hash = "sha256:dbbc4fd2678cfb21da9bab1b5e30ba951852322d055045ac12042ba34d04597a", size = 102703, upload-time = "2026-04-13T19:51:49.673Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/68/15/3ca5848917214a168134512a5b45f856a56e913659888947a052e02031b5/boto3_stubs-1.42.68-py3-none-any.whl", hash = "sha256:ed7f98334ef7b2377fa8532190e63dc2c6d1dc895e3d7cb3d6d1c83771b81bf6", size = 70011, upload-time = "2026-03-13T19:49:42.801Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/5d/397d2393cba13b6764da0da0aee5a16d05b304de081dfec1be69829ef0d4/boto3_stubs-1.42.89-py3-none-any.whl", hash = "sha256:699e510078a057766e2de1d2d91d99dac2ce3ca2d4e6adf8df27b305d04b91d2", size = 70667, upload-time = "2026-04-13T19:51:43.131Z" },
]
[package.optional-dependencies]
@@ -72,16 +72,16 @@ s3 = [
[[package]]
name = "botocore"
-version = "1.42.68"
+version = "1.42.89"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/3f/22/87502d5fbbfa8189406a617b30b1e2a3dc0ab2669f7268e91b385c1c1c7a/botocore-1.42.68.tar.gz", hash = "sha256:3951c69e12ac871dda245f48dac5c7dd88ea1bfdd74a8879ec356cf2874b806a", size = 14994514, upload-time = "2026-03-13T19:32:03.577Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/cc/e6be943efa9051bd15c2ee14077c2b10d6e27c9e9385fc43a03a5c4ed8b5/botocore-1.42.89.tar.gz", hash = "sha256:95ac52f472dad29942f3088b278ab493044516c16dbf9133c975af16527baa99", size = 15206290, upload-time = "2026-04-13T19:36:02.321Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3c/2a/1428f6594799780fe6ee845d8e6aeffafe026cd16a70c878684e2dcbbfc8/botocore-1.42.68-py3-none-any.whl", hash = "sha256:9df7da26374601f890e2f115bfa573d65bf15b25fe136bb3aac809f6145f52ab", size = 14668816, upload-time = "2026-03-13T19:31:58.572Z" },
+ { url = "https://files.pythonhosted.org/packages/91/f1/90a7b8eda38b7c3a65ca7ee0075bdf310b6b471cb1b95fab6e8994323a50/botocore-1.42.89-py3-none-any.whl", hash = "sha256:d9b786c8d9db6473063b4cc5be0ba7e6a381082307bd6afb69d4216f9fa95f35", size = 14887287, upload-time = "2026-04-13T19:35:56.677Z" },
]
[[package]]
@@ -191,71 +191,87 @@ wheels = [
[[package]]
name = "charset-normalizer"
-version = "3.4.5"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976, upload-time = "2026-03-06T06:01:15.276Z" },
- { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356, upload-time = "2026-03-06T06:01:16.511Z" },
- { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369, upload-time = "2026-03-06T06:01:17.853Z" },
- { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285, upload-time = "2026-03-06T06:01:19.473Z" },
- { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274, upload-time = "2026-03-06T06:01:20.728Z" },
- { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715, upload-time = "2026-03-06T06:01:22.194Z" },
- { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426, upload-time = "2026-03-06T06:01:23.795Z" },
- { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780, upload-time = "2026-03-06T06:01:25.053Z" },
- { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805, upload-time = "2026-03-06T06:01:26.294Z" },
- { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342, upload-time = "2026-03-06T06:01:27.55Z" },
- { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661, upload-time = "2026-03-06T06:01:29.051Z" },
- { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819, upload-time = "2026-03-06T06:01:30.298Z" },
- { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080, upload-time = "2026-03-06T06:01:31.478Z" },
- { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630, upload-time = "2026-03-06T06:01:33.056Z" },
- { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856, upload-time = "2026-03-06T06:01:34.489Z" },
- { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982, upload-time = "2026-03-06T06:01:35.641Z" },
- { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" },
- { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" },
- { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" },
- { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" },
- { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" },
- { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" },
- { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" },
- { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" },
- { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" },
- { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" },
- { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" },
- { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" },
- { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" },
- { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" },
- { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" },
- { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" },
- { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" },
- { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" },
- { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" },
- { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" },
- { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" },
- { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" },
- { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" },
- { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" },
- { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" },
- { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" },
- { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" },
- { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" },
- { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" },
- { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" },
- { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" },
- { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" },
- { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" },
+version = "3.4.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" },
+ { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" },
+ { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" },
+ { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" },
+ { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" },
+ { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
+ { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
+ { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
+ { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
+ { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
+ { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
+ { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
+ { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
+ { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
+ { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
+ { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
+ { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
+ { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
+ { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
+ { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
+ { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
+ { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
+ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
]
[[package]]
name = "click"
-version = "8.3.1"
+version = "8.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },
]
[[package]]
@@ -269,86 +285,86 @@ wheels = [
[[package]]
name = "coverage"
-version = "7.13.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" },
- { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" },
- { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" },
- { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" },
- { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" },
- { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" },
- { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" },
- { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" },
- { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" },
- { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" },
- { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" },
- { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" },
- { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" },
- { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" },
- { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" },
- { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" },
- { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" },
- { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" },
- { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" },
- { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" },
- { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" },
- { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" },
- { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" },
- { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" },
- { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" },
- { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" },
- { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" },
- { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" },
- { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" },
- { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" },
- { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" },
- { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" },
- { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" },
- { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" },
- { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" },
- { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" },
- { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" },
- { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" },
- { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" },
- { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" },
- { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" },
- { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" },
- { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" },
- { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" },
- { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" },
- { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" },
- { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" },
- { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" },
- { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" },
- { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" },
- { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" },
- { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" },
- { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" },
- { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" },
- { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" },
- { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" },
- { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" },
- { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" },
- { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" },
- { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" },
- { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" },
- { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" },
- { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" },
- { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" },
- { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" },
- { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" },
- { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" },
- { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" },
- { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" },
- { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" },
- { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" },
- { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" },
- { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" },
- { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" },
- { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" },
- { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" },
+version = "7.13.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" },
+ { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" },
+ { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" },
+ { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" },
+ { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" },
+ { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" },
+ { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" },
+ { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" },
+ { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" },
+ { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" },
+ { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" },
+ { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" },
+ { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" },
+ { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" },
+ { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" },
+ { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" },
+ { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" },
+ { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" },
+ { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" },
+ { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" },
+ { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" },
+ { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" },
+ { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" },
+ { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" },
+ { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" },
]
[[package]]
@@ -367,60 +383,60 @@ wheels = [
[[package]]
name = "cryptography"
-version = "46.0.5"
+version = "46.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
- { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
- { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
- { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
- { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
- { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
- { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
- { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
- { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
- { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
- { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
- { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
- { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
- { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
- { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" },
- { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" },
- { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" },
- { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" },
- { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" },
- { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" },
- { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" },
- { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" },
- { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" },
- { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" },
- { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" },
- { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" },
- { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" },
- { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" },
- { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
- { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
- { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
- { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
- { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
- { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
- { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
- { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
- { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
- { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
- { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
- { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
- { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
- { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
+ { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
+ { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
+ { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" },
+ { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
+ { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
+ { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
+ { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
+ { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
+ { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
+ { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
+ { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" },
+ { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
+ { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
+ { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
+ { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
+ { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
]
[[package]]
name = "cyclonedx-python-lib"
-version = "11.6.0"
+version = "11.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "license-expression" },
@@ -429,9 +445,9 @@ dependencies = [
{ name = "sortedcontainers" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/89/ed/54ecfa25fc145c58bf4f98090f7b6ffe5188d0759248c57dde44427ea239/cyclonedx_python_lib-11.6.0.tar.gz", hash = "sha256:7fb85a4371fa3a203e5be577ac22b7e9a7157f8b0058b7448731474d6dea7bf0", size = 1408147, upload-time = "2025-12-02T12:28:46.446Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/0d/64f02d3fd9c116d6f50a540d04d1e4f2e3c487f5062d2db53733ddb25917/cyclonedx_python_lib-11.7.0.tar.gz", hash = "sha256:fb1bc3dedfa31208444dbd743007f478ab6984010a184e5bd466bffd969e936e", size = 1411174, upload-time = "2026-03-17T15:19:16.606Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/1b/534ad8a5e0f9470522811a8e5a9bc5d328fb7738ba29faf357467a4ef6d0/cyclonedx_python_lib-11.6.0-py3-none-any.whl", hash = "sha256:94f4aae97db42a452134dafdddcfab9745324198201c4777ed131e64c8380759", size = 511157, upload-time = "2025-12-02T12:28:44.158Z" },
+ { url = "https://files.pythonhosted.org/packages/30/09/fe0e3bc32bd33707c519b102fc064ad2a2ce5a1b53e2be38b86936b476b1/cyclonedx_python_lib-11.7.0-py3-none-any.whl", hash = "sha256:02fa4f15ddbba21ac9093039f8137c0d1813af7fe88b760c5dcd3311a8da2178", size = 513041, upload-time = "2026-03-17T15:19:14.369Z" },
]
[[package]]
@@ -463,31 +479,31 @@ wheels = [
[[package]]
name = "duckdb"
-version = "1.5.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ee/11/e05a7eb73a373d523e45d83c261025e02bc31ebf868e6282c30c4d02cc59/duckdb-1.5.0.tar.gz", hash = "sha256:f974b61b1c375888ee62bc3125c60ac11c4e45e4457dd1bb31a8f8d3cf277edd", size = 17981141, upload-time = "2026-03-09T12:50:26.372Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/43/73/120e673e48ae25aaf689044c25ef51b0ea1d088563c9a2532612aea18e0a/duckdb-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9ea988d1d5c8737720d1b2852fd70e4d9e83b1601b8896a1d6d31df5e6afc7dd", size = 30057869, upload-time = "2026-03-09T12:49:14.65Z" },
- { url = "https://files.pythonhosted.org/packages/21/e9/61143471958d36d3f3e764cb4cd43330be208ddbff1c78d3310b9ee67fe8/duckdb-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb786d5472afc16cc3c7355eb2007172538311d6f0cc6f6a0859e84a60220375", size = 15963092, upload-time = "2026-03-09T12:49:17.478Z" },
- { url = "https://files.pythonhosted.org/packages/4f/71/76e37c9a599ad89dd944e6cbb3e6a8ad196944a421758e83adea507637b6/duckdb-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dc92b238f4122800a7592e99134124cc9048c50f766c37a0778dd2637f5cbe59", size = 14220562, upload-time = "2026-03-09T12:49:23.518Z" },
- { url = "https://files.pythonhosted.org/packages/db/b8/de1831656d5d13173e27c79c7259c8b9a7bdc314fdc8920604838ea4c46d/duckdb-1.5.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b74cb205c21d3696d8f8b88adca401e1063d6e6f57c1c4f56a243610b086e30", size = 19245329, upload-time = "2026-03-09T12:49:26.307Z" },
- { url = "https://files.pythonhosted.org/packages/1f/8d/33d349a3bcbd3e9b7b4e904c19d5b97f058c4c20791b89a8d6323bb93dce/duckdb-1.5.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e56c19ffd1ffe3642fa89639e71e2e00ab0cf107b62fe16e88030acaebcbde6", size = 21348041, upload-time = "2026-03-09T12:49:30.283Z" },
- { url = "https://files.pythonhosted.org/packages/e2/ec/591a4cad582fae04bc8f8b4a435eceaaaf3838cf0ca771daae16a3c2995b/duckdb-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:86525e565ec0c43420106fd34ba2c739a54c01814d476c7fed3007c9ed6efd86", size = 13053781, upload-time = "2026-03-09T12:49:33.574Z" },
- { url = "https://files.pythonhosted.org/packages/db/62/42e0a13f9919173bec121c0ff702406e1cdd91d8084c3e0b3412508c3891/duckdb-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:5faeebc178c986a7bfa68868a023001137a95a1110bf09b7356442a4eae0f7e7", size = 13862906, upload-time = "2026-03-09T12:49:36.598Z" },
- { url = "https://files.pythonhosted.org/packages/35/5d/af5501221f42e4e3662c047ecec4dcd0761229fceeba3c67ad4d9d8741df/duckdb-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11dd05b827846c87f0ae2f67b9ae1d60985882a7c08ce855379e4a08d5be0e1d", size = 30057396, upload-time = "2026-03-09T12:49:39.95Z" },
- { url = "https://files.pythonhosted.org/packages/43/bd/a278d73fedbd3783bf9aedb09cad4171fe8e55bd522952a84f6849522eb6/duckdb-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ad8d9c91b7c280ab6811f59deff554b845706c20baa28c4e8f80a95690b252b", size = 15962700, upload-time = "2026-03-09T12:49:43.504Z" },
- { url = "https://files.pythonhosted.org/packages/76/fc/c916e928606946209c20fb50898dabf120241fb528a244e2bd8cde1bd9e2/duckdb-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0ee4dabe03ed810d64d93927e0fd18cd137060b81ee75dcaeaaff32cbc816656", size = 14220272, upload-time = "2026-03-09T12:49:46.867Z" },
- { url = "https://files.pythonhosted.org/packages/53/07/1390e69db922423b2e111e32ed342b3e8fad0a31c144db70681ea1ba4d56/duckdb-1.5.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409ed1184b363ddea239609c5926f5148ee412b8d9e5ffa617718d755d942f6", size = 19244401, upload-time = "2026-03-09T12:49:49.865Z" },
- { url = "https://files.pythonhosted.org/packages/54/13/b58d718415cde993823a54952ea511d2612302f1d2bc220549d0cef752a4/duckdb-1.5.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1df8c4f9c853a45f3ec1e79ed7fe1957a203e5ec893bbbb853e727eb93e0090f", size = 21345827, upload-time = "2026-03-09T12:49:52.977Z" },
- { url = "https://files.pythonhosted.org/packages/e0/96/4460429651e371eb5ff745a4790e7fa0509c7a58c71fc4f0f893404c9646/duckdb-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:9a3d3dfa2d8bc74008ce3ad9564761ae23505a9e4282f6a36df29bd87249620b", size = 13053101, upload-time = "2026-03-09T12:49:56.134Z" },
- { url = "https://files.pythonhosted.org/packages/ba/54/6d5b805113214b830fa3c267bb3383fb8febaa30760d0162ef59aadb110a/duckdb-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:2deebcbafd9d39c04f31ec968f4dd7cee832c021e10d96b32ab0752453e247c8", size = 13865071, upload-time = "2026-03-09T12:49:59.282Z" },
- { url = "https://files.pythonhosted.org/packages/66/9f/dd806d4e8ecd99006eb240068f34e1054533da1857ad06ac726305cd102d/duckdb-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:d4b618de670cd2271dd7b3397508c7b3c62d8ea70c592c755643211a6f9154fa", size = 30065704, upload-time = "2026-03-09T12:50:02.671Z" },
- { url = "https://files.pythonhosted.org/packages/79/c2/7b7b8a5c65d5535c88a513e267b5e6d7a55ab3e9b67e4ddd474454653268/duckdb-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:065ae50cb185bac4b904287df72e6b4801b3bee2ad85679576dd712b8ba07021", size = 15964883, upload-time = "2026-03-09T12:50:06.343Z" },
- { url = "https://files.pythonhosted.org/packages/23/c5/9a52a2cdb228b8d8d191a603254364d929274d9cc7d285beada8f7daa712/duckdb-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6be5e48e287a24d98306ce9dd55093c3b105a8fbd8a2e7a45e13df34bf081985", size = 14221498, upload-time = "2026-03-09T12:50:10.567Z" },
- { url = "https://files.pythonhosted.org/packages/b8/68/646045cb97982702a8a143dc2e45f3bdcb79fbe2d559a98d74b8c160e5e2/duckdb-1.5.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5ee41a0bf793882f02192ce105b9a113c3e8c505a27c7ef9437d7b756317113", size = 19249787, upload-time = "2026-03-09T12:50:13.524Z" },
- { url = "https://files.pythonhosted.org/packages/15/1b/5abf0c7f38febb3b4a231c784223fceccfd3f2bfd957699d786f46e41ce6/duckdb-1.5.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8e42aaf3cd217417c5dc9ff522dc3939d18b25a6fe5f846348277e831e6f59c", size = 21351583, upload-time = "2026-03-09T12:50:16.701Z" },
- { url = "https://files.pythonhosted.org/packages/93/a4/a90f2901cc0a1ce7ca4f0564b8492b9dbfe048a6395b27933d46ae9be473/duckdb-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:11ae50aaeda2145b50294ee0247e4f11fb9448b3cc3d2aea1cfc456637dfb977", size = 13575130, upload-time = "2026-03-09T12:50:19.716Z" },
- { url = "https://files.pythonhosted.org/packages/64/aa/f14dd5e241ec80d9f9d82196ca65e0c53badfc8a7a619d5497c5626657ad/duckdb-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:d6d2858c734d1a7e7a1b6e9b8403b3fce26dfefb4e0a2479c420fba6cd36db36", size = 14341879, upload-time = "2026-03-09T12:50:22.347Z" },
+version = "1.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0c/66/744b4931b799a42f8cb9bc7a6f169e7b8e51195b62b246db407fd90bf15f/duckdb-1.5.2.tar.gz", hash = "sha256:638da0d5102b6cb6f7d47f83d0600708ac1d3cb46c5e9aaabc845f9ba4d69246", size = 18017166, upload-time = "2026-04-13T11:30:09.065Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/de/ebe66bbe78125fc610f4fd415447a65349d94245950f3b3dfb31d028af02/duckdb-1.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e6495b00cad16888384119842797c49316a96ae1cb132bb03856d980d95afee1", size = 30064950, upload-time = "2026-04-13T11:29:11.468Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/8a/3e25b5d03bcf1fb99d189912f8ce92b1db4f9c8778e1b1f55745973a855a/duckdb-1.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d72b8856b1839d35648f38301b058f6232f4d36b463fe4dc8f4d3fdff2df1a2e", size = 15969113, upload-time = "2026-04-13T11:29:14.139Z" },
+ { url = "https://files.pythonhosted.org/packages/19/bb/58001f0815002b1a93431bf907f77854085c7d049b83d521814a07b9db0b/duckdb-1.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a1de4f4d454b8c97aec546c82003fc834d3422ce4bc6a19902f3462ef293bed", size = 14224774, upload-time = "2026-04-13T11:29:16.758Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/2f/a7f0de9509d1cef35608aeb382919041cdd70f58c173865c3da6a0d87979/duckdb-1.5.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce0b8141a10d37ecef729c45bc41d334854013f4389f1488bd6035c5579aaac1", size = 19313510, upload-time = "2026-04-13T11:29:19.574Z" },
+ { url = "https://files.pythonhosted.org/packages/26/78/eb1e064ea8b9df3b87b167bfd7a407b2f615a4291e06cba756727adfa06c/duckdb-1.5.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99ef73a277c8921bc0a1f16dee38d924484251d9cfd20951748c20fcd5ed855", size = 21429692, upload-time = "2026-04-13T11:29:22.575Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/12/05b0c47d14839925c5e35b79081d918ca82e3f236bb724a6f58409dd5291/duckdb-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:8d599758b4e48bf12e18c9b960cf491d219f0c4972d19a45489c05cc5ab36f83", size = 13107594, upload-time = "2026-04-13T11:29:25.43Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/2c/80558a82b236e044330e84a154b96aacddb343316b479f3d49be03ea11cb/duckdb-1.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:fc85a5dbcbe6eccac1113c72370d1d3aacfdd49198d63950bdf7d8638a307f00", size = 13927537, upload-time = "2026-04-13T11:29:27.842Z" },
+ { url = "https://files.pythonhosted.org/packages/98/f2/e3d742808f138d374be4bb516fade3d1f33749b813650810ab7885cdc363/duckdb-1.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4420b3f47027a7849d0e1815532007f377fa95ee5810b47ea717d35525c12f79", size = 30064879, upload-time = "2026-04-13T11:29:30.763Z" },
+ { url = "https://files.pythonhosted.org/packages/72/0d/f3dc1cf97e1267ca15e4307d456f96ce583961f0703fd75e62b2ad8d64fa/duckdb-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bb42e6ed543902e14eae647850da24103a89f0bc2587dec5601b1c1f213bd2ed", size = 15969327, upload-time = "2026-04-13T11:29:33.481Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/e0/d5418def53ae4e05a63075705ff44ed5af5a1a5932627eb2b600c5df1c93/duckdb-1.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:98c0535cd6d901f61a5ea3c2e26a1fd28482953d794deb183daf568e3aa5dda6", size = 14225107, upload-time = "2026-04-13T11:29:35.882Z" },
+ { url = "https://files.pythonhosted.org/packages/16/a7/15aaa59dbecc35e9711980fcdbf525b32a52470b32d18ef678193a146213/duckdb-1.5.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:486c862bf7f163c0110b6d85b3e5c031d224a671cca468f12ebb1d3a348f6b39", size = 19313433, upload-time = "2026-04-13T11:29:38.367Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/21/d903cc63a5140c822b7b62b373a87dc557e60c29b321dfb435061c5e67cf/duckdb-1.5.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70631c847ca918ee710ec874241b00cf9d2e5be90762cbb2a0389f17823c08f7", size = 21429837, upload-time = "2026-04-13T11:29:41.135Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/0a/b770d1f60c70597302130d6247f418549b7094251a02348fbaf1c7e147ae/duckdb-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:52a21823f3fbb52f0f0e5425e20b07391ad882464b955879499b5ff0b45a376b", size = 13107699, upload-time = "2026-04-13T11:29:43.905Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/cf/e200fe431d700962d1a908d2ce89f53ccee1cc8db260174ae663ba09686b/duckdb-1.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:411ad438bd4140f189a10e7f515781335962c5d18bd07837dc6d202e3985253d", size = 13927646, upload-time = "2026-04-13T11:29:46.598Z" },
+ { url = "https://files.pythonhosted.org/packages/83/a1/f6286c67726cc1ea60a6e3c0d9fbc66527dde24ae089a51bbe298b13ca78/duckdb-1.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6b0fe75c148000f060aa1a27b293cacc0ea08cc1cad724fbf2143d56070a3785", size = 30078598, upload-time = "2026-04-13T11:29:49.828Z" },
+ { url = "https://files.pythonhosted.org/packages/de/6a/59febb02f21a4a5c6b0b0099ef7c965fdd5e61e4904cf813809bb792e35f/duckdb-1.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35579b8e3a064b5eaf15b0eafc558056a13f79a0a62e34cc4baf57119daecfec", size = 15975120, upload-time = "2026-04-13T11:29:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/09/70/ce750854d37bb5a45cccbb2c3cb04df4af56aea8fc30a2499bb643b4a9c0/duckdb-1.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea58ff5b0880593a280cf5511734b17711b32ee1f58b47d726e8600848358160", size = 14227762, upload-time = "2026-04-13T11:29:55.564Z" },
+ { url = "https://files.pythonhosted.org/packages/28/dc/ad45ac3c0b6c4687dc649e8f6cf01af1c8b0443932a39b2abb4ebcb3babd/duckdb-1.5.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef461bca07313412dc09961c4a4757a851f56b95ac01c58fac6007632b7b94f2", size = 19315668, upload-time = "2026-04-13T11:29:58.427Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/b1/1464f468d2e5813f5808de95df9d3113a645a5bfa2ffcaecbc542ddae272/duckdb-1.5.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be37680ddb380015cb37318e378c53511c45c4f0d8fac5599d22b7d092b9217a", size = 21434056, upload-time = "2026-04-13T11:30:01.238Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/32/6673607e024722473fa7aafdd29c0e3dd231dd528f6cd8b5797fbeeb229d/duckdb-1.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:0b291786014df1133f8f18b9df4d004484613146e858d71a21791e0fcca16cf4", size = 13633667, upload-time = "2026-04-13T11:30:04.05Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/e3/9d34173ec068631faea3ea6e73050700729363e7e33306a9a3218e5cdc61/duckdb-1.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:c9f3e0b71b8a50fccfb42794899285d9d318ce2503782b9dd54868e5ecd0ad31", size = 14402513, upload-time = "2026-04-13T11:30:06.609Z" },
]
[[package]]
@@ -524,50 +540,50 @@ wheels = [
[[package]]
name = "greenlet"
-version = "3.3.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" },
- { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" },
- { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" },
- { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" },
- { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" },
- { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" },
- { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" },
- { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" },
- { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" },
- { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" },
- { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" },
- { url = "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", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" },
- { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" },
- { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" },
- { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" },
- { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" },
- { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" },
- { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" },
- { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" },
- { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" },
- { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" },
- { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" },
- { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" },
- { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" },
- { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" },
- { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" },
- { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" },
- { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" },
- { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" },
- { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" },
- { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" },
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/86/94/a5935717b307d7c71fe877b52b884c6af707d2d2090db118a03fbd799369/greenlet-3.4.0.tar.gz", hash = "sha256:f50a96b64dafd6169e595a5c56c9146ef80333e67d4476a65a9c55f400fc22ff", size = 195913, upload-time = "2026-04-08T17:08:00.863Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/65/8b/3669ad3b3f247a791b2b4aceb3aa5a31f5f6817bf547e4e1ff712338145a/greenlet-3.4.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1a54a921561dd9518d31d2d3db4d7f80e589083063ab4d3e2e950756ef809e1a", size = 286902, upload-time = "2026-04-08T15:52:12.138Z" },
+ { url = "https://files.pythonhosted.org/packages/38/3e/3c0e19b82900873e2d8469b590a6c4b3dfd2b316d0591f1c26b38a4879a5/greenlet-3.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16dec271460a9a2b154e3b1c2fa1050ce6280878430320e85e08c166772e3f97", size = 606099, upload-time = "2026-04-08T16:24:38.408Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/33/99fef65e7754fc76a4ed14794074c38c9ed3394a5bd129d7f61b705f3168/greenlet-3.4.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90036ce224ed6fe75508c1907a77e4540176dcf0744473627785dd519c6f9996", size = 618837, upload-time = "2026-04-08T16:30:58.298Z" },
+ { url = "https://files.pythonhosted.org/packages/36/f7/229f3aed6948faa20e0616a0b8568da22e365ede6a54d7d369058b128afd/greenlet-3.4.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c4f6b453006efb8310affb2d132832e9bbb4fc01ce6df6b70d810d38f1f6dc", size = 615062, upload-time = "2026-04-08T15:56:33.766Z" },
+ { url = "https://files.pythonhosted.org/packages/08/97/d988180011aa40135c46cd0d0cf01dd97f7162bae14139b4a3ef54889ba5/greenlet-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b2d9a138ffa0e306d0e2b72976d2fb10b97e690d40ab36a472acaab0838e2de", size = 1573511, upload-time = "2026-04-08T16:26:20.058Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/0f/a5a26fe152fb3d12e6a474181f6e9848283504d0afd095f353d85726374b/greenlet-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8424683caf46eb0eb6f626cb95e008e8cc30d0cb675bdfa48200925c79b38a08", size = 1640396, upload-time = "2026-04-08T15:57:30.88Z" },
+ { url = "https://files.pythonhosted.org/packages/42/cf/bb2c32d9a100e36ee9f6e38fad6b1e082b8184010cb06259b49e1266ca01/greenlet-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0a53fb071531d003b075c444014ff8f8b1a9898d36bb88abd9ac7b3524648a2", size = 238892, upload-time = "2026-04-08T17:03:10.094Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/47/6c41314bac56e71436ce551c7fbe3cc830ed857e6aa9708dbb9c65142eb6/greenlet-3.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:f38b81880ba28f232f1f675893a39cf7b6db25b31cc0a09bb50787ecf957e85e", size = 235599, upload-time = "2026-04-08T15:52:54.3Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/75/7e9cd1126a1e1f0cd67b0eda02e5221b28488d352684704a78ed505bd719/greenlet-3.4.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:43748988b097f9c6f09364f260741aa73c80747f63389824435c7a50bfdfd5c1", size = 285856, upload-time = "2026-04-08T15:52:45.82Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/c4/3e2df392e5cb199527c4d9dbcaa75c14edcc394b45040f0189f649631e3c/greenlet-3.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5566e4e2cd7a880e8c27618e3eab20f3494452d12fd5129edef7b2f7aa9a36d1", size = 610208, upload-time = "2026-04-08T16:24:39.674Z" },
+ { url = "https://files.pythonhosted.org/packages/da/af/750cdfda1d1bd30a6c28080245be8d0346e669a98fdbae7f4102aa95fff3/greenlet-3.4.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1054c5a3c78e2ab599d452f23f7adafef55062a783a8e241d24f3b633ba6ff82", size = 621269, upload-time = "2026-04-08T16:30:59.767Z" },
+ { url = "https://files.pythonhosted.org/packages/54/78/0cbc693622cd54ebe25207efbb3a0eb07c2639cb8594f6e3aaaa0bb077a8/greenlet-3.4.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f82cb6cddc27dd81c96b1506f4aa7def15070c3b2a67d4e46fd19016aacce6cf", size = 617549, upload-time = "2026-04-08T15:56:34.893Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/c0/8966767de01343c1ff47e8b855dc78e7d1a8ed2b7b9c83576a57e289f81d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:227a46251ecba4ff46ae742bc5ce95c91d5aceb4b02f885487aff269c127a729", size = 1575310, upload-time = "2026-04-08T16:26:21.671Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/38/bcdc71ba05e9a5fda87f63ffc2abcd1f15693b659346df994a48c968003d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5b99e87be7eba788dd5b75ba1cde5639edffdec5f91fe0d734a249535ec3408c", size = 1640435, upload-time = "2026-04-08T15:57:32.572Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/c2/19b664b7173b9e4ef5f77e8cef9f14c20ec7fce7920dc1ccd7afd955d093/greenlet-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:849f8bc17acd6295fcb5de8e46d55cc0e52381c56eaf50a2afd258e97bc65940", size = 238760, upload-time = "2026-04-08T17:04:03.878Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/96/795619651d39c7fbd809a522f881aa6f0ead504cc8201c3a5b789dfaef99/greenlet-3.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9390ad88b652b1903814eaabd629ca184db15e0eeb6fe8a390bbf8b9106ae15a", size = 235498, upload-time = "2026-04-08T17:05:00.584Z" },
+ { url = "https://files.pythonhosted.org/packages/78/02/bde66806e8f169cf90b14d02c500c44cdbe02c8e224c9c67bafd1b8cadd1/greenlet-3.4.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:10a07aca6babdd18c16a3f4f8880acfffc2b88dfe431ad6aa5f5740759d7d75e", size = 286291, upload-time = "2026-04-08T17:09:34.307Z" },
+ { url = "https://files.pythonhosted.org/packages/05/1f/39da1c336a87d47c58352fb8a78541ce63d63ae57c5b9dae1fe02801bbc2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:076e21040b3a917d3ce4ad68fb5c3c6b32f1405616c4a57aa83120979649bd3d", size = 656749, upload-time = "2026-04-08T16:24:41.721Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/6c/90ee29a4ee27af7aa2e2ec408799eeb69ee3fcc5abcecac6ddd07a5cd0f2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e82689eea4a237e530bb5cb41b180ef81fa2160e1f89422a67be7d90da67f615", size = 669084, upload-time = "2026-04-08T16:31:01.372Z" },
+ { url = "https://files.pythonhosted.org/packages/07/49/d4cad6e5381a50947bb973d2f6cf6592621451b09368b8c20d9b8af49c5b/greenlet-3.4.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df3b0b2289ec686d3c821a5fee44259c05cfe824dd5e6e12c8e5f5df23085cf", size = 665621, upload-time = "2026-04-08T15:56:35.995Z" },
+ { url = "https://files.pythonhosted.org/packages/37/31/d1edd54f424761b5d47718822f506b435b6aab2f3f93b465441143ea5119/greenlet-3.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8bff29d586ea415688f4cec96a591fcc3bf762d046a796cdadc1fdb6e7f2d5bf", size = 1622259, upload-time = "2026-04-08T16:26:23.201Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/c6/6d3f9cdcb21c4e12a79cb332579f1c6aa1af78eb68059c5a957c7812d95e/greenlet-3.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a569c2fb840c53c13a2b8967c63621fafbd1a0e015b9c82f408c33d626a2fda", size = 1686916, upload-time = "2026-04-08T15:57:34.282Z" },
+ { url = "https://files.pythonhosted.org/packages/63/45/c1ca4a1ad975de4727e52d3ffe641ae23e1d7a8ffaa8ff7a0477e1827b92/greenlet-3.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:207ba5b97ea8b0b60eb43ffcacf26969dd83726095161d676aac03ff913ee50d", size = 239821, upload-time = "2026-04-08T17:03:48.423Z" },
+ { url = "https://files.pythonhosted.org/packages/71/c4/6f621023364d7e85a4769c014c8982f98053246d142420e0328980933ceb/greenlet-3.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:f8296d4e2b92af34ebde81085a01690f26a51eb9ac09a0fcadb331eb36dbc802", size = 236932, upload-time = "2026-04-08T17:04:33.551Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/8f/18d72b629783f5e8d045a76f5325c1e938e659a9e4da79c7dcd10169a48d/greenlet-3.4.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d70012e51df2dbbccfaf63a40aaf9b40c8bed37c3e3a38751c926301ce538ece", size = 294681, upload-time = "2026-04-08T15:52:35.778Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/ad/5fa86ec46769c4153820d58a04062285b3b9e10ba3d461ee257b68dcbf53/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a58bec0751f43068cd40cff31bb3ca02ad6000b3a51ca81367af4eb5abc480c8", size = 658899, upload-time = "2026-04-08T16:24:43.32Z" },
+ { url = "https://files.pythonhosted.org/packages/43/f0/4e8174ca0e87ae748c409f055a1ba161038c43cc0a5a6f1433a26ac2e5bf/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05fa0803561028f4b2e3b490ee41216a842eaee11aed004cc343a996d9523aa2", size = 665284, upload-time = "2026-04-08T16:31:02.833Z" },
+ { url = "https://files.pythonhosted.org/packages/19/da/991cf7cd33662e2df92a1274b7eb4d61769294d38a1bba8a45f31364845e/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e60d38719cb80b3ab5e85f9f1aed4960acfde09868af6762ccb27b260d68f4ed", size = 661861, upload-time = "2026-04-08T15:56:37.269Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c5/6c2c708e14db3d9caea4b459d8464f58c32047451142fe2cfd90e7458f41/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f50c804733b43eded05ae694691c9aa68bca7d0a867d67d4a3f514742a2d53f", size = 1622182, upload-time = "2026-04-08T16:26:24.777Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/4c/50c5fed19378e11a29fabab1f6be39ea95358f4a0a07e115a51ca93385d8/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2d4f0635dc4aa638cda4b2f5a07ae9a2cff9280327b581a3fcb6f317b4fbc38a", size = 1685050, upload-time = "2026-04-08T15:57:36.453Z" },
+ { url = "https://files.pythonhosted.org/packages/db/72/85ae954d734703ab48e622c59d4ce35d77ce840c265814af9c078cacc7aa/greenlet-3.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1a4a48f24681300c640f143ba7c404270e1ebbbcf34331d7104a4ff40f8ea705", size = 245554, upload-time = "2026-04-08T17:03:50.044Z" },
]
[[package]]
name = "identify"
-version = "2.6.17"
+version = "2.6.18"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/57/84/376a3b96e5a8d33a7aa2c5b3b31a4b3c364117184bf0b17418055f6ace66/identify-2.6.17.tar.gz", hash = "sha256:f816b0b596b204c9fdf076ded172322f2723cf958d02f9c3587504834c8ff04d", size = 99579, upload-time = "2026-03-01T20:04:12.702Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/40/66/71c1227dff78aaeb942fed29dd5651f2aec166cc7c9aeea3e8b26a539b7d/identify-2.6.17-py2.py3-none-any.whl", hash = "sha256:be5f8412d5ed4b20f2bd41a65f920990bdccaa6a4a18a08f1eefdcd0bdd885f0", size = 99382, upload-time = "2026-03-01T20:04:11.439Z" },
+ { url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" },
]
[[package]]
@@ -590,7 +606,7 @@ wheels = [
[[package]]
name = "ipython"
-version = "9.11.0"
+version = "9.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -604,9 +620,9 @@ dependencies = [
{ name = "stack-data" },
{ name = "traitlets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl", hash = "sha256:6922d5bcf944c6e525a76a0a304451b60a2b6f875e86656d8bc2dfda5d710e19", size = 624222, upload-time = "2026-03-05T08:57:28.94Z" },
+ { url = "https://files.pythonhosted.org/packages/59/22/906c8108974c673ebef6356c506cebb6870d48cedea3c41e949e2dd556bb/ipython-9.12.0-py3-none-any.whl", hash = "sha256:0f2701e8ee86e117e37f50563205d36feaa259d2e08d4a6bc6b6d74b18ce128d", size = 625661, upload-time = "2026-03-27T09:42:42.831Z" },
]
[[package]]
@@ -656,62 +672,62 @@ wheels = [
[[package]]
name = "librt"
-version = "0.8.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" },
- { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" },
- { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" },
- { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" },
- { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" },
- { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" },
- { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" },
- { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" },
- { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" },
- { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" },
- { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" },
- { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" },
- { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" },
- { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" },
- { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" },
- { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" },
- { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" },
- { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" },
- { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" },
- { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" },
- { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" },
- { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" },
- { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" },
- { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" },
- { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" },
- { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" },
- { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" },
- { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" },
- { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" },
- { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" },
- { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" },
- { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" },
- { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" },
- { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" },
- { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" },
- { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" },
- { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" },
- { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" },
- { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" },
- { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" },
- { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" },
- { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" },
- { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" },
- { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" },
- { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" },
- { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" },
- { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" },
- { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" },
- { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" },
- { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" },
- { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" },
- { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" },
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" },
+ { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" },
+ { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" },
+ { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" },
+ { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" },
+ { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" },
+ { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" },
+ { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" },
+ { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" },
+ { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" },
+ { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" },
+ { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" },
+ { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" },
+ { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" },
+ { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" },
]
[[package]]
@@ -888,7 +904,7 @@ wheels = [
[[package]]
name = "mypy"
-version = "1.19.1"
+version = "1.20.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
@@ -896,36 +912,46 @@ dependencies = [
{ name = "pathspec" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
- { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
- { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
- { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" },
- { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" },
- { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" },
- { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
- { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
- { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
- { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
- { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
- { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
- { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" },
- { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" },
- { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" },
- { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" },
- { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" },
- { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" },
- { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/0b/3d/5b373635b3146264eb7a68d09e5ca11c305bbb058dfffbb47c47daf4f632/mypy-1.20.1.tar.gz", hash = "sha256:6fc3f4ecd52de81648fed1945498bf42fa2993ddfad67c9056df36ae5757f804", size = 3815892, upload-time = "2026-04-13T02:46:51.474Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/1b/75a7c825a02781ca10bc2f2f12fba2af5202f6d6005aad8d2d1f264d8d78/mypy-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:36ee2b9c6599c230fea89bbd79f401f9f9f8e9fcf0c777827789b19b7da90f51", size = 14494077, upload-time = "2026-04-13T02:45:55.085Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/54/5e5a569ea5c2b4d48b729fb32aa936eeb4246e4fc3e6f5b3d36a2dfbefb9/mypy-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fba3fb0968a7b48806b0c90f38d39296f10766885a94c83bd21399de1e14eb28", size = 13319495, upload-time = "2026-04-13T02:45:29.674Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/a4/a1945b19f33e91721b59deee3abb484f2fa5922adc33bb166daf5325d76d/mypy-1.20.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef1415a637cd3627d6304dfbeddbadd21079dafc2a8a753c477ce4fc0c2af54f", size = 13696948, upload-time = "2026-04-13T02:46:15.006Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/c6/75e969781c2359b2f9c15b061f28ec6d67c8b61865ceda176e85c8e7f2de/mypy-1.20.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef3461b1ad5cd446e540016e90b5984657edda39f982f4cc45ca317b628f5a37", size = 14706744, upload-time = "2026-04-13T02:46:00.482Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/6e/b221b1de981fc4262fe3e0bf9ec272d292dfe42394a689c2d49765c144c4/mypy-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:542dd63c9e1339b6092eb25bd515f3a32a1453aee8c9521d2ddb17dacd840237", size = 14949035, upload-time = "2026-04-13T02:45:06.021Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/4b/298ba2de0aafc0da3ff2288da06884aae7ba6489bc247c933f87847c41b3/mypy-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d55c7cd8ca22e31f93af2a01160a9e95465b5878de23dba7e48116052f20a8d", size = 10883216, upload-time = "2026-04-13T02:45:47.232Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/f9/5e25b8f0b8cb92f080bfed9c21d3279b2a0b6a601cdca369a039ba84789d/mypy-1.20.1-cp312-cp312-win_arm64.whl", hash = "sha256:f5b84a79070586e0d353ee07b719d9d0a4aa7c8ee90c0ea97747e98cbe193019", size = 9814299, upload-time = "2026-04-13T02:45:21.934Z" },
+ { url = "https://files.pythonhosted.org/packages/21/e8/ef0991aa24c8f225df10b034f3c2681213cb54cf247623c6dec9a5744e70/mypy-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f3886c03e40afefd327bd70b3f634b39ea82e87f314edaa4d0cce4b927ddcc1", size = 14500739, upload-time = "2026-04-13T02:46:05.442Z" },
+ { url = "https://files.pythonhosted.org/packages/23/73/416ebec3047636ed89fa871dc8c54bf05e9e20aa9499da59790d7adb312d/mypy-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e860eb3904f9764e83bafd70c8250bdffdc7dde6b82f486e8156348bf7ceb184", size = 13314735, upload-time = "2026-04-13T02:46:47.154Z" },
+ { url = "https://files.pythonhosted.org/packages/10/1e/1505022d9c9ac2e014a384eb17638fb37bf8e9d0a833ea60605b66f8f7ba/mypy-1.20.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4b5aac6e785719da51a84f5d09e9e843d473170a9045b1ea7ea1af86225df4b", size = 13704356, upload-time = "2026-04-13T02:45:19.773Z" },
+ { url = "https://files.pythonhosted.org/packages/98/91/275b01f5eba5c467a3318ec214dd865abb66e9c811231c8587287b92876a/mypy-1.20.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f37b6cd0fe2ad3a20f05ace48ca3523fc52ff86940e34937b439613b6854472e", size = 14696420, upload-time = "2026-04-13T02:45:24.205Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/57/b3779e134e1b7250d05f874252780d0a88c068bc054bcff99ca20a3a2986/mypy-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4bbb0f6b54ce7cc350ef4a770650d15fa70edd99ad5267e227133eda9c94218", size = 14936093, upload-time = "2026-04-13T02:45:32.087Z" },
+ { url = "https://files.pythonhosted.org/packages/be/33/81b64991b0f3f278c3b55c335888794af190b2d59031a5ad1401bcb69f1e/mypy-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:c3dc20f8ec76eecd77148cdd2f1542ed496e51e185713bf488a414f862deb8f2", size = 10889659, upload-time = "2026-04-13T02:46:02.926Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/fd/7adcb8053572edf5ef8f3db59599dfeeee3be9cc4c8c97e2d28f66f42ac5/mypy-1.20.1-cp313-cp313-win_arm64.whl", hash = "sha256:a9d62bbac5d6d46718e2b0330b25e6264463ed832722b8f7d4440ff1be3ca895", size = 9815515, upload-time = "2026-04-13T02:46:32.103Z" },
+ { url = "https://files.pythonhosted.org/packages/40/cd/db831e84c81d57d4886d99feee14e372f64bbec6a9cb1a88a19e243f2ef5/mypy-1.20.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:12927b9c0ed794daedcf1dab055b6c613d9d5659ac511e8d936d96f19c087d12", size = 14483064, upload-time = "2026-04-13T02:45:26.901Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/82/74e62e7097fa67da328ac8ece8de09133448c04d20ddeaeba251a3000f01/mypy-1.20.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:752507dd481e958b2c08fc966d3806c962af5a9433b5bf8f3bdd7175c20e34fe", size = 13335694, upload-time = "2026-04-13T02:46:12.514Z" },
+ { url = "https://files.pythonhosted.org/packages/74/c4/97e9a0abe4f3cdbbf4d079cb87a03b786efeccf5bf2b89fe4f96939ab2e6/mypy-1.20.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c614655b5a065e56274c6cbbe405f7cf7e96c0654db7ba39bc680238837f7b08", size = 13726365, upload-time = "2026-04-13T02:45:17.422Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/aa/a19d884a8d28fcd3c065776323029f204dbc774e70ec9c85eba228b680de/mypy-1.20.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c3f6221a76f34d5100c6d35b3ef6b947054123c3f8d6938a4ba00b1308aa572", size = 14693472, upload-time = "2026-04-13T02:46:41.253Z" },
+ { url = "https://files.pythonhosted.org/packages/84/44/cc9324bd21cf786592b44bf3b5d224b3923c1230ec9898d508d00241d465/mypy-1.20.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4bdfc06303ac06500af71ea0cdbe995c502b3c9ba32f3f8313523c137a25d1b6", size = 14919266, upload-time = "2026-04-13T02:46:28.37Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/dc/779abb25a8c63e8f44bf5a336217fa92790fa17e0c40e0c725d10cb01bbd/mypy-1.20.1-cp314-cp314-win_amd64.whl", hash = "sha256:0131edd7eba289973d1ba1003d1a37c426b85cdef76650cd02da6420898a5eb3", size = 11049713, upload-time = "2026-04-13T02:45:57.673Z" },
+ { url = "https://files.pythonhosted.org/packages/28/08/4172be2ad7de9119b5a92ca36abbf641afdc5cb1ef4ae0c3a8182f29674f/mypy-1.20.1-cp314-cp314-win_arm64.whl", hash = "sha256:33f02904feb2c07e1fdf7909026206396c9deeb9e6f34d466b4cfedb0aadbbe4", size = 9999819, upload-time = "2026-04-13T02:46:35.039Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/af/af9e46b0c8eabbce9fc04a477564170f47a1c22b308822282a59b7ff315f/mypy-1.20.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:168472149dd8cc505c98cefd21ad77e4257ed6022cd5ed2fe2999bed56977a5a", size = 15547508, upload-time = "2026-04-13T02:46:25.588Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/cd/39c9e4ad6ba33e069e5837d772a9e6c304b4a5452a14a975d52b36444650/mypy-1.20.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eb674600309a8f22790cca883a97c90299f948183ebb210fbef6bcee07cb1986", size = 14399557, upload-time = "2026-04-13T02:46:10.021Z" },
+ { url = "https://files.pythonhosted.org/packages/83/c1/3fd71bdc118ffc502bf57559c909927bb7e011f327f7bb8e0488e98a5870/mypy-1.20.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef2b2e4cc464ba9795459f2586923abd58a0055487cbe558cb538ea6e6bc142a", size = 15045789, upload-time = "2026-04-13T02:45:10.81Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/73/6f07ff8b57a7d7b3e6e5bf34685d17632382395c8bb53364ec331661f83e/mypy-1.20.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee461d396dd46b3f0ed5a098dbc9b8860c81c46ad44fa071afcfbc149f167c9", size = 15850795, upload-time = "2026-04-13T02:45:03.349Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e2/f7dffec1c7767078f9e9adf0c786d1fe0ff30964a77eb213c09b8b58cb76/mypy-1.20.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e364926308b3e66f1361f81a566fc1b2f8cd47fc8525e8136d4058a65a4b4f02", size = 16088539, upload-time = "2026-04-13T02:46:17.841Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/76/e0dee71035316e75a69d73aec2f03c39c21c967b97e277fd0ef8fd6aec66/mypy-1.20.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a0c17fbd746d38c70cbc42647cfd884f845a9708a4b160a8b4f7e70d41f4d7fa", size = 12575567, upload-time = "2026-04-13T02:45:34.795Z" },
+ { url = "https://files.pythonhosted.org/packages/22/a8/7ed43c9d9c3d1468f86605e323a5d97e411a448790a00f07e779f3211a46/mypy-1.20.1-cp314-cp314t-win_arm64.whl", hash = "sha256:db2cb89654626a912efda69c0d5c1d22d948265e2069010d3dde3abf751c7d08", size = 10378823, upload-time = "2026-04-13T02:45:13.35Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/28/926bd972388e65a39ee98e188ccf67e81beb3aacfd5d6b310051772d974b/mypy-1.20.1-py3-none-any.whl", hash = "sha256:1aae28507f253fe82d883790d1c0a0d35798a810117c88184097fe8881052f06", size = 2636553, upload-time = "2026-04-13T02:46:30.45Z" },
]
[[package]]
name = "mypy-boto3-s3"
-version = "1.42.67"
+version = "1.42.85"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/90/b3/d2cdd49f272add9a178a1a238f6bdd80f0e6b503506b002e727ba699f23a/mypy_boto3_s3-1.42.67.tar.gz", hash = "sha256:3a3a918a9949f2d6f8071d490b8968ddce634aa19590697537e5189cbdca403e", size = 76415, upload-time = "2026-03-12T20:02:08.476Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/3e/5e/026461fef8e163ec261df1668ee88611124170bb4da3d1b144c970e7c9b4/mypy_boto3_s3-1.42.85.tar.gz", hash = "sha256:401e3a184ac0973bc08b556cc3b2655d8f2e56570b6ed87ce635210df4f666fb", size = 76543, upload-time = "2026-04-07T19:51:20.608Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b1/a5/7d4a7bb51c7bb9b188f306555bfcf5537c7f0524964350ff9d04d86e0786/mypy_boto3_s3-1.42.67-py3-none-any.whl", hash = "sha256:93208799734611da4caa5fa8f5ce677b62758ddcd34b737b9f7ae471d179b95e", size = 83570, upload-time = "2026-03-12T20:02:04.391Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/58/fb6373ca66898620ecb7b9ab92563f3f7277627994bc4ceba75c721de4a1/mypy_boto3_s3-1.42.85-py3-none-any.whl", hash = "sha256:b2cad995ea733b16ae3be5510fd6a0038aa44400c22d010d4def9286cf6eaf82", size = 83751, upload-time = "2026-04-07T19:51:17.727Z" },
]
[[package]]
@@ -948,63 +974,63 @@ wheels = [
[[package]]
name = "numpy"
-version = "2.4.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" },
- { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" },
- { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" },
- { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" },
- { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" },
- { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" },
- { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" },
- { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" },
- { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" },
- { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" },
- { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" },
- { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" },
- { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" },
- { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" },
- { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" },
- { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" },
- { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" },
- { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" },
- { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" },
- { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" },
- { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" },
- { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" },
- { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" },
- { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" },
- { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" },
- { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" },
- { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" },
- { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" },
- { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" },
- { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" },
- { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" },
- { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" },
- { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" },
- { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" },
- { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" },
- { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" },
- { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" },
- { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" },
- { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" },
- { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" },
- { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" },
- { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" },
- { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" },
- { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" },
- { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" },
- { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" },
- { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" },
- { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" },
- { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" },
- { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" },
- { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" },
- { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" },
- { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" },
+version = "2.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" },
+ { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" },
+ { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" },
+ { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" },
+ { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" },
+ { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" },
+ { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" },
+ { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" },
+ { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" },
+ { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" },
+ { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" },
+ { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" },
+ { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" },
+ { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" },
+ { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" },
+ { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" },
+ { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" },
+ { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" },
+ { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" },
+ { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" },
]
[[package]]
@@ -1171,11 +1197,11 @@ wheels = [
[[package]]
name = "platformdirs"
-version = "4.9.4"
+version = "4.9.6"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
+ { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
]
[[package]]
@@ -1311,11 +1337,11 @@ wheels = [
[[package]]
name = "pygments"
-version = "2.19.2"
+version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
@@ -1329,7 +1355,7 @@ wheels = [
[[package]]
name = "pytest"
-version = "9.0.2"
+version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -1338,9 +1364,9 @@ dependencies = [
{ name = "pluggy" },
{ name = "pygments" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
@@ -1369,15 +1395,15 @@ wheels = [
[[package]]
name = "python-discovery"
-version = "1.1.3"
+version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "platformdirs" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d7/7e/9f3b0dd3a074a6c3e1e79f35e465b1f2ee4b262d619de00cfce523cc9b24/python_discovery-1.1.3.tar.gz", hash = "sha256:7acca36e818cd88e9b2ba03e045ad7e93e1713e29c6bbfba5d90202310b7baa5", size = 56945, upload-time = "2026-03-10T15:08:15.038Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e7/80/73211fc5bfbfc562369b4aa61dc1e4bf07dc7b34df7b317e4539316b809c/python_discovery-1.1.3-py3-none-any.whl", hash = "sha256:90e795f0121bc84572e737c9aa9966311b9fde44ffb88a5953b3ec9b31c6945e", size = 31485, upload-time = "2026-03-10T15:08:13.06Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" },
]
[[package]]
@@ -1437,7 +1463,7 @@ wheels = [
[[package]]
name = "requests"
-version = "2.32.5"
+version = "2.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -1445,9 +1471,9 @@ dependencies = [
{ name = "idna" },
{ name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
]
[[package]]
@@ -1466,40 +1492,40 @@ wheels = [
[[package]]
name = "rich"
-version = "14.3.3"
+version = "15.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
+ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[package]]
name = "ruff"
-version = "0.15.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" },
- { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" },
- { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" },
- { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" },
- { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" },
- { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" },
- { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" },
- { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" },
- { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" },
- { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" },
- { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" },
- { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" },
- { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" },
- { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" },
- { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" },
- { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" },
- { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" },
+version = "0.15.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" },
+ { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" },
+ { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" },
+ { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" },
+ { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" },
+ { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" },
+ { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" },
+ { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" },
]
[[package]]
@@ -1586,7 +1612,7 @@ wheels = [
[[package]]
name = "timdex-dataset-api"
-version = "4.1.0"
+version = "5.0.0"
source = { editable = "." }
dependencies = [
{ name = "attrs" },
@@ -1643,47 +1669,47 @@ dev = [
[[package]]
name = "tomli"
-version = "2.4.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
- { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
- { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
- { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
- { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
- { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
- { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
- { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
- { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
- { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
- { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
- { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
- { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
- { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
- { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
- { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
- { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
- { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
- { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
- { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
- { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
- { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
- { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
- { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
- { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
- { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
- { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
- { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
- { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
- { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
- { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
- { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
- { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
- { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
- { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
- { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
- { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
+ { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
+ { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
+ { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
+ { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
+ { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
+ { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
+ { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
+ { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
+ { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
+ { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
+ { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
+ { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
+ { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
+ { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
+ { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
+ { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
+ { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
+ { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
+ { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
+ { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
]
[[package]]
@@ -1748,11 +1774,11 @@ wheels = [
[[package]]
name = "tzdata"
-version = "2025.3"
+version = "2026.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" },
]
[[package]]
@@ -1766,7 +1792,7 @@ wheels = [
[[package]]
name = "virtualenv"
-version = "21.2.0"
+version = "21.2.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
@@ -1774,9 +1800,9 @@ dependencies = [
{ name = "platformdirs" },
{ name = "python-discovery" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/60/8c/bdd9f89f89e4a787ac61bb2da4d884bc45e0c287ec694dfa3170dddd5cfe/virtualenv-21.2.3.tar.gz", hash = "sha256:9bb6d1414ab55ca624371e30c7719c32f183ef44da544ef8aa44a456de7ac191", size = 5844776, upload-time = "2026-04-14T01:10:36.692Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" },
+ { url = "https://files.pythonhosted.org/packages/95/19/bc7c4e05f42532863cf2ae7e7e847beab25835934e0410160b47eeff1e35/virtualenv-21.2.3-py3-none-any.whl", hash = "sha256:486652347ea8526d91e9807c0274583cb7ba31dd4942ff10fb5621402f0fe0d8", size = 5828329, upload-time = "2026-04-14T01:10:34.809Z" },
]
[[package]]
@@ -1790,14 +1816,14 @@ wheels = [
[[package]]
name = "werkzeug"
-version = "3.1.6"
+version = "3.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" },
+ { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" },
]
[[package]]