cfb-data 0.5.1__tar.gz → 0.7.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.5.1 → cfb_data-0.7.0}/PKG-INFO +26 -4
- {cfb_data-0.5.1 → cfb_data-0.7.0}/README.md +24 -2
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/__init__.py +12 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_executor.py +101 -7
- cfb_data-0.7.0/cfb_data/cfb_data/_observability.py +111 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_transport.py +166 -13
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_coordinator.py +494 -33
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_redis.py +62 -28
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/client.py +13 -1
- cfb_data-0.7.0/cfb_data/cfb_data/observability.py +632 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data.egg-info/PKG-INFO +26 -4
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data.egg-info/SOURCES.txt +2 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data.egg-info/requires.txt +1 -1
- {cfb_data-0.5.1 → cfb_data-0.7.0}/pyproject.toml +2 -2
- {cfb_data-0.5.1 → cfb_data-0.7.0}/LICENSE +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_catalog/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_catalog/merge.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_catalog/models.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_catalog/projection.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_catalog/sources.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_dataframes.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_parquet.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_reference_enums.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_request_rules.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_tabular.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/adjusted_metrics/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/adjusted_metrics/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/adjusted_metrics/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/adjusted_metrics/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/adjusted_metrics/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/adjusted_metrics/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/base/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/base/types.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/betting/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/betting/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/betting/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/betting/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/betting/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/betting/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_backend.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_catalog.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_catalog_codecs.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_identity_codecs.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_key.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_models.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_null.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_sqlite.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_sqlite_sql.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/config.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/policy.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/acquire_refresh_lease.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/begin_immediate.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/catalog_counts.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/cleanup_responses.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/delete_coverage_failure.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/delete_response.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/delete_team_aliases.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/enable_foreign_keys.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/enable_wal.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_athletes.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_conference_by_id.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_conference_by_name.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_fresh_coverage.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_game_by_id.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_games.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_team_by_id.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_team_by_name.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_venue_by_id.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_venue_by_name.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/get_response.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/get_response_size.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/get_schema_version.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/initialize_schema_version.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/release_refresh_lease.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/renew_refresh_lease.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/schema.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/select_catalog_observations.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/set_busy_timeout.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/set_synchronous_normal.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_athlete.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_athlete_team_season.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_catalog_observation.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_coach.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_coach_team_season.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_conference.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_conference_affiliation.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_coverage.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_coverage_failure.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_drive.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_game.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_play.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_playoff_matchup.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_recruit.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_response.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_team.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_team_alias.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_team_season.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_venue.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_vocabulary.sql +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/coaches/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/coaches/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/coaches/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/coaches/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/coaches/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/coaches/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/conferences/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/conferences/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/conferences/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/conferences/models/pydantic/identity.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/conferences/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/conferences/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/conferences/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/draft/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/draft/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/draft/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/draft/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/draft/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/draft/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/drives/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/drives/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/drives/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/drives/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/drives/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/drives/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/enums.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/errors.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/games/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/games/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/games/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/games/models/pydantic/identity.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/games/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/games/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/games/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/identities/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/identities/_normalization.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/identities/contracts.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/identities/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/info/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/info/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/info/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/info/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/info/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/info/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/metrics/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/metrics/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/metrics/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/metrics/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/metrics/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/metrics/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/players/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/players/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/players/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/players/models/pydantic/identity.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/players/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/players/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/players/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/playoffs/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/playoffs/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/playoffs/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/playoffs/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/playoffs/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/playoffs/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/plays/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/plays/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/plays/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/plays/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/plays/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/plays/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/py.typed +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/rankings/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/rankings/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/rankings/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/rankings/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/rankings/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/rankings/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/ratings/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/ratings/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/ratings/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/ratings/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/ratings/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/ratings/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/recruiting/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/recruiting/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/recruiting/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/recruiting/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/recruiting/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/recruiting/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/retry.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/stats/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/stats/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/stats/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/stats/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/stats/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/stats/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/teams/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/teams/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/teams/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/teams/models/pydantic/identity.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/teams/models/pydantic/requests.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/teams/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/teams/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/venues/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/venues/models/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/venues/models/pydantic/__init__.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/venues/models/pydantic/identity.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/venues/models/pydantic/responses.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/venues/resource.py +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data.egg-info/dependency_links.txt +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data.egg-info/top_level.txt +0 -0
- {cfb_data-0.5.1 → cfb_data-0.7.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cfb-data
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Async validated CollegeFootballData access for pandas and Polars
|
|
5
5
|
Author: Ryan Anderson
|
|
6
6
|
License-Expression: MIT
|
|
@@ -24,7 +24,7 @@ Requires-Dist: pyarrow<26,>=25
|
|
|
24
24
|
Provides-Extra: polars
|
|
25
25
|
Requires-Dist: polars<2,>=1.43.2; extra == "polars"
|
|
26
26
|
Provides-Extra: redis
|
|
27
|
-
Requires-Dist: redis<
|
|
27
|
+
Requires-Dist: redis<9,>=7.1; extra == "redis"
|
|
28
28
|
Provides-Extra: dev
|
|
29
29
|
Requires-Dist: build<2,>=1.5; extra == "dev"
|
|
30
30
|
Requires-Dist: furo>=2025.12.19; extra == "dev"
|
|
@@ -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.7.0 is a pre-alpha 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.
|
|
@@ -231,9 +231,28 @@ async with CFBDClient(cache=cache) as client:
|
|
|
231
231
|
games = await client.games.list(year=2025)
|
|
232
232
|
```
|
|
233
233
|
|
|
234
|
+
Add a bounded in-memory collector when you want to see whether calls reached
|
|
235
|
+
the API or were served from cache:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from cfb_data import CFBDClient, RetrievalStats, SQLiteCacheConfig
|
|
239
|
+
|
|
240
|
+
stats = RetrievalStats()
|
|
241
|
+
|
|
242
|
+
async with CFBDClient(cache=SQLiteCacheConfig(), observer=stats) as client:
|
|
243
|
+
await client.games.list(year=2025)
|
|
244
|
+
await client.games.list(year=2025)
|
|
245
|
+
|
|
246
|
+
snapshot = stats.snapshot()
|
|
247
|
+
print(snapshot.http_attempts) # actual client-side HTTP attempts
|
|
248
|
+
print(snapshot.fresh_cache_hits) # fresh initial cache hits
|
|
249
|
+
print(snapshot.fresh_hit_rate)
|
|
250
|
+
```
|
|
251
|
+
|
|
234
252
|
See [Cache responses and look up
|
|
235
253
|
identities](docs/guides/cache-and-identities.md) for use cases, the included
|
|
236
|
-
local Redis setup, cache modes, identity examples, and
|
|
254
|
+
local Redis setup, cache modes, observability, identity examples, and
|
|
255
|
+
troubleshooting.
|
|
237
256
|
|
|
238
257
|
## Troubleshooting
|
|
239
258
|
|
|
@@ -258,6 +277,9 @@ is organized in layers:
|
|
|
258
277
|
behavior.
|
|
259
278
|
- **Project internals:** architecture decisions and contributor-facing design.
|
|
260
279
|
|
|
280
|
+
The [product constitution](docs/product-constitution.md) is the concise north
|
|
281
|
+
star used to evaluate product and engineering decisions.
|
|
282
|
+
|
|
261
283
|
The project is pre-alpha, and the documentation describes the current version.
|
|
262
284
|
|
|
263
285
|
## Development
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# College Football Data Python Toolkit
|
|
2
2
|
|
|
3
|
-
`cfb-data` 0.
|
|
3
|
+
`cfb-data` 0.7.0 is a pre-alpha 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.
|
|
@@ -190,9 +190,28 @@ async with CFBDClient(cache=cache) as client:
|
|
|
190
190
|
games = await client.games.list(year=2025)
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
+
Add a bounded in-memory collector when you want to see whether calls reached
|
|
194
|
+
the API or were served from cache:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from cfb_data import CFBDClient, RetrievalStats, SQLiteCacheConfig
|
|
198
|
+
|
|
199
|
+
stats = RetrievalStats()
|
|
200
|
+
|
|
201
|
+
async with CFBDClient(cache=SQLiteCacheConfig(), observer=stats) as client:
|
|
202
|
+
await client.games.list(year=2025)
|
|
203
|
+
await client.games.list(year=2025)
|
|
204
|
+
|
|
205
|
+
snapshot = stats.snapshot()
|
|
206
|
+
print(snapshot.http_attempts) # actual client-side HTTP attempts
|
|
207
|
+
print(snapshot.fresh_cache_hits) # fresh initial cache hits
|
|
208
|
+
print(snapshot.fresh_hit_rate)
|
|
209
|
+
```
|
|
210
|
+
|
|
193
211
|
See [Cache responses and look up
|
|
194
212
|
identities](docs/guides/cache-and-identities.md) for use cases, the included
|
|
195
|
-
local Redis setup, cache modes, identity examples, and
|
|
213
|
+
local Redis setup, cache modes, observability, identity examples, and
|
|
214
|
+
troubleshooting.
|
|
196
215
|
|
|
197
216
|
## Troubleshooting
|
|
198
217
|
|
|
@@ -217,6 +236,9 @@ is organized in layers:
|
|
|
217
236
|
behavior.
|
|
218
237
|
- **Project internals:** architecture decisions and contributor-facing design.
|
|
219
238
|
|
|
239
|
+
The [product constitution](docs/product-constitution.md) is the concise north
|
|
240
|
+
star used to evaluate product and engineering decisions.
|
|
241
|
+
|
|
220
242
|
The project is pre-alpha, and the documentation describes the current version.
|
|
221
243
|
|
|
222
244
|
## Development
|
|
@@ -94,6 +94,13 @@ from .metrics.models.pydantic.requests import (
|
|
|
94
94
|
TeamSeasonPPARequest,
|
|
95
95
|
WinProbabilityRequest,
|
|
96
96
|
)
|
|
97
|
+
from .observability import (
|
|
98
|
+
EndpointRetrievalStats,
|
|
99
|
+
RetrievalEvent,
|
|
100
|
+
RetrievalObserver,
|
|
101
|
+
RetrievalStats,
|
|
102
|
+
RetrievalStatsSnapshot,
|
|
103
|
+
)
|
|
97
104
|
from .players.models.pydantic.identity import AthleteIdentity
|
|
98
105
|
from .players.models.pydantic.requests import (
|
|
99
106
|
PlayerSearchRequest,
|
|
@@ -210,6 +217,7 @@ __all__ = [
|
|
|
210
217
|
"DraftPicksRequest",
|
|
211
218
|
"DrivesRequest",
|
|
212
219
|
"EloRatingsRequest",
|
|
220
|
+
"EndpointRetrievalStats",
|
|
213
221
|
"ExpandedSRSRatingsRequest",
|
|
214
222
|
"FPIRatingsRequest",
|
|
215
223
|
"FreshnessMode",
|
|
@@ -249,6 +257,10 @@ __all__ = [
|
|
|
249
257
|
"RecruitingPlayersRequest",
|
|
250
258
|
"RecruitingTeamsRequest",
|
|
251
259
|
"RedisCacheConfig",
|
|
260
|
+
"RetrievalEvent",
|
|
261
|
+
"RetrievalObserver",
|
|
262
|
+
"RetrievalStats",
|
|
263
|
+
"RetrievalStatsSnapshot",
|
|
252
264
|
"RetryPolicy",
|
|
253
265
|
"RosterRequest",
|
|
254
266
|
"ReturningProductionRequest",
|
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import hashlib
|
|
6
7
|
import json
|
|
8
|
+
from collections.abc import Callable
|
|
7
9
|
from typing import TypeVar
|
|
8
10
|
|
|
9
11
|
from pydantic import BaseModel, TypeAdapter, ValidationError
|
|
10
12
|
|
|
13
|
+
from cfb_data._observability import (
|
|
14
|
+
_EventDispatcher,
|
|
15
|
+
_failure_category,
|
|
16
|
+
_OperationContext,
|
|
17
|
+
)
|
|
11
18
|
from cfb_data._transport import _HTTPTransport
|
|
12
19
|
from cfb_data.base.types import (
|
|
13
20
|
json_list,
|
|
@@ -21,6 +28,11 @@ from cfb_data.errors import (
|
|
|
21
28
|
CFBDResponseValidationError,
|
|
22
29
|
_sanitized_cause,
|
|
23
30
|
)
|
|
31
|
+
from cfb_data.observability import (
|
|
32
|
+
RetrievalFinished,
|
|
33
|
+
RetrievalOutcome,
|
|
34
|
+
RetrievalStarted,
|
|
35
|
+
)
|
|
24
36
|
|
|
25
37
|
_ModelT = TypeVar("_ModelT", bound=BaseModel)
|
|
26
38
|
_ValueT = TypeVar("_ValueT")
|
|
@@ -30,14 +42,21 @@ class _EndpointExecutor:
|
|
|
30
42
|
"""Return validated models without depending on DataFrame presentation."""
|
|
31
43
|
|
|
32
44
|
def __init__(
|
|
33
|
-
self,
|
|
45
|
+
self,
|
|
46
|
+
transport: _HTTPTransport,
|
|
47
|
+
cache_coordinator: CacheCoordinator,
|
|
48
|
+
*,
|
|
49
|
+
event_dispatcher: _EventDispatcher,
|
|
34
50
|
) -> None:
|
|
35
51
|
"""Bind endpoint execution to one owned transport.
|
|
36
52
|
|
|
37
53
|
:param transport: Context-managed transport used by all endpoint calls.
|
|
54
|
+
:param cache_coordinator: Cache policy and refresh coordinator.
|
|
55
|
+
:param event_dispatcher: Optional-observer event dispatcher.
|
|
38
56
|
"""
|
|
39
57
|
self._transport = transport
|
|
40
58
|
self._cache_coordinator = cache_coordinator
|
|
59
|
+
self._event_dispatcher = event_dispatcher
|
|
41
60
|
|
|
42
61
|
async def fetch_many(
|
|
43
62
|
self,
|
|
@@ -54,9 +73,9 @@ class _EndpointExecutor:
|
|
|
54
73
|
:return: Validated models in upstream row order.
|
|
55
74
|
:raises CFBDResponseValidationError: If response shape or values fail.
|
|
56
75
|
"""
|
|
57
|
-
return await self.
|
|
76
|
+
return await self._execute(
|
|
58
77
|
endpoint=endpoint,
|
|
59
|
-
|
|
78
|
+
request=request,
|
|
60
79
|
response_contract=_response_contract(response_adapter),
|
|
61
80
|
validate=lambda raw: _validate_many(endpoint, raw, response_adapter),
|
|
62
81
|
)
|
|
@@ -76,9 +95,9 @@ class _EndpointExecutor:
|
|
|
76
95
|
:return: Validated response model.
|
|
77
96
|
:raises CFBDResponseValidationError: If response shape or values fail.
|
|
78
97
|
"""
|
|
79
|
-
return await self.
|
|
98
|
+
return await self._execute(
|
|
80
99
|
endpoint=endpoint,
|
|
81
|
-
|
|
100
|
+
request=request,
|
|
82
101
|
response_contract=_response_contract(response_adapter),
|
|
83
102
|
validate=lambda raw: _validate_one(endpoint, raw, response_adapter),
|
|
84
103
|
)
|
|
@@ -98,13 +117,88 @@ class _EndpointExecutor:
|
|
|
98
117
|
:return: Validated values in upstream order.
|
|
99
118
|
:raises CFBDResponseValidationError: If response shape or values fail.
|
|
100
119
|
"""
|
|
101
|
-
return await self.
|
|
120
|
+
return await self._execute(
|
|
102
121
|
endpoint=endpoint,
|
|
103
|
-
|
|
122
|
+
request=request,
|
|
104
123
|
response_contract=_response_contract(response_adapter),
|
|
105
124
|
validate=lambda raw: _validate_values(endpoint, raw, response_adapter),
|
|
106
125
|
)
|
|
107
126
|
|
|
127
|
+
async def _execute[ValueT](
|
|
128
|
+
self,
|
|
129
|
+
*,
|
|
130
|
+
endpoint: str,
|
|
131
|
+
request: BaseModel,
|
|
132
|
+
response_contract: str,
|
|
133
|
+
validate: Callable[[object], ValueT],
|
|
134
|
+
) -> ValueT:
|
|
135
|
+
"""Execute and observe one serialized, validated endpoint retrieval.
|
|
136
|
+
|
|
137
|
+
:param endpoint: Fixed endpoint path.
|
|
138
|
+
:param request: Validated endpoint request model.
|
|
139
|
+
:param response_contract: Stable response-schema fingerprint.
|
|
140
|
+
:param validate: Callable that validates the decoded response.
|
|
141
|
+
:return: Validated response value.
|
|
142
|
+
"""
|
|
143
|
+
parameters = _serialize_request(endpoint, request)
|
|
144
|
+
context = self._event_dispatcher.new_operation(endpoint)
|
|
145
|
+
if context is not None:
|
|
146
|
+
self._event_dispatcher.emit(
|
|
147
|
+
RetrievalStarted(
|
|
148
|
+
client_id=context.client_id,
|
|
149
|
+
operation_id=context.operation_id,
|
|
150
|
+
endpoint=endpoint,
|
|
151
|
+
parameter_names=tuple(sorted(parameters)),
|
|
152
|
+
cache_mode=self._cache_coordinator.current_mode,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
try:
|
|
156
|
+
value = await self._cache_coordinator.execute(
|
|
157
|
+
endpoint=endpoint,
|
|
158
|
+
parameters=parameters,
|
|
159
|
+
response_contract=response_contract,
|
|
160
|
+
validate=validate,
|
|
161
|
+
context=context,
|
|
162
|
+
)
|
|
163
|
+
except asyncio.CancelledError as exc:
|
|
164
|
+
self._emit_finished(context, RetrievalOutcome.cancelled, None, exc)
|
|
165
|
+
raise
|
|
166
|
+
except Exception as exc:
|
|
167
|
+
self._emit_finished(context, RetrievalOutcome.error, None, exc)
|
|
168
|
+
raise
|
|
169
|
+
self._emit_finished(context, RetrievalOutcome.success, _row_count(value), None)
|
|
170
|
+
return value
|
|
171
|
+
|
|
172
|
+
def _emit_finished(
|
|
173
|
+
self,
|
|
174
|
+
context: _OperationContext | None,
|
|
175
|
+
outcome: RetrievalOutcome,
|
|
176
|
+
row_count: int | None,
|
|
177
|
+
error: BaseException | None,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Emit one terminal retrieval event when observation is enabled."""
|
|
180
|
+
if context is None or not self._event_dispatcher.enabled:
|
|
181
|
+
return
|
|
182
|
+
self._event_dispatcher.emit(
|
|
183
|
+
RetrievalFinished(
|
|
184
|
+
client_id=context.client_id,
|
|
185
|
+
operation_id=context.operation_id,
|
|
186
|
+
endpoint=context.endpoint,
|
|
187
|
+
outcome=outcome,
|
|
188
|
+
source=context.source,
|
|
189
|
+
row_count=row_count,
|
|
190
|
+
duration_seconds=self._event_dispatcher.elapsed(context.started_at),
|
|
191
|
+
failure_category=(
|
|
192
|
+
_failure_category(error) if error is not None else None
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _row_count(value: object) -> int:
|
|
199
|
+
"""Return the validated logical row count for one endpoint result."""
|
|
200
|
+
return len(value) if isinstance(value, list) else 1
|
|
201
|
+
|
|
108
202
|
|
|
109
203
|
def _serialize_request(
|
|
110
204
|
endpoint: str, request: BaseModel
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Dispatch public observability events without affecting retrieval behavior."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from cfb_data.observability import (
|
|
13
|
+
RetrievalEvent,
|
|
14
|
+
RetrievalObserver,
|
|
15
|
+
RetrievalSource,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_LOGGER = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(slots=True)
|
|
22
|
+
class _OperationContext:
|
|
23
|
+
"""Carry safe correlation state across cache and transport boundaries."""
|
|
24
|
+
|
|
25
|
+
client_id: str
|
|
26
|
+
operation_id: str
|
|
27
|
+
endpoint: str
|
|
28
|
+
started_at: float
|
|
29
|
+
source: RetrievalSource = RetrievalSource.unknown
|
|
30
|
+
refresh_id: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _EventDispatcher:
|
|
34
|
+
"""Deliver ordered events and isolate failures in an optional observer."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
observer: RetrievalObserver | None,
|
|
39
|
+
*,
|
|
40
|
+
monotonic: Callable[[], float] = time.monotonic,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Initialize one client-scoped dispatcher.
|
|
43
|
+
|
|
44
|
+
:param observer: Optional synchronous event observer.
|
|
45
|
+
:param monotonic: Monotonic clock used for bounded durations.
|
|
46
|
+
"""
|
|
47
|
+
self._observer = observer
|
|
48
|
+
self._monotonic = monotonic
|
|
49
|
+
self._client_id = uuid.uuid4().hex
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def enabled(self) -> bool:
|
|
53
|
+
"""Return whether a healthy observer is currently configured."""
|
|
54
|
+
return self._observer is not None
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def client_id(self) -> str:
|
|
58
|
+
"""Return this dispatcher's non-secret client correlation identifier."""
|
|
59
|
+
return self._client_id
|
|
60
|
+
|
|
61
|
+
def new_operation(self, endpoint: str) -> _OperationContext | None:
|
|
62
|
+
"""Return a new safe operation context when observation is enabled."""
|
|
63
|
+
if self._observer is None:
|
|
64
|
+
return None
|
|
65
|
+
return _OperationContext(
|
|
66
|
+
client_id=self._client_id,
|
|
67
|
+
operation_id=uuid.uuid4().hex,
|
|
68
|
+
endpoint=endpoint,
|
|
69
|
+
started_at=self._monotonic(),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def new_refresh_id(self) -> str:
|
|
73
|
+
"""Return a non-secret identifier for one shared refresh."""
|
|
74
|
+
return uuid.uuid4().hex
|
|
75
|
+
|
|
76
|
+
def now(self) -> float:
|
|
77
|
+
"""Return the current monotonic clock value."""
|
|
78
|
+
return self._monotonic()
|
|
79
|
+
|
|
80
|
+
def elapsed(self, started_at: float) -> float:
|
|
81
|
+
"""Return a finite non-negative elapsed duration."""
|
|
82
|
+
return max(0.0, self._monotonic() - started_at)
|
|
83
|
+
|
|
84
|
+
def emit(self, event: RetrievalEvent) -> None:
|
|
85
|
+
"""Deliver one event or disable a failing observer without propagating."""
|
|
86
|
+
observer = self._observer
|
|
87
|
+
if observer is None:
|
|
88
|
+
return
|
|
89
|
+
try:
|
|
90
|
+
observer(event)
|
|
91
|
+
except (Exception, asyncio.CancelledError) as exc:
|
|
92
|
+
self._observer = None
|
|
93
|
+
_LOGGER.warning(
|
|
94
|
+
"CFBD retrieval observer disabled event=%s category=%s",
|
|
95
|
+
type(event).__name__,
|
|
96
|
+
type(exc).__name__,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _failure_category(error: BaseException) -> str:
|
|
101
|
+
"""Return a bounded safe category without retaining an exception object."""
|
|
102
|
+
category = getattr(error, "category", None)
|
|
103
|
+
if isinstance(category, str) and category:
|
|
104
|
+
return category[:64]
|
|
105
|
+
status = getattr(error, "status", None)
|
|
106
|
+
if isinstance(status, int):
|
|
107
|
+
return f"http_{status}"
|
|
108
|
+
return type(error).__name__[:64]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
__all__ = ["_EventDispatcher", "_OperationContext", "_failure_category"]
|
|
@@ -17,6 +17,11 @@ from urllib.parse import urlsplit
|
|
|
17
17
|
|
|
18
18
|
import aiohttp
|
|
19
19
|
|
|
20
|
+
from cfb_data._observability import (
|
|
21
|
+
_EventDispatcher,
|
|
22
|
+
_failure_category,
|
|
23
|
+
_OperationContext,
|
|
24
|
+
)
|
|
20
25
|
from cfb_data.base.types import QueryParameters
|
|
21
26
|
from cfb_data.cache._models import MAX_RESPONSE_BODY_BYTES
|
|
22
27
|
from cfb_data.errors import (
|
|
@@ -35,6 +40,11 @@ from cfb_data.errors import (
|
|
|
35
40
|
_sanitized_cause,
|
|
36
41
|
_SanitizedCause,
|
|
37
42
|
)
|
|
43
|
+
from cfb_data.observability import (
|
|
44
|
+
HTTPAttemptFinished,
|
|
45
|
+
HTTPAttemptOutcome,
|
|
46
|
+
HTTPAttemptStarted,
|
|
47
|
+
)
|
|
38
48
|
from cfb_data.retry import RetryPolicy
|
|
39
49
|
|
|
40
50
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -91,6 +101,7 @@ class _HTTPTransport:
|
|
|
91
101
|
base_url: str,
|
|
92
102
|
timeout_seconds: float,
|
|
93
103
|
retry_policy: RetryPolicy,
|
|
104
|
+
event_dispatcher: _EventDispatcher,
|
|
94
105
|
sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
|
|
95
106
|
random_source: Callable[[], float] = random.random,
|
|
96
107
|
utc_now: Callable[[], datetime] | None = None,
|
|
@@ -101,6 +112,7 @@ class _HTTPTransport:
|
|
|
101
112
|
:param base_url: Validated API origin and optional base path.
|
|
102
113
|
:param timeout_seconds: Finite timeout applied to each attempt.
|
|
103
114
|
:param retry_policy: Bounded GET retry configuration.
|
|
115
|
+
:param event_dispatcher: Client-scoped retrieval event dispatcher.
|
|
104
116
|
:param sleep: Awaitable delay function used by retries.
|
|
105
117
|
:param random_source: Uniform random source in the inclusive range 0..1.
|
|
106
118
|
:param utc_now: Clock used to interpret HTTP-date ``Retry-After`` values.
|
|
@@ -110,6 +122,7 @@ class _HTTPTransport:
|
|
|
110
122
|
self._timeout_seconds = timeout_seconds
|
|
111
123
|
self._timeout = aiohttp.ClientTimeout(total=timeout_seconds)
|
|
112
124
|
self._retry_policy = retry_policy
|
|
125
|
+
self._event_dispatcher = event_dispatcher
|
|
113
126
|
self._sleep = sleep
|
|
114
127
|
self._random_source = random_source
|
|
115
128
|
self._utc_now = utc_now or (lambda: datetime.now(UTC))
|
|
@@ -181,12 +194,16 @@ class _HTTPTransport:
|
|
|
181
194
|
params: QueryParameters,
|
|
182
195
|
*,
|
|
183
196
|
conditional_headers: Mapping[str, str] | None = None,
|
|
197
|
+
context: _OperationContext | None = None,
|
|
198
|
+
refresh_id: str | None = None,
|
|
184
199
|
) -> _ResponseEnvelope:
|
|
185
200
|
"""Return one bounded response envelope using safe-GET retries.
|
|
186
201
|
|
|
187
202
|
:param endpoint: Fixed endpoint path without query parameters.
|
|
188
203
|
:param params: Validated scalar query parameters.
|
|
189
204
|
:param conditional_headers: Safe validators for retained records.
|
|
205
|
+
:param context: Optional safe endpoint-operation correlation context.
|
|
206
|
+
:param refresh_id: Optional shared-refresh correlation identifier.
|
|
190
207
|
:return: Decoded response and cache-relevant metadata.
|
|
191
208
|
:raises CFBDClientStateError: If used outside its active context.
|
|
192
209
|
:raises CFBDHTTPError: If the API returns an unsuccessful status.
|
|
@@ -197,6 +214,12 @@ class _HTTPTransport:
|
|
|
197
214
|
url = f"{self._base_url}{endpoint}"
|
|
198
215
|
|
|
199
216
|
for attempt in range(1, self._retry_policy.max_attempts + 1):
|
|
217
|
+
attempt_started_at = self._start_attempt(
|
|
218
|
+
context=context,
|
|
219
|
+
refresh_id=refresh_id,
|
|
220
|
+
endpoint=endpoint,
|
|
221
|
+
attempt=attempt,
|
|
222
|
+
)
|
|
200
223
|
failure: _TransportFailure | None = None
|
|
201
224
|
try:
|
|
202
225
|
result = await self._request_once(
|
|
@@ -208,6 +231,42 @@ class _HTTPTransport:
|
|
|
208
231
|
conditional_headers=conditional_headers,
|
|
209
232
|
)
|
|
210
233
|
except asyncio.CancelledError:
|
|
234
|
+
self._finish_attempt(
|
|
235
|
+
context=context,
|
|
236
|
+
refresh_id=refresh_id,
|
|
237
|
+
endpoint=endpoint,
|
|
238
|
+
attempt=attempt,
|
|
239
|
+
started_at=attempt_started_at,
|
|
240
|
+
outcome=HTTPAttemptOutcome.cancelled,
|
|
241
|
+
terminal=True,
|
|
242
|
+
failure_category="cancelled",
|
|
243
|
+
)
|
|
244
|
+
raise
|
|
245
|
+
except CFBDHTTPError as exc:
|
|
246
|
+
self._finish_attempt(
|
|
247
|
+
context=context,
|
|
248
|
+
refresh_id=refresh_id,
|
|
249
|
+
endpoint=endpoint,
|
|
250
|
+
attempt=attempt,
|
|
251
|
+
started_at=attempt_started_at,
|
|
252
|
+
outcome=HTTPAttemptOutcome.http_error,
|
|
253
|
+
terminal=True,
|
|
254
|
+
status_class=exc.status // 100,
|
|
255
|
+
failure_category=f"http_{exc.status}",
|
|
256
|
+
)
|
|
257
|
+
raise
|
|
258
|
+
except (CFBDNoContentError, CFBDResponseDecodeError) as exc:
|
|
259
|
+
self._finish_attempt(
|
|
260
|
+
context=context,
|
|
261
|
+
refresh_id=refresh_id,
|
|
262
|
+
endpoint=endpoint,
|
|
263
|
+
attempt=attempt,
|
|
264
|
+
started_at=attempt_started_at,
|
|
265
|
+
outcome=HTTPAttemptOutcome.response_error,
|
|
266
|
+
terminal=True,
|
|
267
|
+
status_class=(2 if isinstance(exc, CFBDNoContentError) else None),
|
|
268
|
+
failure_category=_failure_category(exc),
|
|
269
|
+
)
|
|
211
270
|
raise
|
|
212
271
|
except aiohttp.InvalidURL as exc:
|
|
213
272
|
failure = _TransportFailure(
|
|
@@ -270,17 +329,66 @@ class _HTTPTransport:
|
|
|
270
329
|
|
|
271
330
|
if failure is not None:
|
|
272
331
|
if failure.retryable and attempt < self._retry_policy.max_attempts:
|
|
273
|
-
|
|
332
|
+
delay = self._backoff_delay(attempt)
|
|
333
|
+
self._finish_attempt(
|
|
334
|
+
context=context,
|
|
335
|
+
refresh_id=refresh_id,
|
|
274
336
|
endpoint=endpoint,
|
|
275
337
|
attempt=attempt,
|
|
276
|
-
|
|
338
|
+
started_at=attempt_started_at,
|
|
339
|
+
outcome=HTTPAttemptOutcome.retry,
|
|
340
|
+
terminal=False,
|
|
341
|
+
retry_delay_seconds=delay,
|
|
342
|
+
failure_category=failure.category,
|
|
343
|
+
)
|
|
344
|
+
_LOGGER.debug(
|
|
345
|
+
"Retrying CFBD GET endpoint=%s category=%s "
|
|
346
|
+
"attempt=%d delay=%.3f",
|
|
347
|
+
endpoint,
|
|
348
|
+
failure.category,
|
|
349
|
+
attempt,
|
|
350
|
+
delay,
|
|
277
351
|
)
|
|
352
|
+
await self._sleep(delay)
|
|
278
353
|
continue
|
|
354
|
+
self._finish_attempt(
|
|
355
|
+
context=context,
|
|
356
|
+
refresh_id=refresh_id,
|
|
357
|
+
endpoint=endpoint,
|
|
358
|
+
attempt=attempt,
|
|
359
|
+
started_at=attempt_started_at,
|
|
360
|
+
outcome=HTTPAttemptOutcome.transport_error,
|
|
361
|
+
terminal=True,
|
|
362
|
+
failure_category=failure.category,
|
|
363
|
+
)
|
|
279
364
|
raise failure.error from failure.cause
|
|
280
365
|
|
|
281
366
|
if not isinstance(result, _RetryDecision):
|
|
367
|
+
self._finish_attempt(
|
|
368
|
+
context=context,
|
|
369
|
+
refresh_id=refresh_id,
|
|
370
|
+
endpoint=endpoint,
|
|
371
|
+
attempt=attempt,
|
|
372
|
+
started_at=attempt_started_at,
|
|
373
|
+
outcome=HTTPAttemptOutcome.success,
|
|
374
|
+
terminal=True,
|
|
375
|
+
status_class=result.status // 100,
|
|
376
|
+
response_bytes=len(result.raw_body),
|
|
377
|
+
)
|
|
282
378
|
return result
|
|
283
379
|
|
|
380
|
+
self._finish_attempt(
|
|
381
|
+
context=context,
|
|
382
|
+
refresh_id=refresh_id,
|
|
383
|
+
endpoint=endpoint,
|
|
384
|
+
attempt=attempt,
|
|
385
|
+
started_at=attempt_started_at,
|
|
386
|
+
outcome=HTTPAttemptOutcome.retry,
|
|
387
|
+
terminal=False,
|
|
388
|
+
status_class=result.error.status // 100,
|
|
389
|
+
retry_delay_seconds=result.delay_seconds,
|
|
390
|
+
failure_category=result.category,
|
|
391
|
+
)
|
|
284
392
|
_LOGGER.debug(
|
|
285
393
|
"Retrying CFBD GET endpoint=%s category=%s attempt=%d delay=%.3f",
|
|
286
394
|
endpoint,
|
|
@@ -382,23 +490,68 @@ class _HTTPTransport:
|
|
|
382
490
|
attempts=attempt,
|
|
383
491
|
) from safe_cause
|
|
384
492
|
|
|
385
|
-
|
|
493
|
+
def _start_attempt(
|
|
386
494
|
self,
|
|
387
495
|
*,
|
|
496
|
+
context: _OperationContext | None,
|
|
497
|
+
refresh_id: str | None,
|
|
388
498
|
endpoint: str,
|
|
389
499
|
attempt: int,
|
|
390
|
-
|
|
500
|
+
) -> float | None:
|
|
501
|
+
"""Emit one attempt start and return its monotonic start time."""
|
|
502
|
+
if context is None or refresh_id is None or not self._event_dispatcher.enabled:
|
|
503
|
+
return None
|
|
504
|
+
started_at = self._event_dispatcher.now()
|
|
505
|
+
self._event_dispatcher.emit(
|
|
506
|
+
HTTPAttemptStarted(
|
|
507
|
+
client_id=context.client_id,
|
|
508
|
+
operation_id=context.operation_id,
|
|
509
|
+
refresh_id=refresh_id,
|
|
510
|
+
endpoint=endpoint,
|
|
511
|
+
attempt_number=attempt,
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
return started_at
|
|
515
|
+
|
|
516
|
+
def _finish_attempt(
|
|
517
|
+
self,
|
|
518
|
+
*,
|
|
519
|
+
context: _OperationContext | None,
|
|
520
|
+
refresh_id: str | None,
|
|
521
|
+
endpoint: str,
|
|
522
|
+
attempt: int,
|
|
523
|
+
started_at: float | None,
|
|
524
|
+
outcome: HTTPAttemptOutcome,
|
|
525
|
+
terminal: bool,
|
|
526
|
+
status_class: int | None = None,
|
|
527
|
+
response_bytes: int = 0,
|
|
528
|
+
retry_delay_seconds: float | None = None,
|
|
529
|
+
failure_category: str | None = None,
|
|
391
530
|
) -> None:
|
|
392
|
-
"""
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
531
|
+
"""Emit one bounded terminal event for a started HTTP attempt."""
|
|
532
|
+
if (
|
|
533
|
+
context is None
|
|
534
|
+
or refresh_id is None
|
|
535
|
+
or started_at is None
|
|
536
|
+
or not self._event_dispatcher.enabled
|
|
537
|
+
):
|
|
538
|
+
return
|
|
539
|
+
self._event_dispatcher.emit(
|
|
540
|
+
HTTPAttemptFinished(
|
|
541
|
+
client_id=context.client_id,
|
|
542
|
+
operation_id=context.operation_id,
|
|
543
|
+
refresh_id=refresh_id,
|
|
544
|
+
endpoint=endpoint,
|
|
545
|
+
attempt_number=attempt,
|
|
546
|
+
outcome=outcome,
|
|
547
|
+
terminal=terminal,
|
|
548
|
+
status_class=status_class,
|
|
549
|
+
duration_seconds=self._event_dispatcher.elapsed(started_at),
|
|
550
|
+
response_bytes=response_bytes,
|
|
551
|
+
retry_delay_seconds=retry_delay_seconds,
|
|
552
|
+
failure_category=failure_category,
|
|
553
|
+
)
|
|
400
554
|
)
|
|
401
|
-
await self._sleep(delay)
|
|
402
555
|
|
|
403
556
|
def _backoff_delay(self, attempt: int) -> float:
|
|
404
557
|
"""Return capped exponential full-jitter backoff after an attempt."""
|