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.
Files changed (213) hide show
  1. {cfb_data-0.7.0 → cfb_data-0.8.0}/PKG-INFO +7 -4
  2. {cfb_data-0.7.0 → cfb_data-0.8.0}/README.md +5 -2
  3. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_dataframes.py +16 -21
  4. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_executor.py +7 -12
  5. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_parquet.py +13 -11
  6. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_requests.py +2 -3
  7. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_transport.py +38 -16
  8. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_backend.py +11 -0
  9. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_coordinator.py +94 -4
  10. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_null.py +10 -0
  11. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_redis.py +14 -1
  12. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_sqlite.py +15 -0
  13. cfb_data-0.8.0/cfb_data/cfb_data/cache/sql/select_projection_contract.sql +3 -0
  14. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/errors.py +1 -24
  15. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data.egg-info/PKG-INFO +7 -4
  16. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data.egg-info/SOURCES.txt +1 -0
  17. {cfb_data-0.7.0 → cfb_data-0.8.0}/pyproject.toml +2 -2
  18. {cfb_data-0.7.0 → cfb_data-0.8.0}/LICENSE +0 -0
  19. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/__init__.py +0 -0
  20. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_catalog/__init__.py +0 -0
  21. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_catalog/merge.py +0 -0
  22. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_catalog/models.py +0 -0
  23. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_catalog/projection.py +0 -0
  24. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_catalog/sources.py +0 -0
  25. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_observability.py +0 -0
  26. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_reference_enums.py +0 -0
  27. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_request_rules.py +0 -0
  28. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/_tabular.py +0 -0
  29. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/adjusted_metrics/__init__.py +0 -0
  30. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/adjusted_metrics/models/__init__.py +0 -0
  31. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/adjusted_metrics/models/pydantic/__init__.py +0 -0
  32. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/adjusted_metrics/models/pydantic/requests.py +0 -0
  33. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/adjusted_metrics/models/pydantic/responses.py +0 -0
  34. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/adjusted_metrics/resource.py +0 -0
  35. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/base/__init__.py +0 -0
  36. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/base/types.py +0 -0
  37. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/betting/__init__.py +0 -0
  38. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/betting/models/__init__.py +0 -0
  39. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/betting/models/pydantic/__init__.py +0 -0
  40. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/betting/models/pydantic/requests.py +0 -0
  41. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/betting/models/pydantic/responses.py +0 -0
  42. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/betting/resource.py +0 -0
  43. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/__init__.py +0 -0
  44. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_catalog.py +0 -0
  45. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_catalog_codecs.py +0 -0
  46. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_identity_codecs.py +0 -0
  47. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_key.py +0 -0
  48. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_models.py +0 -0
  49. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/_sqlite_sql.py +0 -0
  50. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/config.py +0 -0
  51. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/policy.py +0 -0
  52. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/acquire_refresh_lease.sql +0 -0
  53. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/begin_immediate.sql +0 -0
  54. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/catalog_counts.sql +0 -0
  55. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/cleanup_responses.sql +0 -0
  56. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/delete_coverage_failure.sql +0 -0
  57. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/delete_response.sql +0 -0
  58. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/delete_team_aliases.sql +0 -0
  59. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/enable_foreign_keys.sql +0 -0
  60. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/enable_wal.sql +0 -0
  61. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_athletes.sql +0 -0
  62. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_conference_by_id.sql +0 -0
  63. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_conference_by_name.sql +0 -0
  64. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_fresh_coverage.sql +0 -0
  65. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_game_by_id.sql +0 -0
  66. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_games.sql +0 -0
  67. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_team_by_id.sql +0 -0
  68. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_team_by_name.sql +0 -0
  69. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_venue_by_id.sql +0 -0
  70. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/find_venue_by_name.sql +0 -0
  71. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/get_response.sql +0 -0
  72. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/get_response_size.sql +0 -0
  73. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/get_schema_version.sql +0 -0
  74. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/initialize_schema_version.sql +0 -0
  75. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/release_refresh_lease.sql +0 -0
  76. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/renew_refresh_lease.sql +0 -0
  77. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/schema.sql +0 -0
  78. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/select_catalog_observations.sql +0 -0
  79. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/set_busy_timeout.sql +0 -0
  80. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/set_synchronous_normal.sql +0 -0
  81. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_athlete.sql +0 -0
  82. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_athlete_team_season.sql +0 -0
  83. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_catalog_observation.sql +0 -0
  84. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_coach.sql +0 -0
  85. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_coach_team_season.sql +0 -0
  86. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_conference.sql +0 -0
  87. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_conference_affiliation.sql +0 -0
  88. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_coverage.sql +0 -0
  89. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_coverage_failure.sql +0 -0
  90. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_drive.sql +0 -0
  91. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_game.sql +0 -0
  92. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_play.sql +0 -0
  93. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_playoff_matchup.sql +0 -0
  94. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_recruit.sql +0 -0
  95. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_response.sql +0 -0
  96. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_team.sql +0 -0
  97. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_team_alias.sql +0 -0
  98. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_team_season.sql +0 -0
  99. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_venue.sql +0 -0
  100. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/cache/sql/upsert_vocabulary.sql +0 -0
  101. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/client.py +0 -0
  102. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/coaches/__init__.py +0 -0
  103. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/coaches/models/__init__.py +0 -0
  104. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/coaches/models/pydantic/__init__.py +0 -0
  105. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/coaches/models/pydantic/requests.py +0 -0
  106. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/coaches/models/pydantic/responses.py +0 -0
  107. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/coaches/resource.py +0 -0
  108. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/conferences/__init__.py +0 -0
  109. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/conferences/models/__init__.py +0 -0
  110. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/conferences/models/pydantic/__init__.py +0 -0
  111. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/conferences/models/pydantic/identity.py +0 -0
  112. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/conferences/models/pydantic/requests.py +0 -0
  113. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/conferences/models/pydantic/responses.py +0 -0
  114. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/conferences/resource.py +0 -0
  115. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/draft/__init__.py +0 -0
  116. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/draft/models/__init__.py +0 -0
  117. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/draft/models/pydantic/__init__.py +0 -0
  118. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/draft/models/pydantic/requests.py +0 -0
  119. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/draft/models/pydantic/responses.py +0 -0
  120. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/draft/resource.py +0 -0
  121. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/drives/__init__.py +0 -0
  122. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/drives/models/__init__.py +0 -0
  123. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/drives/models/pydantic/__init__.py +0 -0
  124. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/drives/models/pydantic/requests.py +0 -0
  125. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/drives/models/pydantic/responses.py +0 -0
  126. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/drives/resource.py +0 -0
  127. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/enums.py +0 -0
  128. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/games/__init__.py +0 -0
  129. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/games/models/__init__.py +0 -0
  130. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/games/models/pydantic/__init__.py +0 -0
  131. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/games/models/pydantic/identity.py +0 -0
  132. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/games/models/pydantic/requests.py +0 -0
  133. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/games/models/pydantic/responses.py +0 -0
  134. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/games/resource.py +0 -0
  135. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/identities/__init__.py +0 -0
  136. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/identities/_normalization.py +0 -0
  137. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/identities/contracts.py +0 -0
  138. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/identities/resource.py +0 -0
  139. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/info/__init__.py +0 -0
  140. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/info/models/__init__.py +0 -0
  141. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/info/models/pydantic/__init__.py +0 -0
  142. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/info/models/pydantic/requests.py +0 -0
  143. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/info/models/pydantic/responses.py +0 -0
  144. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/info/resource.py +0 -0
  145. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/metrics/__init__.py +0 -0
  146. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/metrics/models/__init__.py +0 -0
  147. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/metrics/models/pydantic/__init__.py +0 -0
  148. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/metrics/models/pydantic/requests.py +0 -0
  149. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/metrics/models/pydantic/responses.py +0 -0
  150. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/metrics/resource.py +0 -0
  151. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/observability.py +0 -0
  152. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/players/__init__.py +0 -0
  153. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/players/models/__init__.py +0 -0
  154. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/players/models/pydantic/__init__.py +0 -0
  155. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/players/models/pydantic/identity.py +0 -0
  156. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/players/models/pydantic/requests.py +0 -0
  157. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/players/models/pydantic/responses.py +0 -0
  158. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/players/resource.py +0 -0
  159. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/playoffs/__init__.py +0 -0
  160. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/playoffs/models/__init__.py +0 -0
  161. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/playoffs/models/pydantic/__init__.py +0 -0
  162. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/playoffs/models/pydantic/requests.py +0 -0
  163. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/playoffs/models/pydantic/responses.py +0 -0
  164. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/playoffs/resource.py +0 -0
  165. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/plays/__init__.py +0 -0
  166. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/plays/models/__init__.py +0 -0
  167. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/plays/models/pydantic/__init__.py +0 -0
  168. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/plays/models/pydantic/requests.py +0 -0
  169. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/plays/models/pydantic/responses.py +0 -0
  170. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/plays/resource.py +0 -0
  171. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/py.typed +0 -0
  172. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/rankings/__init__.py +0 -0
  173. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/rankings/models/__init__.py +0 -0
  174. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/rankings/models/pydantic/__init__.py +0 -0
  175. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/rankings/models/pydantic/requests.py +0 -0
  176. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/rankings/models/pydantic/responses.py +0 -0
  177. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/rankings/resource.py +0 -0
  178. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/ratings/__init__.py +0 -0
  179. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/ratings/models/__init__.py +0 -0
  180. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/ratings/models/pydantic/__init__.py +0 -0
  181. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/ratings/models/pydantic/requests.py +0 -0
  182. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/ratings/models/pydantic/responses.py +0 -0
  183. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/ratings/resource.py +0 -0
  184. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/recruiting/__init__.py +0 -0
  185. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/recruiting/models/__init__.py +0 -0
  186. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/recruiting/models/pydantic/__init__.py +0 -0
  187. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/recruiting/models/pydantic/requests.py +0 -0
  188. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/recruiting/models/pydantic/responses.py +0 -0
  189. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/recruiting/resource.py +0 -0
  190. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/retry.py +0 -0
  191. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/stats/__init__.py +0 -0
  192. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/stats/models/__init__.py +0 -0
  193. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/stats/models/pydantic/__init__.py +0 -0
  194. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/stats/models/pydantic/requests.py +0 -0
  195. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/stats/models/pydantic/responses.py +0 -0
  196. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/stats/resource.py +0 -0
  197. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/teams/__init__.py +0 -0
  198. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/teams/models/__init__.py +0 -0
  199. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/teams/models/pydantic/__init__.py +0 -0
  200. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/teams/models/pydantic/identity.py +0 -0
  201. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/teams/models/pydantic/requests.py +0 -0
  202. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/teams/models/pydantic/responses.py +0 -0
  203. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/teams/resource.py +0 -0
  204. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/venues/__init__.py +0 -0
  205. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/venues/models/__init__.py +0 -0
  206. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/venues/models/pydantic/__init__.py +0 -0
  207. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/venues/models/pydantic/identity.py +0 -0
  208. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/venues/models/pydantic/responses.py +0 -0
  209. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data/venues/resource.py +0 -0
  210. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data.egg-info/dependency_links.txt +0 -0
  211. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data.egg-info/requires.txt +0 -0
  212. {cfb_data-0.7.0 → cfb_data-0.8.0}/cfb_data/cfb_data.egg-info/top_level.txt +0 -0
  213. {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.7.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 :: 2 - Pre-Alpha
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.7.0 is a pre-alpha Python toolkit for exploring the public
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 pre-alpha, and the documentation describes the current version.
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.7.0 is a pre-alpha Python toolkit for exploring the public
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 pre-alpha, and the documentation describes the current version.
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
- safe_cause = _sanitized_cause(exc)
82
- raise CFBDDataFrameConversionError(
83
- endpoint=endpoint,
84
- backend="pandas",
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
- safe_cause = _sanitized_cause(exc)
129
- raise CFBDDataFrameConversionError(
130
- endpoint=endpoint,
131
- backend="pandas",
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
- safe_cause = _sanitized_cause(exc)
166
- raise CFBDDataFrameConversionError(
167
- endpoint=endpoint,
168
- backend="polars",
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
- safe_cause = _sanitized_cause(exc)
233
- raise CFBDDataFrameConversionError(
234
- endpoint=endpoint,
235
- backend="polars",
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
- safe_cause = _sanitized_cause(exc)
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 a sanitized cause."""
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
- safe_cause = _sanitized_cause(exc)
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 a sanitized cause."""
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
- safe_cause = _sanitized_cause(exc)
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 a sanitized cause."""
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
- safe_cause = _sanitized_cause(exc)
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, _sanitized_cause
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 safe categorized failure in the internal Parquet codec."""
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 path- and payload-free codec failure.
53
+ """Initialize a categorized codec failure.
54
54
 
55
55
  :param operation: File operation that failed.
56
- :param category: Safe failure classification for internal policy.
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
- category = _codec_error_category(exc)
84
- safe_cause = _sanitized_cause(exc)
85
- raise _ParquetCodecError(operation="write", category=category) from safe_cause
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
- category = _codec_error_category(exc)
122
- safe_cause = _sanitized_cause(exc)
123
- raise _ParquetCodecError(operation="read", category=category) from safe_cause
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 without retaining its values or path."""
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, _sanitized_cause
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
- safe_cause = _sanitized_cause(exc)
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: _SanitizedCause
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=_sanitized_cause(exc),
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=_sanitized_cause(exc),
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=_sanitized_cause(exc),
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=_sanitized_cause(exc),
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=_sanitized_cause(exc),
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=_sanitized_cause(exc),
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
- aiohttp.ContentTypeError,
482
- json.JSONDecodeError,
483
- UnicodeDecodeError,
484
- ) as exc:
485
- safe_cause = _sanitized_cause(exc)
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 safe_cause
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
- await self._commit_catalog_response(
1188
- "reproject", record, projection, context=context
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=self._catalog_commit_timeout(projection),
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=self._config.io_timeout_seconds,
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:
@@ -0,0 +1,3 @@
1
+ SELECT projection_contract
2
+ FROM coverage
3
+ WHERE endpoint = ? AND canonical_filters = ?;
@@ -1,4 +1,4 @@
1
- """Define safe, actionable exceptions raised by :mod:`cfb_data`."""
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