cfb-data 0.7.0__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cfb_data-0.7.0 → cfb_data-0.8.0}/PKG-INFO +7 -4
- {cfb_data-0.7.0 → cfb_data-0.8.0}/README.md +5 -2
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_dataframes.py +16 -21
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_executor.py +7 -12
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_parquet.py +13 -11
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_requests.py +2 -3
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_transport.py +38 -16
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_backend.py +11 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_coordinator.py +94 -4
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_null.py +10 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_redis.py +14 -1
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_sqlite.py +15 -0
- cfb_data-0.8.0/cfb_data/cfb_data/cache/sql/select_projection_contract.sql +3 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/errors.py +1 -24
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data.egg-info/PKG-INFO +7 -4
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data.egg-info/SOURCES.txt +1 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/pyproject.toml +2 -2
- {cfb_data-0.7.0 → cfb_data-0.8.0}/LICENSE +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_catalog/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_catalog/merge.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_catalog/models.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_catalog/projection.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_catalog/sources.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_observability.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_reference_enums.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_request_rules.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_tabular.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/adjusted_metrics/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/adjusted_metrics/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/adjusted_metrics/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/adjusted_metrics/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/adjusted_metrics/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/adjusted_metrics/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/base/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/base/types.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/betting/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/betting/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/betting/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/betting/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/betting/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/betting/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_catalog.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_catalog_codecs.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_identity_codecs.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_key.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_models.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_sqlite_sql.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/config.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/policy.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/acquire_refresh_lease.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/begin_immediate.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/catalog_counts.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/cleanup_responses.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/delete_coverage_failure.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/delete_response.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/delete_team_aliases.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/enable_foreign_keys.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/enable_wal.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_athletes.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_conference_by_id.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_conference_by_name.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_fresh_coverage.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_game_by_id.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_games.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_team_by_id.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_team_by_name.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_venue_by_id.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_venue_by_name.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/get_response.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/get_response_size.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/get_schema_version.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/initialize_schema_version.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/release_refresh_lease.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/renew_refresh_lease.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/schema.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/select_catalog_observations.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/set_busy_timeout.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/set_synchronous_normal.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_athlete.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_athlete_team_season.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_catalog_observation.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_coach.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_coach_team_season.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_conference.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_conference_affiliation.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_coverage.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_coverage_failure.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_drive.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_game.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_play.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_playoff_matchup.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_recruit.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_response.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_team.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_team_alias.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_team_season.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_venue.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_vocabulary.sql +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/client.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/coaches/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/coaches/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/coaches/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/coaches/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/coaches/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/coaches/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/conferences/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/conferences/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/conferences/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/conferences/models/pydantic/identity.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/conferences/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/conferences/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/conferences/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/draft/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/draft/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/draft/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/draft/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/draft/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/draft/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/drives/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/drives/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/drives/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/drives/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/drives/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/drives/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/enums.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/games/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/games/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/games/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/games/models/pydantic/identity.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/games/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/games/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/games/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/identities/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/identities/_normalization.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/identities/contracts.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/identities/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/info/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/info/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/info/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/info/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/info/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/info/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/metrics/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/metrics/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/metrics/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/metrics/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/metrics/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/metrics/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/observability.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/players/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/players/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/players/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/players/models/pydantic/identity.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/players/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/players/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/players/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/playoffs/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/playoffs/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/playoffs/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/playoffs/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/playoffs/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/playoffs/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/plays/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/plays/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/plays/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/plays/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/plays/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/plays/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/py.typed +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/rankings/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/rankings/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/rankings/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/rankings/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/rankings/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/rankings/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/ratings/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/ratings/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/ratings/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/ratings/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/ratings/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/ratings/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/recruiting/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/recruiting/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/recruiting/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/recruiting/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/recruiting/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/recruiting/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/retry.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/stats/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/stats/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/stats/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/stats/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/stats/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/stats/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/teams/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/teams/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/teams/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/teams/models/pydantic/identity.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/teams/models/pydantic/requests.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/teams/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/teams/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/venues/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/venues/models/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/venues/models/pydantic/__init__.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/venues/models/pydantic/identity.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/venues/models/pydantic/responses.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/venues/resource.py +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data.egg-info/dependency_links.txt +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data.egg-info/requires.txt +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data.egg-info/top_level.txt +0 -0
- {cfb_data-0.7.0 → cfb_data-0.8.0}/setup.cfg +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cfb-data
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Async validated CollegeFootballData access for pandas and Polars
|
|
5
5
|
Author: Ryan Anderson
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Documentation, https://ryanpaulanderson.github.io/cfb-data/
|
|
8
8
|
Project-URL: Repository, https://github.com/ryanpaulanderson/cfb-data
|
|
9
9
|
Project-URL: Issues, https://github.com/ryanpaulanderson/cfb-data/issues
|
|
10
|
-
Classifier: Development Status ::
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -41,7 +41,7 @@ Dynamic: license-file
|
|
|
41
41
|
|
|
42
42
|
# College Football Data Python Toolkit
|
|
43
43
|
|
|
44
|
-
`cfb-data` 0.
|
|
44
|
+
`cfb-data` 0.8.0 is a beta Python toolkit for exploring the public
|
|
45
45
|
[CollegeFootballData API](https://collegefootballdata.com/). Most calls return
|
|
46
46
|
eager pandas DataFrames that are ready for analysis. Polars is available as an
|
|
47
47
|
option, and a few naturally nested results return Pydantic models.
|
|
@@ -280,7 +280,10 @@ is organized in layers:
|
|
|
280
280
|
The [product constitution](docs/product-constitution.md) is the concise north
|
|
281
281
|
star used to evaluate product and engineering decisions.
|
|
282
282
|
|
|
283
|
-
The project is
|
|
283
|
+
The project is in beta: its core retrieval, validation, DataFrame, caching, and
|
|
284
|
+
identity workflows are usable and carefully tested. Public APIs and local cache
|
|
285
|
+
formats may still change before 1.0, and the documentation describes the
|
|
286
|
+
current version.
|
|
284
287
|
|
|
285
288
|
## Development
|
|
286
289
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# College Football Data Python Toolkit
|
|
2
2
|
|
|
3
|
-
`cfb-data` 0.
|
|
3
|
+
`cfb-data` 0.8.0 is a beta Python toolkit for exploring the public
|
|
4
4
|
[CollegeFootballData API](https://collegefootballdata.com/). Most calls return
|
|
5
5
|
eager pandas DataFrames that are ready for analysis. Polars is available as an
|
|
6
6
|
option, and a few naturally nested results return Pydantic models.
|
|
@@ -239,7 +239,10 @@ is organized in layers:
|
|
|
239
239
|
The [product constitution](docs/product-constitution.md) is the concise north
|
|
240
240
|
star used to evaluate product and engineering decisions.
|
|
241
241
|
|
|
242
|
-
The project is
|
|
242
|
+
The project is in beta: its core retrieval, validation, DataFrame, caching, and
|
|
243
|
+
identity workflows are usable and carefully tested. Public APIs and local cache
|
|
244
|
+
formats may still change before 1.0, and the documentation describes the
|
|
245
|
+
current version.
|
|
243
246
|
|
|
244
247
|
## Development
|
|
245
248
|
|
|
@@ -19,7 +19,6 @@ from cfb_data._tabular import (
|
|
|
19
19
|
from cfb_data.errors import (
|
|
20
20
|
CFBDDataFrameConversionError,
|
|
21
21
|
CFBDOptionalDependencyError,
|
|
22
|
-
_sanitized_cause,
|
|
23
22
|
)
|
|
24
23
|
|
|
25
24
|
if TYPE_CHECKING:
|
|
@@ -78,11 +77,10 @@ class _PandasAdapter:
|
|
|
78
77
|
except CFBDDataFrameConversionError:
|
|
79
78
|
raise
|
|
80
79
|
except Exception as exc:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
) from safe_cause
|
|
80
|
+
raise CFBDDataFrameConversionError(
|
|
81
|
+
endpoint=endpoint,
|
|
82
|
+
backend="pandas",
|
|
83
|
+
) from exc
|
|
86
84
|
|
|
87
85
|
def from_table(
|
|
88
86
|
self,
|
|
@@ -125,11 +123,10 @@ class _PandasAdapter:
|
|
|
125
123
|
except CFBDDataFrameConversionError:
|
|
126
124
|
raise
|
|
127
125
|
except Exception as exc:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
) from safe_cause
|
|
126
|
+
raise CFBDDataFrameConversionError(
|
|
127
|
+
endpoint=endpoint,
|
|
128
|
+
backend="pandas",
|
|
129
|
+
) from exc
|
|
133
130
|
|
|
134
131
|
|
|
135
132
|
class _PolarsAdapter:
|
|
@@ -162,11 +159,10 @@ class _PolarsAdapter:
|
|
|
162
159
|
except CFBDDataFrameConversionError:
|
|
163
160
|
raise
|
|
164
161
|
except Exception as exc:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
) from safe_cause
|
|
162
|
+
raise CFBDDataFrameConversionError(
|
|
163
|
+
endpoint=endpoint,
|
|
164
|
+
backend="polars",
|
|
165
|
+
) from exc
|
|
170
166
|
|
|
171
167
|
def from_table(
|
|
172
168
|
self,
|
|
@@ -229,11 +225,10 @@ class _PolarsAdapter:
|
|
|
229
225
|
except CFBDOptionalDependencyError:
|
|
230
226
|
raise
|
|
231
227
|
except Exception as exc:
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
) from safe_cause
|
|
228
|
+
raise CFBDDataFrameConversionError(
|
|
229
|
+
endpoint=endpoint,
|
|
230
|
+
backend="polars",
|
|
231
|
+
) from exc
|
|
237
232
|
|
|
238
233
|
@staticmethod
|
|
239
234
|
def _require_polars() -> None:
|
|
@@ -26,7 +26,6 @@ from cfb_data.cache._coordinator import CacheCoordinator
|
|
|
26
26
|
from cfb_data.errors import (
|
|
27
27
|
CFBDRequestValidationError,
|
|
28
28
|
CFBDResponseValidationError,
|
|
29
|
-
_sanitized_cause,
|
|
30
29
|
)
|
|
31
30
|
from cfb_data.observability import (
|
|
32
31
|
RetrievalFinished,
|
|
@@ -210,8 +209,7 @@ def _serialize_request(
|
|
|
210
209
|
)
|
|
211
210
|
return parameters
|
|
212
211
|
except TypeError as exc:
|
|
213
|
-
|
|
214
|
-
raise CFBDRequestValidationError(endpoint=endpoint) from safe_cause
|
|
212
|
+
raise CFBDRequestValidationError(endpoint=endpoint) from exc
|
|
215
213
|
|
|
216
214
|
|
|
217
215
|
def _response_contract[ValueT](response_adapter: TypeAdapter[ValueT]) -> str:
|
|
@@ -227,12 +225,11 @@ def _validate_many[ModelT: BaseModel](
|
|
|
227
225
|
raw: object,
|
|
228
226
|
response_adapter: TypeAdapter[list[ModelT]],
|
|
229
227
|
) -> list[ModelT]:
|
|
230
|
-
"""Validate one decoded model-list response with
|
|
228
|
+
"""Validate one decoded model-list response with its diagnostic cause."""
|
|
231
229
|
try:
|
|
232
230
|
return response_adapter.validate_python(json_object_list(raw))
|
|
233
231
|
except (TypeError, ValidationError) as exc:
|
|
234
|
-
|
|
235
|
-
raise CFBDResponseValidationError(endpoint=endpoint) from safe_cause
|
|
232
|
+
raise CFBDResponseValidationError(endpoint=endpoint) from exc
|
|
236
233
|
|
|
237
234
|
|
|
238
235
|
def _validate_one[ModelT: BaseModel](
|
|
@@ -240,12 +237,11 @@ def _validate_one[ModelT: BaseModel](
|
|
|
240
237
|
raw: object,
|
|
241
238
|
response_adapter: TypeAdapter[ModelT],
|
|
242
239
|
) -> ModelT:
|
|
243
|
-
"""Validate one decoded model response with
|
|
240
|
+
"""Validate one decoded model response with its diagnostic cause."""
|
|
244
241
|
try:
|
|
245
242
|
return response_adapter.validate_python(json_object(raw))
|
|
246
243
|
except (TypeError, ValidationError) as exc:
|
|
247
|
-
|
|
248
|
-
raise CFBDResponseValidationError(endpoint=endpoint) from safe_cause
|
|
244
|
+
raise CFBDResponseValidationError(endpoint=endpoint) from exc
|
|
249
245
|
|
|
250
246
|
|
|
251
247
|
def _validate_values[ValueT](
|
|
@@ -253,9 +249,8 @@ def _validate_values[ValueT](
|
|
|
253
249
|
raw: object,
|
|
254
250
|
response_adapter: TypeAdapter[list[ValueT]],
|
|
255
251
|
) -> list[ValueT]:
|
|
256
|
-
"""Validate one decoded scalar-list response with
|
|
252
|
+
"""Validate one decoded scalar-list response with its diagnostic cause."""
|
|
257
253
|
try:
|
|
258
254
|
return response_adapter.validate_python(json_list(raw))
|
|
259
255
|
except (TypeError, ValidationError) as exc:
|
|
260
|
-
|
|
261
|
-
raise CFBDResponseValidationError(endpoint=endpoint) from safe_cause
|
|
256
|
+
raise CFBDResponseValidationError(endpoint=endpoint) from exc
|
|
@@ -22,7 +22,7 @@ from cfb_data._tabular import (
|
|
|
22
22
|
_ScalarEncodingError,
|
|
23
23
|
_UnsupportedTableAnnotationError,
|
|
24
24
|
)
|
|
25
|
-
from cfb_data.errors import CFBDError
|
|
25
|
+
from cfb_data.errors import CFBDError
|
|
26
26
|
|
|
27
27
|
_ParquetOperation = Literal["read", "write"]
|
|
28
28
|
_ParquetErrorCategory = Literal[
|
|
@@ -39,7 +39,7 @@ _PARQUET_COMPRESSION: Final = "snappy"
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
class _ParquetCodecError(CFBDError):
|
|
42
|
-
"""Report a
|
|
42
|
+
"""Report a categorized failure in the internal Parquet codec."""
|
|
43
43
|
|
|
44
44
|
operation: _ParquetOperation
|
|
45
45
|
category: _ParquetErrorCategory
|
|
@@ -50,10 +50,10 @@ class _ParquetCodecError(CFBDError):
|
|
|
50
50
|
operation: _ParquetOperation,
|
|
51
51
|
category: _ParquetErrorCategory,
|
|
52
52
|
) -> None:
|
|
53
|
-
"""Initialize a
|
|
53
|
+
"""Initialize a categorized codec failure.
|
|
54
54
|
|
|
55
55
|
:param operation: File operation that failed.
|
|
56
|
-
:param category:
|
|
56
|
+
:param category: Failure classification for internal policy.
|
|
57
57
|
"""
|
|
58
58
|
self.operation = operation
|
|
59
59
|
self.category = category
|
|
@@ -80,9 +80,10 @@ def _write_parquet(
|
|
|
80
80
|
_write_parquet_file(Path(path), row_model=row_model, table=table)
|
|
81
81
|
return
|
|
82
82
|
except Exception as exc:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
raise _ParquetCodecError(
|
|
84
|
+
operation="write",
|
|
85
|
+
category=_codec_error_category(exc),
|
|
86
|
+
) from exc
|
|
86
87
|
|
|
87
88
|
|
|
88
89
|
def _read_parquet[ModelT: BaseModel](
|
|
@@ -118,9 +119,10 @@ def _read_parquet[ModelT: BaseModel](
|
|
|
118
119
|
)
|
|
119
120
|
return table
|
|
120
121
|
except Exception as exc:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
raise _ParquetCodecError(
|
|
123
|
+
operation="read",
|
|
124
|
+
category=_codec_error_category(exc),
|
|
125
|
+
) from exc
|
|
124
126
|
|
|
125
127
|
|
|
126
128
|
def _write_parquet_file(
|
|
@@ -181,7 +183,7 @@ def _read_parquet_file[ModelT: BaseModel](
|
|
|
181
183
|
|
|
182
184
|
|
|
183
185
|
def _codec_error_category(source: Exception) -> _ParquetErrorCategory:
|
|
184
|
-
"""Classify a source exception
|
|
186
|
+
"""Classify a source exception for internal recovery policy."""
|
|
185
187
|
if isinstance(source, _CanonicalTableMetadataError):
|
|
186
188
|
return "metadata"
|
|
187
189
|
if isinstance(source, _CanonicalTableSchemaError):
|
|
@@ -6,7 +6,7 @@ from collections.abc import Mapping
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, ValidationError
|
|
8
8
|
|
|
9
|
-
from cfb_data.errors import CFBDRequestValidationError
|
|
9
|
+
from cfb_data.errors import CFBDRequestValidationError
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def _resolve_request[RequestT: BaseModel](
|
|
@@ -32,5 +32,4 @@ def _resolve_request[RequestT: BaseModel](
|
|
|
32
32
|
try:
|
|
33
33
|
return request_type.model_validate(filters)
|
|
34
34
|
except ValidationError as exc:
|
|
35
|
-
|
|
36
|
-
raise CFBDRequestValidationError(endpoint=endpoint) from safe_cause
|
|
35
|
+
raise CFBDRequestValidationError(endpoint=endpoint) from exc
|
|
@@ -37,8 +37,6 @@ from cfb_data.errors import (
|
|
|
37
37
|
CFBDTimeoutError,
|
|
38
38
|
CFBDTLSError,
|
|
39
39
|
CFBDTransportError,
|
|
40
|
-
_sanitized_cause,
|
|
41
|
-
_SanitizedCause,
|
|
42
40
|
)
|
|
43
41
|
from cfb_data.observability import (
|
|
44
42
|
HTTPAttemptFinished,
|
|
@@ -51,6 +49,29 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
51
49
|
_RETRYABLE_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
|
|
52
50
|
|
|
53
51
|
|
|
52
|
+
class _DetachedTransportCause(Exception):
|
|
53
|
+
"""Retain a transport category without request metadata or credentials."""
|
|
54
|
+
|
|
55
|
+
category: str
|
|
56
|
+
|
|
57
|
+
def __init__(self, *, category: str) -> None:
|
|
58
|
+
"""Initialize a credential-safe transport cause.
|
|
59
|
+
|
|
60
|
+
:param category: Source exception type name without its retained state.
|
|
61
|
+
"""
|
|
62
|
+
self.category = category
|
|
63
|
+
super().__init__(category)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _detached_transport_cause(source: BaseException) -> _DetachedTransportCause:
|
|
67
|
+
"""Detach an exception that may retain authenticated request metadata.
|
|
68
|
+
|
|
69
|
+
:param source: Transport exception whose type identifies the failure.
|
|
70
|
+
:return: New exception retaining only the source type name.
|
|
71
|
+
"""
|
|
72
|
+
return _DetachedTransportCause(category=type(source).__name__)
|
|
73
|
+
|
|
74
|
+
|
|
54
75
|
class _TransportState(Enum):
|
|
55
76
|
"""Track the one-shot session lifecycle."""
|
|
56
77
|
|
|
@@ -73,7 +94,7 @@ class _TransportFailure:
|
|
|
73
94
|
"""Carry a safely detached transport failure outside its exception handler."""
|
|
74
95
|
|
|
75
96
|
error: CFBDTransportError
|
|
76
|
-
cause:
|
|
97
|
+
cause: _DetachedTransportCause
|
|
77
98
|
category: str
|
|
78
99
|
retryable: bool
|
|
79
100
|
|
|
@@ -275,21 +296,21 @@ class _HTTPTransport:
|
|
|
275
296
|
attempts=attempt,
|
|
276
297
|
category="invalid_url",
|
|
277
298
|
),
|
|
278
|
-
cause=
|
|
299
|
+
cause=_detached_transport_cause(exc),
|
|
279
300
|
category="invalid_url",
|
|
280
301
|
retryable=False,
|
|
281
302
|
)
|
|
282
303
|
except aiohttp.ClientSSLError as exc:
|
|
283
304
|
failure = _TransportFailure(
|
|
284
305
|
error=CFBDTLSError(endpoint=endpoint, attempts=attempt),
|
|
285
|
-
cause=
|
|
306
|
+
cause=_detached_transport_cause(exc),
|
|
286
307
|
category="tls",
|
|
287
308
|
retryable=False,
|
|
288
309
|
)
|
|
289
310
|
except TimeoutError as exc:
|
|
290
311
|
failure = _TransportFailure(
|
|
291
312
|
error=CFBDTimeoutError(endpoint=endpoint, attempts=attempt),
|
|
292
|
-
cause=
|
|
313
|
+
cause=_detached_transport_cause(exc),
|
|
293
314
|
category="timeout",
|
|
294
315
|
retryable=True,
|
|
295
316
|
)
|
|
@@ -300,7 +321,7 @@ class _HTTPTransport:
|
|
|
300
321
|
attempts=attempt,
|
|
301
322
|
category="truncated_payload",
|
|
302
323
|
),
|
|
303
|
-
cause=
|
|
324
|
+
cause=_detached_transport_cause(exc),
|
|
304
325
|
category="truncated_payload",
|
|
305
326
|
retryable=True,
|
|
306
327
|
)
|
|
@@ -311,7 +332,7 @@ class _HTTPTransport:
|
|
|
311
332
|
attempts=attempt,
|
|
312
333
|
category="connection",
|
|
313
334
|
),
|
|
314
|
-
cause=
|
|
335
|
+
cause=_detached_transport_cause(exc),
|
|
315
336
|
category="connection",
|
|
316
337
|
retryable=True,
|
|
317
338
|
)
|
|
@@ -322,7 +343,7 @@ class _HTTPTransport:
|
|
|
322
343
|
attempts=attempt,
|
|
323
344
|
category="client",
|
|
324
345
|
),
|
|
325
|
-
cause=
|
|
346
|
+
cause=_detached_transport_cause(exc),
|
|
326
347
|
category="client",
|
|
327
348
|
retryable=False,
|
|
328
349
|
)
|
|
@@ -477,18 +498,19 @@ class _HTTPTransport:
|
|
|
477
498
|
headers=response.headers,
|
|
478
499
|
)
|
|
479
500
|
decoded: object = json.loads(raw_body)
|
|
480
|
-
except
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
501
|
+
except aiohttp.ContentTypeError as exc:
|
|
502
|
+
detached_cause = _detached_transport_cause(exc)
|
|
503
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
504
|
+
raise CFBDResponseDecodeError(
|
|
505
|
+
endpoint=endpoint,
|
|
506
|
+
attempts=attempt,
|
|
507
|
+
) from exc
|
|
486
508
|
else:
|
|
487
509
|
return _response_envelope(response, body=decoded, raw_body=raw_body)
|
|
488
510
|
raise CFBDResponseDecodeError(
|
|
489
511
|
endpoint=endpoint,
|
|
490
512
|
attempts=attempt,
|
|
491
|
-
) from
|
|
513
|
+
) from detached_cause
|
|
492
514
|
|
|
493
515
|
def _start_attempt(
|
|
494
516
|
self,
|
|
@@ -60,6 +60,16 @@ class CatalogWriter(Protocol):
|
|
|
60
60
|
...
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
class ProjectionStateStore(Protocol):
|
|
64
|
+
"""Report whether durable coverage uses the current projection contract."""
|
|
65
|
+
|
|
66
|
+
async def has_current_projection(
|
|
67
|
+
self, *, endpoint: str, canonical_filters: str
|
|
68
|
+
) -> bool:
|
|
69
|
+
"""Return whether one partition was projected by the current contract."""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
|
|
63
73
|
class CoverageStore(Protocol):
|
|
64
74
|
"""Read and record capability-aware catalog coverage."""
|
|
65
75
|
|
|
@@ -150,6 +160,7 @@ class CacheBackend(
|
|
|
150
160
|
BackendLifecycle,
|
|
151
161
|
ResponseStore,
|
|
152
162
|
CatalogWriter,
|
|
163
|
+
ProjectionStateStore,
|
|
153
164
|
CoverageStore,
|
|
154
165
|
LeaseStore,
|
|
155
166
|
IdentityReader,
|
|
@@ -24,7 +24,7 @@ from cfb_data._observability import (
|
|
|
24
24
|
from cfb_data._transport import _HTTPTransport
|
|
25
25
|
from cfb_data.base.types import QueryParameters
|
|
26
26
|
from cfb_data.cache._backend import CacheBackend
|
|
27
|
-
from cfb_data.cache._catalog import project_catalog
|
|
27
|
+
from cfb_data.cache._catalog import canonical_filters, project_catalog
|
|
28
28
|
from cfb_data.cache._key import response_cache_key
|
|
29
29
|
from cfb_data.cache._models import MAX_RESPONSE_BODY_BYTES, ResponseRecord
|
|
30
30
|
from cfb_data.cache._null import NullCacheBackend
|
|
@@ -154,6 +154,7 @@ class CacheCoordinator:
|
|
|
154
154
|
)
|
|
155
155
|
self._flights: dict[str, _FlightState] = {}
|
|
156
156
|
self._flights_lock = asyncio.Lock()
|
|
157
|
+
self._catalog_write_lock = asyncio.Lock()
|
|
157
158
|
self._backend_available = False
|
|
158
159
|
self._transient_available = False
|
|
159
160
|
|
|
@@ -1184,8 +1185,74 @@ class CacheCoordinator:
|
|
|
1184
1185
|
fresh_until=record.fresh_until,
|
|
1185
1186
|
retained_until=record.retained_until,
|
|
1186
1187
|
)
|
|
1187
|
-
|
|
1188
|
-
|
|
1188
|
+
filters = canonical_filters(parameters)
|
|
1189
|
+
answered, current = await self._projection_is_current(
|
|
1190
|
+
endpoint=endpoint,
|
|
1191
|
+
canonical_filters=filters,
|
|
1192
|
+
context=context,
|
|
1193
|
+
)
|
|
1194
|
+
if not answered:
|
|
1195
|
+
await self._transient.commit_response(record, projection)
|
|
1196
|
+
return
|
|
1197
|
+
if current:
|
|
1198
|
+
await self._mirror_catalog_projection(record, projection, context=context)
|
|
1199
|
+
return
|
|
1200
|
+
|
|
1201
|
+
async with self._catalog_write_lock:
|
|
1202
|
+
answered, current = await self._projection_is_current(
|
|
1203
|
+
endpoint=endpoint,
|
|
1204
|
+
canonical_filters=filters,
|
|
1205
|
+
context=context,
|
|
1206
|
+
)
|
|
1207
|
+
if not answered:
|
|
1208
|
+
await self._transient.commit_response(record, projection)
|
|
1209
|
+
return
|
|
1210
|
+
if current:
|
|
1211
|
+
await self._mirror_catalog_projection(
|
|
1212
|
+
record, projection, context=context
|
|
1213
|
+
)
|
|
1214
|
+
return
|
|
1215
|
+
await self._commit_catalog_response_locked(
|
|
1216
|
+
"reproject",
|
|
1217
|
+
record,
|
|
1218
|
+
projection,
|
|
1219
|
+
timeout_seconds=_MAX_CATALOG_COMMIT_TIMEOUT_SECONDS,
|
|
1220
|
+
context=context,
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1223
|
+
async def _projection_is_current(
|
|
1224
|
+
self,
|
|
1225
|
+
*,
|
|
1226
|
+
endpoint: str,
|
|
1227
|
+
canonical_filters: str,
|
|
1228
|
+
context: _OperationContext | None,
|
|
1229
|
+
) -> tuple[bool, bool]:
|
|
1230
|
+
"""Return whether durable projection metadata answered and is current."""
|
|
1231
|
+
answered, current = await self._catalog_call(
|
|
1232
|
+
"reproject_status",
|
|
1233
|
+
self._backend.has_current_projection(
|
|
1234
|
+
endpoint=endpoint,
|
|
1235
|
+
canonical_filters=canonical_filters,
|
|
1236
|
+
),
|
|
1237
|
+
context=context,
|
|
1238
|
+
)
|
|
1239
|
+
return answered, bool(current)
|
|
1240
|
+
|
|
1241
|
+
async def _mirror_catalog_projection(
|
|
1242
|
+
self,
|
|
1243
|
+
record: ResponseRecord,
|
|
1244
|
+
projection: CatalogProjection,
|
|
1245
|
+
*,
|
|
1246
|
+
context: _OperationContext | None,
|
|
1247
|
+
) -> None:
|
|
1248
|
+
"""Mirror one read-only canonical merge into the transient catalog."""
|
|
1249
|
+
answered, merged = await self._catalog_call(
|
|
1250
|
+
"reproject_merge_read",
|
|
1251
|
+
self._backend.merge_catalog_projection(record, projection),
|
|
1252
|
+
context=context,
|
|
1253
|
+
)
|
|
1254
|
+
await self._transient.commit_response(
|
|
1255
|
+
record, merged if answered and merged is not None else projection
|
|
1189
1256
|
)
|
|
1190
1257
|
|
|
1191
1258
|
async def _commit_catalog_response(
|
|
@@ -1197,10 +1264,33 @@ class CacheCoordinator:
|
|
|
1197
1264
|
context: _OperationContext | None,
|
|
1198
1265
|
) -> None:
|
|
1199
1266
|
"""Commit durably and mirror the canonical merged state in memory."""
|
|
1267
|
+
async with self._catalog_write_lock:
|
|
1268
|
+
await self._commit_catalog_response_locked(
|
|
1269
|
+
operation,
|
|
1270
|
+
record,
|
|
1271
|
+
projection,
|
|
1272
|
+
timeout_seconds=None,
|
|
1273
|
+
context=context,
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1276
|
+
async def _commit_catalog_response_locked(
|
|
1277
|
+
self,
|
|
1278
|
+
operation: str,
|
|
1279
|
+
record: ResponseRecord,
|
|
1280
|
+
projection: CatalogProjection,
|
|
1281
|
+
*,
|
|
1282
|
+
timeout_seconds: float | None,
|
|
1283
|
+
context: _OperationContext | None,
|
|
1284
|
+
) -> None:
|
|
1285
|
+
"""Commit while the process-local catalog-write lock is held."""
|
|
1200
1286
|
answered, merged = await self._catalog_call(
|
|
1201
1287
|
operation,
|
|
1202
1288
|
self._backend.commit_response(record, projection),
|
|
1203
|
-
timeout_seconds=
|
|
1289
|
+
timeout_seconds=(
|
|
1290
|
+
timeout_seconds
|
|
1291
|
+
if timeout_seconds is not None
|
|
1292
|
+
else self._catalog_commit_timeout(projection)
|
|
1293
|
+
),
|
|
1204
1294
|
context=context,
|
|
1205
1295
|
)
|
|
1206
1296
|
write_answered = answered
|
|
@@ -142,6 +142,16 @@ class NullCacheBackend:
|
|
|
142
142
|
)
|
|
143
143
|
return projection_from_observations(merged, original=projection)
|
|
144
144
|
|
|
145
|
+
async def has_current_projection(
|
|
146
|
+
self, *, endpoint: str, canonical_filters: str
|
|
147
|
+
) -> bool:
|
|
148
|
+
"""Return whether transient coverage uses the current contract."""
|
|
149
|
+
coverage = self._coverage.get((endpoint, canonical_filters))
|
|
150
|
+
return bool(
|
|
151
|
+
coverage is not None
|
|
152
|
+
and coverage.projection_contract == projection_contract(endpoint)
|
|
153
|
+
)
|
|
154
|
+
|
|
145
155
|
async def delete_response(self, key: str) -> None:
|
|
146
156
|
"""Delete no response because response bytes are never retained."""
|
|
147
157
|
|
|
@@ -169,7 +169,7 @@ class RedisCacheBackend:
|
|
|
169
169
|
lock = client.lock(
|
|
170
170
|
self._key("lock", "catalog-commit"),
|
|
171
171
|
timeout=_MAX_CATALOG_COMMIT_TIMEOUT_SECONDS,
|
|
172
|
-
blocking_timeout=
|
|
172
|
+
blocking_timeout=None,
|
|
173
173
|
)
|
|
174
174
|
try:
|
|
175
175
|
async with lock:
|
|
@@ -253,6 +253,19 @@ class RedisCacheBackend:
|
|
|
253
253
|
)
|
|
254
254
|
return merged
|
|
255
255
|
|
|
256
|
+
async def has_current_projection(
|
|
257
|
+
self, *, endpoint: str, canonical_filters: str
|
|
258
|
+
) -> bool:
|
|
259
|
+
"""Return whether durable coverage uses the current contract."""
|
|
260
|
+
identity = _digest(f"{endpoint}:{canonical_filters}")
|
|
261
|
+
raw = await self._active_client().get(self._key("coverage", identity))
|
|
262
|
+
if raw is None:
|
|
263
|
+
return False
|
|
264
|
+
record = json_object(json.loads(_bytes(raw)))
|
|
265
|
+
return _json_required_text(
|
|
266
|
+
record, "projection_contract"
|
|
267
|
+
) == projection_contract(endpoint)
|
|
268
|
+
|
|
256
269
|
async def delete_response(self, key: str) -> None:
|
|
257
270
|
"""Delete one invalid response record without catalog pruning."""
|
|
258
271
|
await self._active_client().delete(self._response_key(key))
|
|
@@ -225,6 +225,21 @@ class SQLiteCacheBackend:
|
|
|
225
225
|
persist=False,
|
|
226
226
|
)
|
|
227
227
|
|
|
228
|
+
async def has_current_projection(
|
|
229
|
+
self, *, endpoint: str, canonical_filters: str
|
|
230
|
+
) -> bool:
|
|
231
|
+
"""Return whether durable coverage uses the current contract."""
|
|
232
|
+
async with self._operation_lock:
|
|
233
|
+
cursor = await self._active_connection().execute(
|
|
234
|
+
self._sql.render("select_projection_contract.sql"),
|
|
235
|
+
(endpoint, canonical_filters),
|
|
236
|
+
)
|
|
237
|
+
row = await cursor.fetchone()
|
|
238
|
+
await cursor.close()
|
|
239
|
+
return bool(
|
|
240
|
+
row is not None and _row_str(row, 0) == projection_contract(endpoint)
|
|
241
|
+
)
|
|
242
|
+
|
|
228
243
|
async def delete_response(self, key: str) -> None:
|
|
229
244
|
"""Delete one invalid or expired response record."""
|
|
230
245
|
async with self._operation_lock:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Define
|
|
1
|
+
"""Define actionable exceptions raised by :mod:`cfb_data`."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -7,29 +7,6 @@ class CFBDError(Exception):
|
|
|
7
7
|
"""Provide the root exception for all library-owned failures."""
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class _SanitizedCause(Exception):
|
|
11
|
-
"""Retain an exception category without retaining unsafe source data."""
|
|
12
|
-
|
|
13
|
-
category: str
|
|
14
|
-
|
|
15
|
-
def __init__(self, *, category: str) -> None:
|
|
16
|
-
"""Initialize a safe diagnostic cause.
|
|
17
|
-
|
|
18
|
-
:param category: Source exception type name without its values or state.
|
|
19
|
-
"""
|
|
20
|
-
self.category = category
|
|
21
|
-
super().__init__(category)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _sanitized_cause(source: BaseException) -> _SanitizedCause:
|
|
25
|
-
"""Return a diagnostic cause detached from a potentially unsafe exception.
|
|
26
|
-
|
|
27
|
-
:param source: Exception whose type identifies the failure category.
|
|
28
|
-
:return: New exception retaining only the source type name.
|
|
29
|
-
"""
|
|
30
|
-
return _SanitizedCause(category=type(source).__name__)
|
|
31
|
-
|
|
32
|
-
|
|
33
10
|
class CFBDConfigurationError(CFBDError):
|
|
34
11
|
"""Report invalid or missing client configuration."""
|
|
35
12
|
|