diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 2a9fd7c..4210939 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -18,13 +18,12 @@ jobs: compatibility: name: ${{ matrix.os }} / Python ${{ matrix.python-version }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.python-version == '3.15' }} timeout-minutes: 45 strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.12", "3.13", "3.14", "3.15"] + python-version: ["3.12", "3.13", "3.14"] env: UV_PYTHON: ${{ matrix.python-version }} UV_NO_MANAGED_PYTHON: "1" @@ -39,7 +38,6 @@ jobs: uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} - allow-prereleases: true - name: Install uv uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 @@ -47,14 +45,9 @@ jobs: version: ${{ env.UV_VERSION }} enable-cache: true - - name: Install the complete stable environment - if: matrix.python-version != '3.15' + - name: Install the complete supported environment run: uv sync --locked --all-extras --dev - - name: Install the core prerelease environment - if: matrix.python-version == '3.15' - run: uv sync --locked --no-default-groups --group prerelease - - name: Verify import run: uv run --no-sync python -c "import ml4t.data as package; print(package.__version__)" diff --git a/README.md b/README.md index 7ec8aeb..dc035b7 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,10 @@ The goal is to support an ongoing research workflow rather than one-off download ## Installation ML4T Data supports stable CPython 3.12 through 3.14 on Linux, macOS, and Windows. -Python 3.15 prereleases are tested as informational compatibility checks but are not supported -until the required dependencies publish compatible distributions. The Databento SDK currently -has no CPython 3.15 distribution, so the 3.15 prerelease lane tests the core package without the -Databento extra. +Python 3.15 is not supported until the core dependency stack passes the complete compatibility +suite on all three operating systems. Releases 0.1.0 and 0.1.1 predate this upper bound, so an +unpinned installation on Python 3.15 may select one of those older releases. Use Python 3.12 +through 3.14 instead. ```bash pip install ml4t-data diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index c4e8bd6..be60283 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -67,8 +67,9 @@ print(f"Fetched {len(df)} rows") ## Requirements - Stable CPython 3.12 through 3.14 on Linux, macOS, or Windows -- Python 3.15 prereleases are tested as informational checks but are not supported -- The Databento SDK does not yet publish a Python 3.15 distribution, so that optional extra is - excluded from prerelease testing until an upstream build is available +- Python 3.15 is not supported until the core dependency stack passes the complete compatibility + suite on Linux, macOS, and Windows +- Releases 0.1.0 and 0.1.1 predate the Python 3.15 upper bound, so an unpinned installation on + Python 3.15 may select one of those older releases - Polars (automatically installed) - Provider-specific SDKs (optional) diff --git a/pyproject.toml b/pyproject.toml index a9d7624..6ca50a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Information Analysis", "Typing :: Typed", ] -requires-python = ">=3.12" +requires-python = ">=3.12,<3.15" # Core dependencies dependencies = [ @@ -88,7 +88,6 @@ dependencies = [ "python-dotenv>=1.0.0", "pydantic-settings>=2.0.0", "pydantic>=2.12,<3", - "pydantic>=2.14.0b1; python_version >= '3.15'", # Utilities "structlog>=23.0.0", "platformdirs>=4.0.0", @@ -173,13 +172,6 @@ test = [ "ty", "xlsxwriter>=3.1.0", ] -prerelease = [ - { include-group = "test" }, - "yfinance>=0.2.0", - "oandapyV20>=0.7.0", - "requests>=2.32.0", - "cot-reports>=0.1.0", -] dev = [ { include-group = "test" }, "ruff>=0.8.0", @@ -191,6 +183,9 @@ dev = [ "oandapyV20>=0.7.0", ] +[tool.uv] +prerelease = "disallow" + [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] diff --git a/src/ml4t/data/managers/storage_manager.py b/src/ml4t/data/managers/storage_manager.py index 6825517..7eb9925 100644 --- a/src/ml4t/data/managers/storage_manager.py +++ b/src/ml4t/data/managers/storage_manager.py @@ -15,9 +15,11 @@ import polars as pl import structlog +from pydantic import ValidationError from tenacity import RetryError from ml4t.data.core.schemas import align_frames_for_concat, timestamp_bounds +from ml4t.data.storage.backend import normalize_storage_metadata from ml4t.data.utils.conversion import pandas_to_polars if TYPE_CHECKING: @@ -493,6 +495,20 @@ def update( logger.info("Found existing data", rows=len(existing_df)) + existing_metadata = ( + normalize_storage_metadata(self.storage.get_metadata(key), key) or {} + ) + existing_provider = existing_metadata.get("provider") + if ( + not provider + and existing_provider is not None + and not isinstance(existing_provider, str) + ): + raise ValueError( + f"Stored metadata provider for '{key}' must be a string, " + f"got {type(existing_provider).__name__}" + ) + # Get the date range from existing data _, last_timestamp = timestamp_bounds(existing_df) if last_timestamp.tzinfo is None: @@ -588,29 +604,78 @@ def update( from ml4t.data.core.models import DataObject, Metadata min_ts, max_ts = timestamp_bounds(merged_df) - - updated_metadata = Metadata( - provider=provider or "auto", - symbol=symbol, - asset_class=asset_class, - bar_type="time", - bar_params={"frequency": frequency}, - data_range={ - "start": str(min_ts), - "end": str(max_ts), - }, - attributes={ - "last_update": datetime.now().isoformat(), + existing_attributes = existing_metadata.get("attributes") + updated_attributes = ( + existing_attributes.copy() if isinstance(existing_attributes, dict) else {} + ) + updated_at = datetime.now(UTC) + updated_attributes.update( + { + # BulkManager compares this legacy field with naive local cutoffs. + "last_update": updated_at.astimezone().replace(tzinfo=None).isoformat(), "update_type": "incremental", "gaps_filled": fill_gaps and len(gaps) > 0, "provider_history_limited": provider_history_limited, - }, + } + ) + + existing_model_values = { + name: value + for name in Metadata.model_fields + if (value := existing_metadata.get(name)) is not None + } + resolved_provider = ( + provider + or (existing_provider if isinstance(existing_provider, str) else None) + or "auto" ) + existing_bar_type = existing_metadata.get("bar_type") + existing_bar_params = existing_metadata.get("bar_params") + metadata_values = { + **existing_model_values, + "provider": resolved_provider, + "symbol": symbol, + "asset_class": asset_class, + "bar_type": existing_bar_type if isinstance(existing_bar_type, str) else "time", + "bar_params": ( + existing_bar_params + if isinstance(existing_bar_params, dict) + else {"frequency": frequency} + ), + "start_date": min_ts, + "end_date": max_ts, + "last_updated": updated_at, + "data_range": { + "start": str(min_ts), + "end": str(max_ts), + }, + "attributes": updated_attributes, + } + + try: + updated_metadata = Metadata.model_validate(metadata_values) + except ValidationError as error: + invalid_fields = { + location[0] + for detail in error.errors() + if (location := detail.get("loc")) and isinstance(location[0], str) + } + removable_fields = invalid_fields - {"provider", "symbol", "asset_class"} + if not removable_fields: + raise + logger.warning( + "Ignoring invalid optional fields in stored metadata", + key=key, + fields=sorted(removable_fields), + ) + for field in removable_fields: + metadata_values.pop(field, None) + updated_metadata = Metadata.model_validate(metadata_values) updated_obj = DataObject(data=merged_df, metadata=updated_metadata) metadata_dict = updated_obj.metadata.model_dump() if updated_obj.metadata else None - self.storage.write(updated_obj.data, key, metadata_dict) + self.storage.write(updated_obj.data, key, metadata_dict, preserve_metadata=True) logger.info( "Incremental update completed", diff --git a/src/ml4t/data/storage/backend.py b/src/ml4t/data/storage/backend.py index 1d3df72..b2ea43d 100644 --- a/src/ml4t/data/storage/backend.py +++ b/src/ml4t/data/storage/backend.py @@ -28,12 +28,20 @@ def normalize_storage_metadata(metadata: Any, key: str | None = None) -> dict[str, Any] | None: - """Return domain metadata from a canonical or legacy storage record.""" + """Return flattened domain metadata from a canonical or legacy storage record. + + Non-null values in ``custom`` override committed fields. A null custom value does not clear a + non-null committed value, but custom-only keys remain present even when their value is null. + """ if not isinstance(metadata, dict) or not metadata: return None custom = metadata.get("custom") - normalized = {**metadata, **custom} if isinstance(custom, dict) else metadata.copy() + normalized = metadata.copy() + if isinstance(custom, dict): + for name, value in custom.items(): + if value is not None or normalized.get(name) is None: + normalized[name] = value if key is not None: parts = key.split("/", 2) @@ -93,12 +101,50 @@ def _recover_key_staging(key_path: Path) -> None: elif staging_path.is_dir(): shutil.rmtree(staging_path) + def _effective_metadata( + self, + key: str, + metadata: dict[str, Any] | None, + preserve_metadata: bool, + ) -> dict[str, Any]: + """Resolve metadata for a write while holding its key lock.""" + effective_metadata = metadata.copy() if metadata else {} + if not preserve_metadata: + return effective_metadata + try: + current_record = self._current_commit(key).metadata + except KeyError: + return effective_metadata + except RuntimeError as error: + logger.warning( + "Ignoring unreadable metadata while replacing stored data", + key=key, + error=str(error), + ) + return effective_metadata + + current_custom = current_record.get("custom") + if not isinstance(current_custom, dict): + return effective_metadata + + existing_attributes = current_custom.get("attributes") + updated_attributes = effective_metadata.get("attributes") + effective_metadata = {**current_custom, **effective_metadata} + if isinstance(existing_attributes, dict) and isinstance(updated_attributes, dict): + effective_metadata["attributes"] = { + **existing_attributes, + **updated_attributes, + } + return effective_metadata + @abstractmethod def write( self, data: pl.LazyFrame | pl.DataFrame, key: str, metadata: dict[str, Any] | None = None, + *, + preserve_metadata: bool = False, ) -> Path: """Write data to storage. @@ -106,6 +152,7 @@ def write( data: Polars LazyFrame to write key: Storage key (e.g., "BTC-USD", "SPY") metadata: Optional metadata to store alongside data + preserve_metadata: Merge metadata into the current custom block under the write lock Returns: Path to written file diff --git a/src/ml4t/data/storage/flat.py b/src/ml4t/data/storage/flat.py index 50bdf1a..c0029e6 100644 --- a/src/ml4t/data/storage/flat.py +++ b/src/ml4t/data/storage/flat.py @@ -39,7 +39,12 @@ def __init__(self, config: StorageConfig): super().__init__(config) def write( - self, data: pl.LazyFrame | pl.DataFrame, key: str, metadata: dict[str, Any] | None = None + self, + data: pl.LazyFrame | pl.DataFrame, + key: str, + metadata: dict[str, Any] | None = None, + *, + preserve_metadata: bool = False, ) -> Path: """Write data as a single file. @@ -47,6 +52,7 @@ def write( data: Data to write key: Storage key (e.g., "BTC-USD") metadata: Optional metadata + preserve_metadata: Merge metadata into the current custom block under the write lock Returns: Path to written file @@ -56,18 +62,19 @@ def write( df = lazy_data.collect() with self._key_lock(key): + effective_metadata = self._effective_metadata(key, metadata, preserve_metadata) staging_path, generation_id = self._prepare_generation(key) try: staged_file = staging_path / "data.parquet" self._atomic_write(df, staged_file) commit_metadata = ( { - "last_updated": datetime.now().isoformat(), + "last_updated": datetime.now(UTC).isoformat(), "file_path": "data.parquet", "row_count": len(df), "schema": list(df.columns), "file_size_mb": staged_file.stat().st_size / (1024 * 1024), - "custom": metadata or {}, + "custom": effective_metadata, } if self.config.metadata_tracking else {} diff --git a/src/ml4t/data/storage/hive.py b/src/ml4t/data/storage/hive.py index 68309a6..167adaf 100644 --- a/src/ml4t/data/storage/hive.py +++ b/src/ml4t/data/storage/hive.py @@ -277,6 +277,8 @@ def write( data: pl.LazyFrame | pl.DataFrame, key: str, metadata: dict[str, Any] | None = None, + *, + preserve_metadata: bool = False, ) -> Path: """Write data using Hive partitioning. @@ -284,6 +286,7 @@ def write( data: DataFrame or LazyFrame to write key: Storage key (e.g., "BTC-USD" or "equities/daily/AAPL") metadata: Optional metadata dict + preserve_metadata: Merge metadata into the current custom block under the write lock Returns: Path to the committed data generation @@ -305,6 +308,8 @@ def write( df = self._add_partition_columns(df, partition_cols) with self._key_lock(key): + effective_metadata = self._effective_metadata(key, metadata, preserve_metadata) + staging_path, generation_id = self._prepare_generation(key) try: partitions_written = [] @@ -321,11 +326,11 @@ def write( commit_metadata = ( { - "last_updated": datetime.now().isoformat(), + "last_updated": datetime.now(UTC).isoformat(), "partitions": partitions_written, "row_count": len(df), "schema": list(df.columns), - "custom": metadata or {}, + "custom": effective_metadata, } if self.config.metadata_tracking else {} diff --git a/src/ml4t/data/update_manager.py b/src/ml4t/data/update_manager.py index 90dc7bb..ce2d1fe 100644 --- a/src/ml4t/data/update_manager.py +++ b/src/ml4t/data/update_manager.py @@ -13,7 +13,7 @@ from ml4t.data.core.schemas import align_frames_for_concat from ml4t.data.providers.base import BaseProvider -from ml4t.data.storage.hive import HiveStorage +from ml4t.data.storage.backend import StorageBackend from ml4t.data.storage.metadata_tracker import MetadataTracker, UpdateRecord logger = structlog.get_logger() @@ -28,6 +28,35 @@ def _ensure_datetime(value: Any) -> datetime: raise TypeError(f"Expected a date or datetime, got {type(value).__name__}") +def _rewrite_preserving_metadata( + storage: StorageBackend, + key: str, + data: pl.DataFrame, +) -> None: + """Publish replacement data with retained identity and a recomputed date range.""" + if data.is_empty(): + raise ValueError("Data must not be empty") + if "timestamp" not in data.columns: + raise ValueError("Data must have a 'timestamp' column") + min_ts = _ensure_datetime(data["timestamp"].min()) + max_ts = _ensure_datetime(data["timestamp"].max()) + updated_at = datetime.now(UTC) + storage.write( + data, + key, + { + "start_date": min_ts, + "end_date": max_ts, + # Clear the inherited value so normalization uses the commit's fresh UTC stamp. + "last_updated": None, + "data_range": {"start": str(min_ts), "end": str(max_ts)}, + # BulkManager compares this legacy field with naive local cutoffs. + "attributes": {"last_update": updated_at.astimezone().replace(tzinfo=None).isoformat()}, + }, + preserve_metadata=True, + ) + + class UpdateStrategy(Enum): """Update strategy types.""" @@ -144,17 +173,17 @@ def detect_gaps( def detect_gaps_in_storage( self, - storage: HiveStorage, + storage: StorageBackend, key: str, start_date: datetime, end_date: datetime, frequency: str = "daily", ) -> list[dict[str, Any]]: """ - Detect gaps directly in Hive storage structure. + Detect gaps through a storage backend. Args: - storage: HiveStorage instance + storage: Storage backend key: Storage key for the dataset start_date: Start of range to check end_date: End of range to check @@ -247,7 +276,7 @@ def __init__( def determine_update_range( self, - storage: HiveStorage, + storage: StorageBackend, key: str, requested_start: datetime, requested_end: datetime, @@ -305,7 +334,7 @@ def determine_update_range( def update_incremental( self, - storage: HiveStorage, + storage: StorageBackend, tracker: MetadataTracker, key: str, new_data: pl.DataFrame, @@ -421,7 +450,7 @@ def update_incremental( def apply_strategy( self, - storage: HiveStorage, + storage: StorageBackend, key: str, new_data: pl.DataFrame, strategy: UpdateStrategy, @@ -440,8 +469,7 @@ def apply_strategy( """ if strategy == UpdateStrategy.FULL_REFRESH: # Replace all data - storage.delete(key) - storage.write(new_data, key) + _rewrite_preserving_metadata(storage, key, new_data) return UpdateResult( success=True, @@ -465,8 +493,7 @@ def apply_strategy( if not new_rows.is_empty(): # Append new rows combined = pl.concat([existing_df, new_rows]) - storage.delete(key) - storage.write(combined, key) + _rewrite_preserving_metadata(storage, key, combined) return UpdateResult( success=True, @@ -519,8 +546,7 @@ def apply_strategy( if not gap_data.is_empty(): # Merge with existing data combined = pl.concat([existing_df, gap_data]).sort("timestamp") - storage.delete(key) - storage.write(combined, key) + _rewrite_preserving_metadata(storage, key, combined) return UpdateResult( success=True, @@ -574,8 +600,7 @@ def apply_strategy( # Combine all data combined = pl.concat([existing_filtered, new_data]).sort("timestamp") - storage.delete(key) - storage.write(combined, key) + _rewrite_preserving_metadata(storage, key, combined) return UpdateResult( success=True, @@ -587,8 +612,7 @@ def apply_strategy( ) # No overlap, just append combined = pl.concat([existing_df, new_data]).sort("timestamp") - storage.delete(key) - storage.write(combined, key) + _rewrite_preserving_metadata(storage, key, combined) return UpdateResult( success=True, @@ -614,7 +638,7 @@ class BackfillManager: def __init__( self, - storage: HiveStorage | None, + storage: StorageBackend | None, tracker: MetadataTracker | None, ) -> None: """ diff --git a/tests/test_data_manager_storage.py b/tests/test_data_manager_storage.py index 1f18c8e..d929963 100644 --- a/tests/test_data_manager_storage.py +++ b/tests/test_data_manager_storage.py @@ -374,6 +374,121 @@ def test_update_incremental(self, manager, storage): # Should have at least the initial data (update merges data) assert len(stored_df) >= initial_rows + def test_update_preserves_complete_metadata( + self, + manager, + initial_data, + new_data, + ): + """Incremental updates retain identity fields and report the merged date range.""" + manager.import_data( + initial_data, + symbol="AAPL", + provider="yahoo", + exchange="XNYS", + calendar="NYSE", + ) + + before = datetime.now(UTC) + with ( + patch.object(manager._fetch_manager, "fetch_raw", return_value=new_data), + patch.object(manager._fetch_manager, "get_max_history_days", return_value=None), + ): + manager.update("AAPL", fill_gaps=False) + + metadata = manager.get_metadata("AAPL") + assert metadata is not None + assert metadata["provider"] == "yahoo" + assert metadata["exchange"] == "XNYS" + assert metadata["calendar"] == "NYSE" + assert datetime.fromisoformat(metadata["start_date"]) == initial_data["timestamp"].min() + assert datetime.fromisoformat(metadata["end_date"]) == new_data["timestamp"].max() + assert datetime.fromisoformat(metadata["last_updated"]) >= before + + def test_update_accepts_partial_legacy_metadata( + self, + manager, + storage, + initial_data, + new_data, + ): + """A malformed optional legacy field cannot prevent an otherwise valid update.""" + key = "equities/daily/AAPL" + storage.write( + initial_data, + key, + { + "provider": "legacy", + "exchange": None, + "schema_version": "legacy", + "provider_params": "invalid", + "download_utc_timestamp": None, + "attributes": {"source": "legacy-sidecar"}, + }, + ) + + with ( + patch.object(manager._fetch_manager, "fetch_raw", return_value=new_data), + patch.object(manager._fetch_manager, "get_max_history_days", return_value=None), + ): + manager.update("AAPL", fill_gaps=False) + + metadata = manager.get_metadata("AAPL") + assert metadata is not None + assert metadata["provider"] == "legacy" + assert metadata["exchange"] == "UNKNOWN" + assert metadata["schema_version"] == "1.0" + assert metadata["provider_params"] == {} + assert metadata["attributes"]["source"] == "legacy-sidecar" + + def test_update_rejects_invalid_required_legacy_metadata( + self, + manager, + storage, + initial_data, + new_data, + ): + """A corrupt stored provider fails explicitly and leaves the generation untouched.""" + key = "equities/daily/AAPL" + storage.write(initial_data, key, {"provider": {"invalid": True}}) + record_before = storage.get_metadata(key) + + with ( + patch.object(manager._fetch_manager, "fetch_raw", return_value=new_data), + patch.object(manager._fetch_manager, "get_max_history_days", return_value=None), + pytest.raises(ValueError, match="Stored metadata provider.*must be a string"), + ): + manager.update("AAPL", fill_gaps=False) + + assert storage.get_metadata(key) == record_before + + def test_second_update_retains_attributes_and_earliest_timestamp( + self, + manager, + initial_data, + new_data, + ): + """Repeated updates retain user attributes and the complete date range.""" + key = manager.import_data(initial_data, symbol="AAPL", provider="yahoo") + record = manager._storage_manager.storage.get_metadata(key) + assert record is not None + record["custom"]["attributes"]["source"] = "research" + manager._storage_manager.storage.write(initial_data, key, record["custom"]) + + final_data = new_data.with_columns(pl.col("timestamp") + timedelta(days=8)) + with ( + patch.object(manager._fetch_manager, "get_max_history_days", return_value=None), + patch.object(manager._fetch_manager, "fetch_raw", side_effect=[new_data, final_data]), + ): + manager.update("AAPL", fill_gaps=False) + manager.update("AAPL", fill_gaps=False) + + metadata = manager.get_metadata("AAPL") + assert metadata is not None + assert metadata["attributes"]["source"] == "research" + assert datetime.fromisoformat(metadata["start_date"]) == initial_data["timestamp"].min() + assert datetime.fromisoformat(metadata["end_date"]) == final_data["timestamp"].max() + def test_update_handles_duplicates(self, manager, storage): """Test that update handles duplicate timestamps correctly.""" key = manager.load("AAPL", "2024-01-01", "2024-01-05", provider="mock") diff --git a/tests/test_hive_storage.py b/tests/test_hive_storage.py index c34302f..1d4dfb5 100644 --- a/tests/test_hive_storage.py +++ b/tests/test_hive_storage.py @@ -6,7 +6,7 @@ import polars as pl import pytest -from ml4t.data.storage.backend import StorageConfig +from ml4t.data.storage.backend import StorageConfig, normalize_storage_metadata from ml4t.data.storage.hive import HiveStorage from ml4t.data.storage.keys import decode_storage_key, encode_storage_key, storage_key_path @@ -749,3 +749,48 @@ def test_reports_the_current_generation_after_a_rewrite(self, stored): assert [part.label for part in parts] == ["2023-01", "2023-02", "2023-03"] assert len({part.path.parent.parent for part in parts}) == 1 + + +class TestNormalizeStorageMetadata: + """`custom` overrides the record it sits in, but a null in it is not an override.""" + + def test_custom_value_overrides_the_record(self): + normalized = normalize_storage_metadata( + {"provider": "record", "row_count": 10, "custom": {"provider": "yahoo"}} + ) + + assert normalized["provider"] == "yahoo" + assert normalized["row_count"] == 10 + + def test_null_in_custom_does_not_erase_the_record(self): + """An incremental update writes custom.last_updated=None over a correct record value.""" + normalized = normalize_storage_metadata( + { + "last_updated": "2026-08-10T09:57:41.487386", + "row_count": 1316, + "custom": {"last_updated": None, "end_date": None, "provider": "yahoo"}, + } + ) + + assert normalized["last_updated"] == "2026-08-10T09:57:41.487386" + assert normalized["provider"] == "yahoo" + + def test_key_absent_from_the_record_keeps_its_null(self): + """Dropping the key instead would turn a reported null into a KeyError for callers.""" + normalized = normalize_storage_metadata({"row_count": 1, "custom": {"calendar": None}}) + + assert "calendar" in normalized + assert normalized["calendar"] is None + + def test_normalization_does_not_mutate_the_record(self): + metadata = { + "last_updated": "2026-08-10T09:57:41.487386", + "custom": {"last_updated": None, "provider": "yahoo"}, + } + + normalize_storage_metadata(metadata) + + assert metadata == { + "last_updated": "2026-08-10T09:57:41.487386", + "custom": {"last_updated": None, "provider": "yahoo"}, + } diff --git a/tests/test_package_metadata.py b/tests/test_package_metadata.py index 4569ede..4ddd479 100644 --- a/tests/test_package_metadata.py +++ b/tests/test_package_metadata.py @@ -25,7 +25,7 @@ def test_supported_python_and_platform_metadata() -> None: project = load_project() classifiers = set(project["classifiers"]) - assert project["requires-python"] == ">=3.12" + assert project["requires-python"] == ">=3.12,<3.15" assert "Development Status :: 5 - Production/Stable" in classifiers assert "Operating System :: OS Independent" not in classifiers assert { @@ -40,6 +40,16 @@ def test_supported_python_and_platform_metadata() -> None: } <= classifiers assert "Programming Language :: Python :: 3.11" not in classifiers assert "Programming Language :: Python :: 3.15" not in classifiers + pydantic_requirement = next( + (item for item in project["dependencies"] if item.startswith("pydantic>=")), + None, + ) + assert pydantic_requirement == "pydantic>=2.12,<3" + + +def test_resolver_uses_stable_dependencies() -> None: + """The default lock cannot select prerelease-only compatibility packages.""" + assert load_config()["tool"]["uv"]["prerelease"] == "disallow" def test_license_uses_spdx_metadata() -> None: diff --git a/tests/test_release_workflows.py b/tests/test_release_workflows.py index c702fe7..e98cc61 100644 --- a/tests/test_release_workflows.py +++ b/tests/test_release_workflows.py @@ -39,8 +39,8 @@ def test_compatibility_matrix_covers_release_policy() -> None: matrix = compatibility["strategy"]["matrix"] assert matrix["os"] == ["ubuntu-latest", "macos-latest", "windows-latest"] - assert matrix["python-version"] == ["3.12", "3.13", "3.14", "3.15"] - assert compatibility["continue-on-error"] == "${{ matrix.python-version == '3.15' }}" + assert matrix["python-version"] == ["3.12", "3.13", "3.14"] + assert "continue-on-error" not in compatibility def test_publish_uses_only_the_validated_package_directory() -> None: diff --git a/tests/test_storage_backends.py b/tests/test_storage_backends.py index f56bd03..4e68944 100644 --- a/tests/test_storage_backends.py +++ b/tests/test_storage_backends.py @@ -5,6 +5,7 @@ from concurrent.futures import ThreadPoolExecutor from datetime import UTC, datetime, timedelta from pathlib import Path +from types import SimpleNamespace import polars as pl import pytest @@ -150,6 +151,74 @@ def test_metadata_tracking(self, temp_dir, sample_data, strategy): assert "custom" in metadata assert metadata["custom"]["source"] == "test" + @pytest.mark.parametrize("strategy", ["hive", "flat"]) + def test_preserve_metadata_merges_under_the_write_lock(self, temp_dir, sample_data, strategy): + """Both backends retain custom fields and merge attribute updates.""" + storage = create_storage(temp_dir, strategy=strategy) + storage.write( + sample_data, + "test_key", + {"provider": "yahoo", "attributes": {"source": "research"}}, + preserve_metadata=True, + ) + storage.write( + sample_data, + "test_key", + {"calendar": "NYSE", "attributes": {"updated": True}}, + preserve_metadata=True, + ) + storage.write(sample_data, "test_key", preserve_metadata=True) + + metadata = storage.get_metadata("test_key") + assert metadata is not None + assert metadata["custom"] == { + "provider": "yahoo", + "calendar": "NYSE", + "attributes": {"source": "research", "updated": True}, + } + + @pytest.mark.parametrize("strategy", ["hive", "flat"]) + def test_preserve_metadata_continues_when_current_record_read_fails( + self, tmp_path, sample_data, strategy, monkeypatch + ): + """A damaged previous manifest cannot prevent a replacement write.""" + storage = create_storage(tmp_path, strategy=strategy) + original_current_commit = storage._current_commit + attempts = 0 + + def damaged_once(key): + nonlocal attempts + attempts += 1 + if attempts == 1: + raise RuntimeError("damaged manifest") + return original_current_commit(key) + + monkeypatch.setattr(storage, "_current_commit", damaged_once) + storage.write( + sample_data, + "test_key", + {"provider": "yahoo"}, + preserve_metadata=True, + ) + + assert storage.read("test_key").collect().height == sample_data.height + metadata = storage.get_metadata("test_key") + assert metadata is not None + assert metadata["custom"] == {"provider": "yahoo"} + + def test_preserve_metadata_ignores_non_mapping_custom_block(self, tmp_path, monkeypatch): + """Legacy non-mapping custom metadata is replaced by the supplied mapping.""" + storage = create_storage(tmp_path, strategy="hive") + monkeypatch.setattr( + storage, + "_current_commit", + lambda _key: SimpleNamespace(metadata={"custom": "legacy"}), + ) + + assert storage._effective_metadata("test_key", {"provider": "yahoo"}, True) == { + "provider": "yahoo" + } + @pytest.mark.parametrize("strategy", ["hive", "flat"]) def test_list_keys(self, temp_dir, sample_data, strategy): """Test listing stored keys.""" diff --git a/tests/test_update_manager.py b/tests/test_update_manager.py index a195c83..2f930e7 100644 --- a/tests/test_update_manager.py +++ b/tests/test_update_manager.py @@ -17,6 +17,9 @@ import polars as pl import pytest +from ml4t.data.core.models import Metadata +from ml4t.data.storage.backend import normalize_storage_metadata +from ml4t.data.storage.flat import FlatStorage from ml4t.data.storage.hive import HiveStorage, StorageConfig from ml4t.data.storage.metadata_tracker import MetadataTracker from ml4t.data.update_manager import ( @@ -548,6 +551,101 @@ def test_incremental_strategy( assert result.success is True # Should have both added and updated rows + def test_incremental_strategy_supports_flat_storage( + self, tmp_path: Path, existing_data: pl.DataFrame, new_data: pl.DataFrame + ): + """Metadata-preserving rewrites work through the shared storage interface.""" + storage = FlatStorage(StorageConfig(base_path=tmp_path)) + storage.write(existing_data, "test", {"provider": "yahoo"}) + + result = IncrementalUpdater().apply_strategy( + storage, + "test", + new_data, + UpdateStrategy.INCREMENTAL, + ) + + assert result.success is True + record = storage.get_metadata("test") + assert record is not None + assert record["custom"]["provider"] == "yahoo" + + @pytest.mark.parametrize( + ("invalid_data", "message"), + [ + (pl.DataFrame(), "Data must not be empty"), + (pl.DataFrame({"close": [1.0]}), "Data must have a 'timestamp' column"), + ], + ) + def test_full_refresh_rejects_invalid_replacement_frames( + self, + storage: HiveStorage, + invalid_data: pl.DataFrame, + message: str, + ): + """Direct strategy callers receive a clear error before a replacement write.""" + with pytest.raises(ValueError, match=message): + IncrementalUpdater().apply_strategy( + storage, + "test", + invalid_data, + UpdateStrategy.FULL_REFRESH, + ) + + @pytest.mark.parametrize("strategy", list(UpdateStrategy)) + def test_rewrite_strategies_preserve_identity_and_refresh_range( + self, + storage: HiveStorage, + existing_data: pl.DataFrame, + new_data: pl.DataFrame, + strategy: UpdateStrategy, + ): + """Every rewrite retains identity while replacing stale range and recency fields.""" + old_update = datetime(2020, 1, 1, tzinfo=UTC) + metadata = Metadata( + provider="yahoo", + symbol="AAPL", + asset_class="equities", + exchange="XNYS", + start_date=existing_data["timestamp"].min(), + end_date=existing_data["timestamp"].max(), + last_updated=old_update, + attributes={"last_update": old_update.isoformat(), "source": "research"}, + ).model_dump() + + stored_data = existing_data + replacement = new_data + if strategy == UpdateStrategy.BACKFILL: + missing_timestamp = existing_data["timestamp"][7] + stored_data = existing_data.filter(pl.col("timestamp") != missing_timestamp) + replacement = existing_data.filter(pl.col("timestamp") == missing_timestamp) + storage.write(stored_data, "test", metadata) + + IncrementalUpdater().apply_strategy( + storage, + "test", + replacement, + strategy, + ) + + record = storage.get_metadata("test") + normalized = normalize_storage_metadata(record) + assert normalized is not None + assert normalized["provider"] == "yahoo" + assert normalized["exchange"] == "XNYS" + assert normalized["calendar"] is None + assert normalized["bar_params"] == {} + assert normalized["attributes"]["source"] == "research" + rewritten = storage.read("test").collect() + assert _ensure_datetime( + datetime.fromisoformat(normalized["start_date"]) + ) == _ensure_datetime(rewritten["timestamp"].min()) + assert _ensure_datetime(datetime.fromisoformat(normalized["end_date"])) == _ensure_datetime( + rewritten["timestamp"].max() + ) + assert _ensure_datetime(datetime.fromisoformat(normalized["last_updated"])) > old_update + assert normalized["attributes"]["last_update"] != old_update.isoformat() + def test_update_incremental_missing_timestamp( self, storage: HiveStorage, tracker: MetadataTracker ): diff --git a/uv.lock b/uv.lock index cc7efbe..7c4e323 100644 --- a/uv.lock +++ b/uv.lock @@ -1,18 +1,18 @@ version = 1 revision = 3 -requires-python = ">=3.12" +requires-python = ">=3.12, <3.15" resolution-markers = [ - "python_full_version >= '3.15' and sys_platform == 'win32'", - "python_full_version == '3.14.*' and sys_platform == 'win32'", - "python_full_version >= '3.15' and sys_platform == 'emscripten'", - "python_full_version == '3.14.*' and sys_platform == 'emscripten'", - "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] +[options] +prerelease-mode = "disallow" + [[package]] name = "aiofiles" version = "25.1.0" @@ -314,30 +314,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, - { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, - { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, - { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, - { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, - { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, - { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, - { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, - { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, - { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, - { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, - { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, - { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, - { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, - { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, - { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, - { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, - { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, - { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, - { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, - { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, - { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, ] [[package]] @@ -508,36 +484,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85", size = 225079, upload-time = "2026-08-06T13:49:15.265Z" }, { url = "https://files.pythonhosted.org/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e", size = 225911, upload-time = "2026-08-06T13:49:17.279Z" }, { url = "https://files.pythonhosted.org/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e", size = 225219, upload-time = "2026-08-06T13:49:19.293Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c4/dc5d2ac8f9142e7ec7de66e7bf0591db29d78955a040bd915870d9c0e657/coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9", size = 222604, upload-time = "2026-08-06T13:49:21.279Z" }, - { url = "https://files.pythonhosted.org/packages/70/39/33e63df81fe2ee100897451841c821467635923e58e37c6bd4b46dd8106c/coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a", size = 222944, upload-time = "2026-08-06T13:49:23.187Z" }, - { url = "https://files.pythonhosted.org/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54", size = 254050, upload-time = "2026-08-06T13:49:25.008Z" }, - { url = "https://files.pythonhosted.org/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb", size = 256967, upload-time = "2026-08-06T13:49:26.896Z" }, - { url = "https://files.pythonhosted.org/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed", size = 258587, upload-time = "2026-08-06T13:49:28.692Z" }, - { url = "https://files.pythonhosted.org/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce", size = 260785, upload-time = "2026-08-06T13:49:30.472Z" }, - { url = "https://files.pythonhosted.org/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c", size = 254545, upload-time = "2026-08-06T13:49:32.271Z" }, - { url = "https://files.pythonhosted.org/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced", size = 256682, upload-time = "2026-08-06T13:49:34.089Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800", size = 254560, upload-time = "2026-08-06T13:49:36.052Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3", size = 258792, upload-time = "2026-08-06T13:49:38.01Z" }, - { url = "https://files.pythonhosted.org/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768", size = 253968, upload-time = "2026-08-06T13:49:39.934Z" }, - { url = "https://files.pythonhosted.org/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753", size = 255893, upload-time = "2026-08-06T13:49:41.825Z" }, - { url = "https://files.pythonhosted.org/packages/5c/62/13be29b3ddab35f14c87967a4820a05106d2a3eccb4fa4ff550bf30b75e0/coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2", size = 224768, upload-time = "2026-08-06T13:49:44.08Z" }, - { url = "https://files.pythonhosted.org/packages/a1/70/af0c6be0f964af6954f6b74bc109b0dbca02824696d2520fb17fe1ab06e3/coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809", size = 225242, upload-time = "2026-08-06T13:49:45.899Z" }, - { url = "https://files.pythonhosted.org/packages/4f/2d/f3bd3aab899fc9efc18b53133ee68f5f98574ef480649b23e12962226387/coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d", size = 224674, upload-time = "2026-08-06T13:49:48.322Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ca/f69251cd63eabc6438321aea22148754cce758a26bde07dd490e3fe7cfc5/coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d", size = 223333, upload-time = "2026-08-06T13:49:50.293Z" }, - { url = "https://files.pythonhosted.org/packages/a7/a7/037b53b2885b0d8447064432491a4d5a1014cd9f97a594d53acd0c04541a/coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26", size = 223630, upload-time = "2026-08-06T13:49:52.637Z" }, - { url = "https://files.pythonhosted.org/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645", size = 264489, upload-time = "2026-08-06T13:49:54.52Z" }, - { url = "https://files.pythonhosted.org/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a", size = 267567, upload-time = "2026-08-06T13:49:56.721Z" }, - { url = "https://files.pythonhosted.org/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624", size = 270123, upload-time = "2026-08-06T13:49:58.736Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa", size = 271107, upload-time = "2026-08-06T13:50:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f", size = 264955, upload-time = "2026-08-06T13:50:03.023Z" }, - { url = "https://files.pythonhosted.org/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f", size = 267949, upload-time = "2026-08-06T13:50:05.557Z" }, - { url = "https://files.pythonhosted.org/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67", size = 264421, upload-time = "2026-08-06T13:50:07.612Z" }, - { url = "https://files.pythonhosted.org/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc", size = 269121, upload-time = "2026-08-06T13:50:09.58Z" }, - { url = "https://files.pythonhosted.org/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88", size = 264565, upload-time = "2026-08-06T13:50:11.722Z" }, - { url = "https://files.pythonhosted.org/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc", size = 266522, upload-time = "2026-08-06T13:50:13.786Z" }, - { url = "https://files.pythonhosted.org/packages/49/dd/e0e40f3560d878d888c580698ff5ad1179f5e1c3ac949684ef66b41a3817/coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a", size = 225068, upload-time = "2026-08-06T13:50:15.825Z" }, - { url = "https://files.pythonhosted.org/packages/c6/7e/37732ea80eebc30e976e4cdab15c190bc42d96959a42e38ddf6f8c60468f/coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b", size = 225895, upload-time = "2026-08-06T13:50:17.928Z" }, - { url = "https://files.pythonhosted.org/packages/c6/08/1e00f7923eaaba45fb3d51dd794125fc766304b1df264f3a9c6557bfb30e/coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278", size = 225213, upload-time = "2026-08-06T13:50:19.981Z" }, { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, ] @@ -1439,8 +1385,7 @@ dependencies = [ { name = "platformdirs" }, { name = "polars" }, { name = "pybreaker" }, - { name = "pydantic", version = "2.13.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.15'" }, - { name = "pydantic", version = "2.14.0b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.15'" }, + { name = "pydantic" }, { name = "pydantic-settings" }, { name = "python-dotenv" }, { name = "pyyaml" }, @@ -1536,20 +1481,6 @@ dev = [ { name = "xlsxwriter" }, { name = "yfinance" }, ] -prerelease = [ - { name = "cot-reports" }, - { name = "hypothesis" }, - { name = "oandapyv20" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "pytest-timeout" }, - { name = "pytest-xdist" }, - { name = "requests" }, - { name = "ty" }, - { name = "xlsxwriter" }, - { name = "yfinance" }, -] test = [ { name = "hypothesis" }, { name = "pytest" }, @@ -1604,7 +1535,6 @@ requires-dist = [ { name = "pyarrow", marker = "extra == 'databento'", specifier = ">=14.0.0" }, { name = "pybreaker", specifier = ">=1.0.0" }, { name = "pydantic", specifier = ">=2.12,<3" }, - { name = "pydantic", marker = "python_full_version >= '3.15'", specifier = ">=2.14.0b1" }, { name = "pydantic-settings", specifier = ">=2.0.0" }, { name = "pytest", marker = "extra == 'all'", specifier = ">=7.4.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" }, @@ -1654,20 +1584,6 @@ dev = [ { name = "xlsxwriter", specifier = ">=3.1.0" }, { name = "yfinance", specifier = ">=0.2.0" }, ] -prerelease = [ - { name = "cot-reports", specifier = ">=0.1.0" }, - { name = "hypothesis", specifier = ">=6.80.0" }, - { name = "oandapyv20", specifier = ">=0.7.0" }, - { name = "pytest", specifier = ">=7.4.0" }, - { name = "pytest-asyncio", specifier = ">=0.21.0" }, - { name = "pytest-cov", specifier = ">=4.1.0" }, - { name = "pytest-timeout", specifier = ">=2.1.0" }, - { name = "pytest-xdist", specifier = ">=3.3.0" }, - { name = "requests", specifier = ">=2.32.0" }, - { name = "ty" }, - { name = "xlsxwriter", specifier = ">=3.1.0" }, - { name = "yfinance", specifier = ">=0.2.0" }, -] test = [ { name = "hypothesis", specifier = ">=6.80.0" }, { name = "pytest", specifier = ">=7.4.0" }, @@ -2451,59 +2367,23 @@ wheels = [ name = "pydantic" version = "2.13.4" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.14.*' and sys_platform == 'win32'", - "python_full_version == '3.14.*' and sys_platform == 'emscripten'", - "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] dependencies = [ - { name = "annotated-types", marker = "python_full_version < '3.15'" }, - { name = "pydantic-core", version = "2.46.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.15'" }, - { name = "typing-extensions", marker = "python_full_version < '3.15'" }, - { name = "typing-inspection", marker = "python_full_version < '3.15'" }, + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] -[[package]] -name = "pydantic" -version = "2.14.0b1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15' and sys_platform == 'win32'", - "python_full_version >= '3.15' and sys_platform == 'emscripten'", - "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "annotated-types", marker = "python_full_version >= '3.15'" }, - { name = "pydantic-core", version = "2.48.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.15'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.15'" }, - { name = "typing-inspection", marker = "python_full_version >= '3.15'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/08/d6380e1986e14e0d51f8ee498bd97c437992d723d1ea23ad3be92e8bd341/pydantic-2.14.0b1.tar.gz", hash = "sha256:d974b3fe7e6ad2a3a5718842d4fc3f5f78142d431a8f73d2d08bc498acf39c8a", size = 865425, upload-time = "2026-08-06T14:29:30.306Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/77/0deeee9facbe51c1c2dd430ebe785255112d8262663b46a7090ad691a5ba/pydantic-2.14.0b1-py3-none-any.whl", hash = "sha256:c7803eac0d891142724e06f9a53264feb1b4e3dfdf89890cbe7eb819795f6b07", size = 477719, upload-time = "2026-08-06T14:29:28.507Z" }, -] - [[package]] name = "pydantic-core" version = "2.46.4" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.14.*' and sys_platform == 'win32'", - "python_full_version == '3.14.*' and sys_platform == 'emscripten'", - "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.15'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ @@ -2573,124 +2453,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, ] -[[package]] -name = "pydantic-core" -version = "2.48.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15' and sys_platform == 'win32'", - "python_full_version >= '3.15' and sys_platform == 'emscripten'", - "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "typing-extensions", marker = "python_full_version >= '3.15'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/26/1b/34440c0294cbc90e57873cbc28465a7e6d6216984cf3ce195cb8ca6791ee/pydantic_core-2.48.0.tar.gz", hash = "sha256:8714f70dafdffea0a5596cc88eddbdc71f5856563947970dcbd0f1ced61ed05f", size = 479692, upload-time = "2026-08-06T14:27:05.602Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/14/cb52ef72b8290fb9ac7fc8e5feeb20d276dc4f7f5b759af929f8ebc5ce93/pydantic_core-2.48.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:86ab2c8185deea63d9e82406bfd82a8255ae82eaf8cfe8d30caede4b9859be2c", size = 2123088, upload-time = "2026-08-06T14:22:39.953Z" }, - { url = "https://files.pythonhosted.org/packages/89/91/d5f0c8048e8b7b5fa0cd0ba9dc82b0ae9bc342a61ecb1eb0908a9fadb636/pydantic_core-2.48.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:998fe237ad91f8b6a216a344a70cc566557b3c905c4c12c319426425476ddc5a", size = 1935770, upload-time = "2026-08-06T14:22:41.704Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1e/6d3edb8eca798320eae8d6ddec34c95cf2a6f845a120d2dff984ad62a82b/pydantic_core-2.48.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0692d545312b1849111d7a801515dba648cc1d40f4d1a8c3c7355c92736b0af3", size = 1956857, upload-time = "2026-08-06T14:22:43.473Z" }, - { url = "https://files.pythonhosted.org/packages/5f/6e/35938d2e67dbd903c98252f69bab4d597160a472c90db5e10f96dacabe6f/pydantic_core-2.48.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f3f7ab3e97aa6d3d659043391b38c3cf41812c93b49692ed969d5beb66ceb29", size = 2077528, upload-time = "2026-08-06T14:22:45.916Z" }, - { url = "https://files.pythonhosted.org/packages/9f/18/3d05c3ef8f91d755bfa7d8403186b2aad1b50ab298d19e58bebaa2d8ddff/pydantic_core-2.48.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e918f1ef44f350b3c03570b1f9b0f2f97e7dfc843fd295951af3db6df6e639d", size = 2260659, upload-time = "2026-08-06T14:22:47.728Z" }, - { url = "https://files.pythonhosted.org/packages/86/17/557007c55d9991600c2ef08200f5f2635307c5445a94ad7bdb9b83c48f30/pydantic_core-2.48.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8198d74ccef5e243c3662bf7b9f847d4afec93bd08d5db8ef1a16dadaec71a5a", size = 2313283, upload-time = "2026-08-06T14:22:49.915Z" }, - { url = "https://files.pythonhosted.org/packages/cc/7e/d14ffe43dd645221285b8bb989af5920f01dc1ff91d0a696724c838efc56/pydantic_core-2.48.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3463fb9e857362b896f345f6c4349d20b817865a05eb570bf838d7c406536412", size = 2074576, upload-time = "2026-08-06T14:22:51.798Z" }, - { url = "https://files.pythonhosted.org/packages/12/ff/c2dc71559d85f8690ada56a5f72ed4db276f8a478dc7b79c6bf72ccb0a10/pydantic_core-2.48.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:7156d0bcc38c8084291b613696b3d23d24e4b8dc7a7a1070c5baa5907936e531", size = 2152359, upload-time = "2026-08-06T14:22:53.637Z" }, - { url = "https://files.pythonhosted.org/packages/d7/9a/ae461c64f7d4dfc6c84f837fccde9b7a469c6d9a309ff09c6e03b942a1ad/pydantic_core-2.48.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3af1e349ecf55a66c53be2fec30fc5a528664ad3d0434aaef5b7b8475fdbd9a", size = 2200931, upload-time = "2026-08-06T14:22:55.513Z" }, - { url = "https://files.pythonhosted.org/packages/8b/40/2c2339379151004f17d6d7ab7f0dbcc360586673f9a4414bcee58170b7b4/pydantic_core-2.48.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:794aab60bbb5a92bb856961b294e149140d81cdf364206c55832d32588137b3b", size = 2197027, upload-time = "2026-08-06T14:22:58.279Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/637fc0e1f1fc4d02b9034e58cc6ed2f937677b82fb292dae7b6f45759ba8/pydantic_core-2.48.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:a52f3eafe91530b9b4b4016a20aabfce4d456be837d2ea7186eaf23ee3320d72", size = 2353748, upload-time = "2026-08-06T14:23:00.148Z" }, - { url = "https://files.pythonhosted.org/packages/cb/5a/2144f79041c73cf5b5716434dfa2cf1a4395e68b3dbd9cc01324b4ccb4a4/pydantic_core-2.48.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2cff5642d8c477a79f7b7d05b77620d860cf80e4a0d1cdff72e08c598c12dd3a", size = 2385528, upload-time = "2026-08-06T14:23:02.329Z" }, - { url = "https://files.pythonhosted.org/packages/71/5d/b46f81fc81924c57aa31d11b6dc1ab4bbbfae17e931fdb472869c2d577ed/pydantic_core-2.48.0-cp312-cp312-win32.whl", hash = "sha256:c967fab606fc0cccea553c783a47136333bbc0bc27ed4c401460f2cdf4341147", size = 1972085, upload-time = "2026-08-06T14:23:04.07Z" }, - { url = "https://files.pythonhosted.org/packages/91/e0/de0a9390688d2f1b72d193607772693e25302d0d06525dc0e8ca34752bdc/pydantic_core-2.48.0-cp312-cp312-win_amd64.whl", hash = "sha256:403c28347ac5a0fb4b96b52dea1f5c23ce4515a67c9dc1232ffdc31b88988c3c", size = 2072135, upload-time = "2026-08-06T14:23:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a1/e4baad10e93749209c32adb04a4e3346d4fb0042229411af6ee72fb46b98/pydantic_core-2.48.0-cp312-cp312-win_arm64.whl", hash = "sha256:f9620adcc4f0aa76b3e2357a9649b45c17aa2834d960f502224c8477db8a3261", size = 2043455, upload-time = "2026-08-06T14:23:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/83/ba/da6251d26ab569e7c87a3acfa0528af3e0ba0bb49ddcd27183d6f1257d97/pydantic_core-2.48.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:742e93d9bcb63d9e5a0fb26e82f175ebf183bc861f8ee18b2b5d7ed09d831803", size = 2122895, upload-time = "2026-08-06T14:23:09.786Z" }, - { url = "https://files.pythonhosted.org/packages/a6/29/70787c54ff498d961d02432cdb343dfe5b59574b22fe5887240a7cdff095/pydantic_core-2.48.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:482c3707518a7163c8148686cd18d27c8f11f89cafa43a6834b42505d266b117", size = 1948678, upload-time = "2026-08-06T14:23:11.503Z" }, - { url = "https://files.pythonhosted.org/packages/46/4c/debee51c5ef9161093fb1334e4a70728ee244db32952f4cb1c57d24a6de8/pydantic_core-2.48.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73a5e0f879c9fee3b94473f92860119b6c87779214e8545a1334f346ce5adc2e", size = 1957645, upload-time = "2026-08-06T14:23:13.48Z" }, - { url = "https://files.pythonhosted.org/packages/66/e7/55e5f27b74401e070c9eb1e2a4356864207896aa997c25e0b895574de8d8/pydantic_core-2.48.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fde25b657fc1d960ab48f9282f6d01bcbe691a80f67aaa08055cf0842e35d5c", size = 2076704, upload-time = "2026-08-06T14:23:15.525Z" }, - { url = "https://files.pythonhosted.org/packages/19/af/3704e32aa527920e861d9f632dc080e2316a3cd9c57706e5e2c293d0fefc/pydantic_core-2.48.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b630303e2c555395d633c699c7b51b7a3428cee40382b1409698e0feec35f519", size = 2257147, upload-time = "2026-08-06T14:23:17.413Z" }, - { url = "https://files.pythonhosted.org/packages/b8/85/440205d22bbcdbd0b8dc8eeb16ecde5cddf6350db0efc0301d52d311a151/pydantic_core-2.48.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2378f8e2f3cf79c449dce50d203111673b2f229769845338f40ae7cfce4b5693", size = 2312925, upload-time = "2026-08-06T14:23:19.405Z" }, - { url = "https://files.pythonhosted.org/packages/4d/85/a5b36fe37e1cba2f0576d746b3b9c5385f6883d448a7cad548146432e135/pydantic_core-2.48.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a9b575ed1193508c0a2226946e5cb4e091528aa7756100355773e86f3c2d9a3", size = 2091934, upload-time = "2026-08-06T14:23:21.431Z" }, - { url = "https://files.pythonhosted.org/packages/79/0f/5a0acc962c16c30b257a27e84062a99aeac568c1558e32107753d39cdb0e/pydantic_core-2.48.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:1a9f44fac958777c1743c4a239d156c12b85e05c1cc4a040d9b227459bdb19e6", size = 2151214, upload-time = "2026-08-06T14:23:23.62Z" }, - { url = "https://files.pythonhosted.org/packages/e0/69/35c42e00c5b0d1e94ec4ab808381c51a35952d65ea936da68eff40e14a8f/pydantic_core-2.48.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fad9aabe217d8a62cd1c46afa6b48134da8e5f6eb148bb228fac5886b9a40407", size = 2198942, upload-time = "2026-08-06T14:23:26.088Z" }, - { url = "https://files.pythonhosted.org/packages/d6/3b/1cffc72b05551d9ac6acbaa361a42eaa5d740d2cbd75c881970de2055d2d/pydantic_core-2.48.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ddea29d75d8c65dc6c2b3e8b9a178eb27e0093ded129f5bdf0b7c6654dbf90e1", size = 2196306, upload-time = "2026-08-06T14:23:28.275Z" }, - { url = "https://files.pythonhosted.org/packages/23/0e/cd1d186737a45d05d169db907168ca55aa23ee3f2f84fe85355ce9f9c6a8/pydantic_core-2.48.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:85c1006a8ae5b5ddfd908cdc98b01db9c0eb3a012c116e9054f66d4f9a6dd210", size = 2352832, upload-time = "2026-08-06T14:23:30.568Z" }, - { url = "https://files.pythonhosted.org/packages/9c/9f/8c3fa0ceb5dd5336c193414494009f141a78a0fe47145ceadfa909e28c5a/pydantic_core-2.48.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:94ed5b7ac4522a462aa44a3e830abb2ea18c1c6fc633d9fef222f63ae12d610a", size = 2385571, upload-time = "2026-08-06T14:23:32.758Z" }, - { url = "https://files.pythonhosted.org/packages/99/2f/51fad23abd315ab0c6d91da7f4fba8e74b217e194352df076115b7b69665/pydantic_core-2.48.0-cp313-cp313-win32.whl", hash = "sha256:52a860e1f01bced6685d016881338fc74d5beb491aed35a08f64d929d7c894a0", size = 1971015, upload-time = "2026-08-06T14:23:34.864Z" }, - { url = "https://files.pythonhosted.org/packages/c8/85/98ece6458b7fc7cf22dd546d41f0341bff4a049076d809187a0717be811f/pydantic_core-2.48.0-cp313-cp313-win_amd64.whl", hash = "sha256:29f6cf2dfff9206a949eeba1de29578e2199c078d3f2e25f47253e15da44fd4e", size = 2070829, upload-time = "2026-08-06T14:23:37.137Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ab/a72219854be7de4b1080f6946d9a11759ecedde770a28b94460f668f5c60/pydantic_core-2.48.0-cp313-cp313-win_arm64.whl", hash = "sha256:41eaed1185feacdd39cfa02fbb46ea3e15493cedd70a901c6a1aa437cf1afec4", size = 2043289, upload-time = "2026-08-06T14:23:39.24Z" }, - { url = "https://files.pythonhosted.org/packages/52/b7/e486a048aee5eabc7c8581bd795115386b20ab89c48e15a66f582ef2c393/pydantic_core-2.48.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:34fdd956d95ea79cbd58dd29afc7ebf658d32ec20b738a5a9110eac3dcc42a66", size = 2124317, upload-time = "2026-08-06T14:23:41.31Z" }, - { url = "https://files.pythonhosted.org/packages/6d/7b/282025d5305940401cbe18c5b04a27c1790be9d67713653c03ccaddda0d8/pydantic_core-2.48.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:aead7fc064c84c0a57f57ea373b971d33ae0df05b1866c669d7b71c0083b00f2", size = 1936187, upload-time = "2026-08-06T14:23:43.332Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9f/f2395f0addbc578b22ff8352ff888237f00606fa127ede6ecd1658cbcf9f/pydantic_core-2.48.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e94dc01965be40ccf1fa71833fd85d7df588a49b64a929b2d5178208a895ddb", size = 1959223, upload-time = "2026-08-06T14:23:45.376Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/350c3f81ad4c698fb800c1c110884c963a420c9c5ca7a8ff860e6d56d22d/pydantic_core-2.48.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9244be8b15f2edd25e8724b91f0ef406a73fef29a3ba9cdf3a19acc04c135613", size = 2076795, upload-time = "2026-08-06T14:23:47.792Z" }, - { url = "https://files.pythonhosted.org/packages/56/a6/7036bda94ed740ad44174dec7875b299c55c1b0842f079227fcb3880e11f/pydantic_core-2.48.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e615698372a8c93f91fae46ee23dc93c4cc52ba97662d2fa31829e63892d744", size = 2259477, upload-time = "2026-08-06T14:23:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/85/61/c13e8bb5b926488fe65a6f371fab85d69bdc76fb397ee81f3b7f5d7c14f4/pydantic_core-2.48.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f2dccc4c7b4c50854ffe2786c192a7a4521d851fd42dcb3ca697bcebe0dff6e", size = 2320484, upload-time = "2026-08-06T14:23:52.29Z" }, - { url = "https://files.pythonhosted.org/packages/fb/82/192c011f9b4f62df3f620af239289e35dee9205299ad03e3a351de4dc3cc/pydantic_core-2.48.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6643b3df515bb47c8e46c07f4530cdc7f52ffcc1845d437011dbb9198200148a", size = 2078103, upload-time = "2026-08-06T14:23:54.458Z" }, - { url = "https://files.pythonhosted.org/packages/8d/8c/a40773b3f9876f32d4a9e91686f31f83949694381f32baf58e4e6d9469d7/pydantic_core-2.48.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:612c4541a52fc9173df90d62cbd931a2c2f87b9f2eebe5e71b2f2d89410ee6b6", size = 2151310, upload-time = "2026-08-06T14:23:56.551Z" }, - { url = "https://files.pythonhosted.org/packages/1c/8f/b9cc3e09e8c80c6b4f86eee714d615344700fe8d8a8fe8a39fa013406d3e/pydantic_core-2.48.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6cbb4d9f81063600c13b86a45b9413a2c4148ddfe534108a57dad47c72377f27", size = 2201859, upload-time = "2026-08-06T14:23:59.042Z" }, - { url = "https://files.pythonhosted.org/packages/96/c2/7515c394fc5990e4469001ab545637c3861b959377cf4ea7caa2e577302d/pydantic_core-2.48.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:43bec89e5624b0a3b9f765cb7c2f62959547362c48d82471f4da34af00612ac7", size = 2196170, upload-time = "2026-08-06T14:24:01.261Z" }, - { url = "https://files.pythonhosted.org/packages/48/62/577b912824f3efbe20750e54e77ae6e0d6d964ab002a54af9b7aa12e3e3e/pydantic_core-2.48.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9906053654da2b5270ffe3699868b5bb3f39b4cb74b085cf6cbbde329094102c", size = 2353582, upload-time = "2026-08-06T14:24:03.695Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e4/8da652a5be351c66864700556a60f1e0092defb6c7bf8156c63e757db40f/pydantic_core-2.48.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:4dfc6b534178202a3e43e88d73fdf5c85c151df8e920c68ea9d4890fd212cada", size = 2387345, upload-time = "2026-08-06T14:24:06.876Z" }, - { url = "https://files.pythonhosted.org/packages/19/70/b7b9042d5e745d3893f8b5597a00727325d0f41dd5e5fd8875335d876770/pydantic_core-2.48.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:4fc45a49334c54541cbc97bf416d9300b4b1d3b2840dfb079b1123dc3f9ef5a6", size = 1403190, upload-time = "2026-08-06T14:24:09.012Z" }, - { url = "https://files.pythonhosted.org/packages/f7/2f/4bb02fddcaeb4255d5c7d165479ffb4657600f180aa836cbe9f7ef7a16cb/pydantic_core-2.48.0-cp314-cp314-win32.whl", hash = "sha256:def7b0c70a95a9348fcfbedf7dba9c97328b422c38ee894b8b0bb48663e408cd", size = 1972768, upload-time = "2026-08-06T14:24:11.404Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ea/24f620aad5d4e193a9e9644337983c0db086cd9d49c35fa9dec1b658031c/pydantic_core-2.48.0-cp314-cp314-win_amd64.whl", hash = "sha256:2eed50d5dcd57e319e88e117294815eae1c65d2eef316d7d87eb5398d649833d", size = 2072285, upload-time = "2026-08-06T14:24:13.846Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f9/a987c7ff8028af469e845113049d7e5bf39c98a88bb24dc2db0637c875e8/pydantic_core-2.48.0-cp314-cp314-win_arm64.whl", hash = "sha256:69ebfa782ca474ed305227a04d370c9d7f5a7f5039e7802505145d3e8ad8e309", size = 2039011, upload-time = "2026-08-06T14:24:16.183Z" }, - { url = "https://files.pythonhosted.org/packages/46/60/f02acd0a97866449877bb220b0df8241b667b385236379613bfcbdf96116/pydantic_core-2.48.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:1aa7dfad60b42bbaa6b7be7e28f386be9b742483bfeaf26d310b466092f271be", size = 2125136, upload-time = "2026-08-06T14:24:18.54Z" }, - { url = "https://files.pythonhosted.org/packages/a8/7e/a7e969d6c8a31a3bc18bc7a18f64aea8d62635dbb7e3f72c36f7638c221f/pydantic_core-2.48.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:75df20fe8ddab1153017cd791c965674b509b6fafbdf5aaef2e6793a65715694", size = 1914888, upload-time = "2026-08-06T14:24:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/b5/60/c5feb7147483668b9dabd9b66fa530f012d168ce28ee75cb1b1cfc577467/pydantic_core-2.48.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dc59ad81e0e1c3209c2040e24ec99a772155f7dcc62b09829422d4bc5ad3133", size = 1939973, upload-time = "2026-08-06T14:24:23.263Z" }, - { url = "https://files.pythonhosted.org/packages/23/40/6451ddf9dd1853993128b50bf5501b788105d678a402b2f468f4ca7082a3/pydantic_core-2.48.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9cf1ad7d2d3793bd15843180da3f29d1d475661f70788a696c0faac8abc1dffb", size = 2069026, upload-time = "2026-08-06T14:24:25.628Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4d/153ca46ffe938ff4c1b020a3f2d1117e11e8e3c9ce4151aec217b4670f0f/pydantic_core-2.48.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e94566a1c7eb35b9494ad72e3bfce0ef9bff451aa883ce7b73083ac0e05b572f", size = 2264691, upload-time = "2026-08-06T14:24:28.227Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/b69f11e3f162e64dd90cbec4fd5234979600391bf8a46ec9cf5ed4d9a4bc/pydantic_core-2.48.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:643332ae4b638daaa4130089ea32039dbbc9883cc21c555771cb12a3b10f392a", size = 2313963, upload-time = "2026-08-06T14:24:30.433Z" }, - { url = "https://files.pythonhosted.org/packages/84/b2/f5f329d2caa9b397afd89c861ce17d9d95a79218b0949f03597d95106c6e/pydantic_core-2.48.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c950716666760f5b5bb4c115f1466adb9b6a117d28c4a0489049e258a88d01", size = 2070202, upload-time = "2026-08-06T14:24:32.857Z" }, - { url = "https://files.pythonhosted.org/packages/ef/38/96c7017753e4d0ece819c5c245aefa03836e930b6785e8ada6389fb06ace/pydantic_core-2.48.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:fb7f8a79ac0c4fbd0849ac2431106659c0e2c3cfbfdd18810f2305a05661398b", size = 2140157, upload-time = "2026-08-06T14:24:35.614Z" }, - { url = "https://files.pythonhosted.org/packages/14/9c/8c43df859baffee901b02380972c2f14312a30759987c6e5da8f2efc1dde/pydantic_core-2.48.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:782e344102a11e25be4f416d9f14f0cc2710f255339ed95b0d1fb026892cca47", size = 2195094, upload-time = "2026-08-06T14:24:38.04Z" }, - { url = "https://files.pythonhosted.org/packages/a6/27/b29c4a3de121f09d935c95813b3d302bfab338b3ccf9e42944a409151d9d/pydantic_core-2.48.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:5619f21f760a7dfa6fd24f7d73b15d47c7327114d43f683791244a9b7e9127c9", size = 2190993, upload-time = "2026-08-06T14:24:40.581Z" }, - { url = "https://files.pythonhosted.org/packages/0d/70/35979c7fed1ea473571017a346f2d1cd592b2f646c6393a2a453b54018e5/pydantic_core-2.48.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:51ac6763fb9043d00343ffa11c1c46f06d73ce4f3491e64d82729519dec4489f", size = 2345602, upload-time = "2026-08-06T14:24:43.093Z" }, - { url = "https://files.pythonhosted.org/packages/b4/54/de9a3b21464f1917719a7c82801f1c5a27840ff9479b0379aaf136f02735/pydantic_core-2.48.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:c066ec851c4a742f7053e615a25794c30c4304f6ab1f6f8b7eb69e06a23758ec", size = 2392353, upload-time = "2026-08-06T14:24:45.615Z" }, - { url = "https://files.pythonhosted.org/packages/c0/92/de6405049ec6ba57419c7a893869d19f92e436fab06b028dc0d170384fb0/pydantic_core-2.48.0-cp314-cp314t-win32.whl", hash = "sha256:9310814fc611a1d40875c8f07cd4dcd156202d0fc2d9210fbf1cb4d130575e0d", size = 1960156, upload-time = "2026-08-06T14:24:48.022Z" }, - { url = "https://files.pythonhosted.org/packages/ad/22/50f6e4b1125859118a2f9604ebc74b96ad16d730d5823b369f4bc4432450/pydantic_core-2.48.0-cp314-cp314t-win_amd64.whl", hash = "sha256:54bdb4c47e063c109722fb723da0a306414700cde573ea59a6f0dd620d434f67", size = 2054413, upload-time = "2026-08-06T14:24:50.546Z" }, - { url = "https://files.pythonhosted.org/packages/2b/fc/2969e40c70d64f0c6e27e0586aa1449d7cb432ac619fe19831b2b52906bc/pydantic_core-2.48.0-cp314-cp314t-win_arm64.whl", hash = "sha256:157e1f7ad13864127d12b5909e2c3b02cc64ae041bd3fa9239ab292aff4ddfd1", size = 2051246, upload-time = "2026-08-06T14:24:52.887Z" }, - { url = "https://files.pythonhosted.org/packages/35/c2/c9c1723484f23e1dba7144b51abead1983a0e88a1d2d2edaf652c19f2833/pydantic_core-2.48.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:951d5f7754eb81381eae252af74f6c751b954b1f36e4e7d3b3b1f7a020ce0c6a", size = 2124365, upload-time = "2026-08-06T14:24:55.75Z" }, - { url = "https://files.pythonhosted.org/packages/97/4f/1d771e5d910fa172991615db3d850d68bc2a18b0fa33e56c9e04ff6d32fc/pydantic_core-2.48.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:c0f553fb3ace879b900e217285f83ab39bccdd0bf91238670b20b825d801037c", size = 1948433, upload-time = "2026-08-06T14:24:58.478Z" }, - { url = "https://files.pythonhosted.org/packages/05/9b/0566b0ab987cbf0648a8fb2386d1fcba1ccf2cdb3a10abb91b0953c13efb/pydantic_core-2.48.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd33c885e553a5bd83a210b341026817374dc192cf62bd169b969d732a155cd1", size = 1959601, upload-time = "2026-08-06T14:25:01.061Z" }, - { url = "https://files.pythonhosted.org/packages/16/b7/28fae984c468b46f5d38d9dc6f7579257aa155bb0ea1d4e4ed3d890535ce/pydantic_core-2.48.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f6b45257e2682fc2ea42d2ffeb1fb92630329a496214c2c438523f9b614cbc3", size = 2076937, upload-time = "2026-08-06T14:25:03.706Z" }, - { url = "https://files.pythonhosted.org/packages/05/7e/6f42bc05bd72a24885a19c6fdcf89891fd3588f719438110a0c7dbef84bc/pydantic_core-2.48.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc088260cc20d901d5c380e9c881ceb644cdb9c69580674e1c25a38d4417eb5b", size = 2260902, upload-time = "2026-08-06T14:25:06.228Z" }, - { url = "https://files.pythonhosted.org/packages/39/56/033f39acd30fb63f650b1a004f04215d4e1a266d6c04eb390138a1e6f449/pydantic_core-2.48.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f144e303ad0eaf41fd596a78a7706542eb97ba81c13c8fd51c2af5233b73590", size = 2320569, upload-time = "2026-08-06T14:25:08.718Z" }, - { url = "https://files.pythonhosted.org/packages/7f/5b/c81a249735b046e028f4287d85a08f5ed0eae86475c33bf6220225adb99b/pydantic_core-2.48.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd51d8521aa76356350f05951e45a9bd05a342e31c5d3ec437bfb5d327ade14d", size = 2078719, upload-time = "2026-08-06T14:25:11.247Z" }, - { url = "https://files.pythonhosted.org/packages/1e/1f/1de5b10bdbfc48bfdd8195b6e0a2747cd0d549aff317894bcffbaac835ad/pydantic_core-2.48.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:b1f382f0e0ec8623ea4f3f477ab22e67d6ba684e25f49a39a01b121dcf528bf3", size = 2151398, upload-time = "2026-08-06T14:25:14.192Z" }, - { url = "https://files.pythonhosted.org/packages/52/cd/ccdb8ee14e985b5acc7e5aae2c2e3a2afdf29429fb4d5dfe19c11f5adedf/pydantic_core-2.48.0-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:69c17bce5458c97b1c34d2ea2b388de6e27285d18d8b5eb14195d7de5dded37e", size = 2202063, upload-time = "2026-08-06T14:25:17.247Z" }, - { url = "https://files.pythonhosted.org/packages/64/f8/1cf0b2a3814492ebc4666bc104d4a451a1559e1f6c1b9b649bab220deab0/pydantic_core-2.48.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:a9e540e17c61ecadc2825cfe097efe1e618f67613c331ff6e35a30f7d0d71180", size = 2196335, upload-time = "2026-08-06T14:25:19.879Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d4/9eb451774f8a643c880e825540a8678d9f6fed0905a51ab879de95c750c5/pydantic_core-2.48.0-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:729e27d5877d3a61f829d43042293ace2b725c1e66f367a187c05d11280dab83", size = 2353940, upload-time = "2026-08-06T14:25:22.477Z" }, - { url = "https://files.pythonhosted.org/packages/8a/79/2a25a6d20dd2f99d95477573b512884557c435c6b08ed58c1b4203c1eb85/pydantic_core-2.48.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:83785d4c37e362ab1efdaf4067cd4a2ee1f7cc380ac7a7fad04a9542c697027f", size = 2387375, upload-time = "2026-08-06T14:25:25.307Z" }, - { url = "https://files.pythonhosted.org/packages/d3/bb/1ddac2399ad29413d2ab3ac9f12e0ea9b584122b7a0aa6716e2ebdbe67c9/pydantic_core-2.48.0-cp315-cp315-win32.whl", hash = "sha256:2ac14fb788fc51fb2cd044817bab9c98abd352fe3e4424463da805d18a0478fb", size = 1972536, upload-time = "2026-08-06T14:25:27.974Z" }, - { url = "https://files.pythonhosted.org/packages/c4/89/5136d40f49ac486e35fc8295f28f6e42c6e891b974004c814e589f586f9b/pydantic_core-2.48.0-cp315-cp315-win_amd64.whl", hash = "sha256:1778e349e1bcaf0b1850d4b9cf0a1ff4edea36bc11b69b560c6ef644e60a9e85", size = 2072192, upload-time = "2026-08-06T14:25:30.627Z" }, - { url = "https://files.pythonhosted.org/packages/35/62/c4fb6b7ac6c1e7420232b699b8b553bd48edca2c999b0aa03d9a2de62752/pydantic_core-2.48.0-cp315-cp315-win_arm64.whl", hash = "sha256:eaa9e2d0ef1bb345d6870f72ab12b2a5522231e38bfaddc0b903b5af00537267", size = 2039143, upload-time = "2026-08-06T14:25:33.373Z" }, - { url = "https://files.pythonhosted.org/packages/3e/fa/4afd93d028d7c6e3da4caa7536a02d4934b41a289a4605715cb1ccd889f3/pydantic_core-2.48.0-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:4e36b7ba6c7713d17d768b21e06763cb6440eaf3798cec4ac22e4c685c73d6cb", size = 2125205, upload-time = "2026-08-06T14:25:35.933Z" }, - { url = "https://files.pythonhosted.org/packages/b3/28/6eaa4ebbdba7abfa546624c5e1df33e1d046e4e04d3ded053792edb0b15f/pydantic_core-2.48.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bb833596eced78da91d24e7d84150085aaca21825e2267f098c972f4a790a1a6", size = 1914987, upload-time = "2026-08-06T14:25:38.79Z" }, - { url = "https://files.pythonhosted.org/packages/e5/04/0a5be1a9ca5b4448d58a2969e576fd4f7dc1514bdb818b8367d5b9f4169a/pydantic_core-2.48.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de1c807c15fd6403ef651c5ae6324a640d92cec6b39a5af58c57f4e031f05859", size = 1939994, upload-time = "2026-08-06T14:25:41.521Z" }, - { url = "https://files.pythonhosted.org/packages/da/80/d70810f2406ef8aeefcad7c79d93ab70ff854749304c911388be25b094e7/pydantic_core-2.48.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2216aae7b54dc12c8f6228dc2ca92dc758f4f8906f2fb5565063e2499361b910", size = 2069282, upload-time = "2026-08-06T14:25:44.157Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f4/8c8e07f4009e828a115d500999727194e30e854f2cc4c8977025255e3050/pydantic_core-2.48.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:837426357feb1f52c42b24e611728b394a6f4ae4ac7c461ed55be201bcbc811f", size = 2264913, upload-time = "2026-08-06T14:25:46.866Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ed/a2e36d8aa2feb98eccac2641c07945cfd58c81656831986c7973cb6b5ecb/pydantic_core-2.48.0-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb8807da40e346c5ae65846e721d96ef84776e103b5ad6923a95843ba0e17eac", size = 2313972, upload-time = "2026-08-06T14:25:49.664Z" }, - { url = "https://files.pythonhosted.org/packages/4b/e8/106c6f135e260f884786e95655b8347a41131f1a2ce807fc2611ee7800b8/pydantic_core-2.48.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55bb2a177d33bbf22e441f34928183230018518578360cd782bbe17e3a877b5e", size = 2071006, upload-time = "2026-08-06T14:25:52.338Z" }, - { url = "https://files.pythonhosted.org/packages/39/8e/19ed0f6e36047ff6fa04990c50d0f031f3b8f54168f1671c34dc0454d636/pydantic_core-2.48.0-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:d77765d9c9bc37edc2ffb98e724a3e57b08932bef5407ba04f00bc59854af113", size = 2140081, upload-time = "2026-08-06T14:25:55.428Z" }, - { url = "https://files.pythonhosted.org/packages/69/43/40fb548caa91b673ba479b4649a45a96875391b5a5ed8769d943e23c3324/pydantic_core-2.48.0-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d058233dba56eb3de2eae96cb0acacc521417ed34de72507c9360202cef32ace", size = 2195173, upload-time = "2026-08-06T14:25:58.361Z" }, - { url = "https://files.pythonhosted.org/packages/31/21/6094939bac24f2b61ca8b994fc8560050656a3828d85e9563b5b6ffba97c/pydantic_core-2.48.0-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:95c0e79985e0c1af0f769384f369412652f06e2e3d5a679223d15820f4881107", size = 2191111, upload-time = "2026-08-06T14:26:01.213Z" }, - { url = "https://files.pythonhosted.org/packages/70/46/7043d35aa1f061ecc1950cd690e559292854d2e300b197c3adf865c49797/pydantic_core-2.48.0-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:2384954580b564a3ae0ffc8bf37c794c75c22dbb3cac0a9f941b9ad1de3e4e03", size = 2345837, upload-time = "2026-08-06T14:26:04.226Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b8/d9d42bdc1a1e45864ffbca118c08f8fec57c736f4b89f93f28282990bb34/pydantic_core-2.48.0-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:c40ba3924215e6c1616e897dc4342942db38c0f21a3bf50d7f6adfe3d9de344c", size = 2392376, upload-time = "2026-08-06T14:26:06.917Z" }, - { url = "https://files.pythonhosted.org/packages/d4/b0/807114c79eb12cd56bf484ac3e0fe584401b7783461c3e12250d9a47dafc/pydantic_core-2.48.0-cp315-cp315t-win32.whl", hash = "sha256:e88a758fc73c5e5e031c33b11f114e112ec0d5b5fa537db7d8d5e8c44c3c1841", size = 1960339, upload-time = "2026-08-06T14:26:09.755Z" }, - { url = "https://files.pythonhosted.org/packages/60/1e/b1104f7ccc1de5be4e2dab5e4dbc3ac92e5b34b902dace37e1308d9d539b/pydantic_core-2.48.0-cp315-cp315t-win_amd64.whl", hash = "sha256:1992edde312b15554d048176de39d7dbd59b3d637a96d71457e5251d089f7662", size = 2054666, upload-time = "2026-08-06T14:26:12.528Z" }, - { url = "https://files.pythonhosted.org/packages/96/0e/60228794a20a6b77ad09e3c6018a85206575828b5aa73782846bc1c68272/pydantic_core-2.48.0-cp315-cp315t-win_arm64.whl", hash = "sha256:1514098b2d6d91e94840b90fa801e717893255c7468061faf38728ac27063d55", size = 2051453, upload-time = "2026-08-06T14:26:15.237Z" }, - { url = "https://files.pythonhosted.org/packages/fc/5d/cebc9ab997f34848ba5d1815fee3312f0f05d9d0dd2005a43ae1fc9457b1/pydantic_core-2.48.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:36196b58c314af63cb1350f57ebfbed122678f2727e8ad3f41015b9d57e5dc94", size = 2109109, upload-time = "2026-08-06T14:26:29.511Z" }, - { url = "https://files.pythonhosted.org/packages/14/5e/c79c22aa32d57b9a0c97be5efe84ecd42804660d5ac83b142d34361c77b9/pydantic_core-2.48.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:83a9509271a3a1314fc2a928d98b3b9910250e42a9a409b852454a4c6fee4710", size = 1940937, upload-time = "2026-08-06T14:26:32.324Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e4/aab480442be0228415eaacadc511971936ad9b5edab539dd3d9c69099e2b/pydantic_core-2.48.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f751b64b99cbb1596ae6e9a044610a1dd716f07f65ef4dfcb8693458d66b14", size = 2006022, upload-time = "2026-08-06T14:26:35.108Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b7/5de142e36cfadbc962d223e5ba854b2bfe67958c9ae5a3ba3fbc97adf6ee/pydantic_core-2.48.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f1d556f02ae80aab0626639eb1aed304e7d37e52791839b39d7c7ae0acfbfbd", size = 2154883, upload-time = "2026-08-06T14:26:38.029Z" }, -] - [[package]] name = "pydantic-settings" version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic", version = "2.13.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.15'" }, - { name = "pydantic", version = "2.14.0b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.15'" }, + { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ]