cfb-data 0.4.1__tar.gz → 0.5.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 (211) hide show
  1. {cfb_data-0.4.1 → cfb_data-0.5.0}/PKG-INFO +77 -9
  2. {cfb_data-0.4.1 → cfb_data-0.5.0}/README.md +72 -8
  3. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/__init__.py +41 -0
  4. cfb_data-0.5.0/cfb_data/cfb_data/_catalog/__init__.py +1 -0
  5. cfb_data-0.5.0/cfb_data/cfb_data/_catalog/merge.py +104 -0
  6. cfb_data-0.5.0/cfb_data/cfb_data/_catalog/models.py +304 -0
  7. cfb_data-0.5.0/cfb_data/cfb_data/_catalog/projection.py +723 -0
  8. cfb_data-0.5.0/cfb_data/cfb_data/_catalog/sources.py +300 -0
  9. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/_executor.py +70 -32
  10. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/_transport.py +137 -5
  11. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/adjusted_metrics/models/pydantic/responses.py +39 -0
  12. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/betting/models/pydantic/responses.py +23 -0
  13. cfb_data-0.5.0/cfb_data/cfb_data/cache/__init__.py +21 -0
  14. cfb_data-0.5.0/cfb_data/cfb_data/cache/_backend.py +159 -0
  15. cfb_data-0.5.0/cfb_data/cfb_data/cache/_catalog.py +94 -0
  16. cfb_data-0.5.0/cfb_data/cfb_data/cache/_catalog_codecs.py +343 -0
  17. cfb_data-0.5.0/cfb_data/cfb_data/cache/_coordinator.py +1094 -0
  18. cfb_data-0.5.0/cfb_data/cfb_data/cache/_identity_codecs.py +106 -0
  19. cfb_data-0.5.0/cfb_data/cfb_data/cache/_key.py +94 -0
  20. cfb_data-0.5.0/cfb_data/cfb_data/cache/_models.py +24 -0
  21. cfb_data-0.5.0/cfb_data/cfb_data/cache/_null.py +405 -0
  22. cfb_data-0.5.0/cfb_data/cfb_data/cache/_redis.py +1474 -0
  23. cfb_data-0.5.0/cfb_data/cfb_data/cache/_sqlite.py +1000 -0
  24. cfb_data-0.5.0/cfb_data/cfb_data/cache/_sqlite_sql.py +68 -0
  25. cfb_data-0.5.0/cfb_data/cfb_data/cache/config.py +174 -0
  26. cfb_data-0.5.0/cfb_data/cfb_data/cache/policy.py +174 -0
  27. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/acquire_refresh_lease.sql +7 -0
  28. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/begin_immediate.sql +1 -0
  29. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/catalog_counts.sql +16 -0
  30. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/cleanup_responses.sql +2 -0
  31. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/delete_coverage_failure.sql +2 -0
  32. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/delete_response.sql +2 -0
  33. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/delete_team_aliases.sql +2 -0
  34. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/enable_foreign_keys.sql +1 -0
  35. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/enable_wal.sql +1 -0
  36. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/find_athletes.sql +7 -0
  37. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/find_conference_by_id.sql +3 -0
  38. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/find_conference_by_name.sql +3 -0
  39. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/find_fresh_coverage.sql +6 -0
  40. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/find_game_by_id.sql +12 -0
  41. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/find_games.sql +28 -0
  42. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/find_team_by_id.sql +3 -0
  43. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/find_team_by_name.sql +6 -0
  44. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/find_venue_by_id.sql +3 -0
  45. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/find_venue_by_name.sql +3 -0
  46. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/get_response.sql +13 -0
  47. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/get_response_size.sql +3 -0
  48. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/get_schema_version.sql +3 -0
  49. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/initialize_schema_version.sql +3 -0
  50. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/release_refresh_lease.sql +2 -0
  51. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/renew_refresh_lease.sql +3 -0
  52. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/schema.sql +235 -0
  53. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/select_catalog_observations.sql +6 -0
  54. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/set_busy_timeout.sql +1 -0
  55. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/set_synchronous_normal.sql +1 -0
  56. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_athlete.sql +7 -0
  57. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_athlete_team_season.sql +5 -0
  58. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_catalog_observation.sql +4 -0
  59. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_coach.sql +7 -0
  60. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_coach_team_season.sql +6 -0
  61. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_conference.sql +9 -0
  62. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_conference_affiliation.sql +5 -0
  63. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_coverage.sql +20 -0
  64. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_coverage_failure.sql +5 -0
  65. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_drive.sql +9 -0
  66. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_game.sql +12 -0
  67. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_play.sql +8 -0
  68. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_playoff_matchup.sql +6 -0
  69. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_recruit.sql +7 -0
  70. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_response.sql +12 -0
  71. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_team.sql +9 -0
  72. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_team_alias.sql +4 -0
  73. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_team_season.sql +6 -0
  74. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_venue.sql +8 -0
  75. cfb_data-0.5.0/cfb_data/cfb_data/cache/sql/upsert_vocabulary.sql +6 -0
  76. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/client.py +92 -4
  77. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/coaches/models/pydantic/responses.py +91 -0
  78. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/conferences/__init__.py +2 -0
  79. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/conferences/models/pydantic/__init__.py +2 -0
  80. cfb_data-0.5.0/cfb_data/cfb_data/conferences/models/pydantic/identity.py +19 -0
  81. cfb_data-0.5.0/cfb_data/cfb_data/conferences/models/pydantic/responses.py +165 -0
  82. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/draft/models/pydantic/responses.py +54 -0
  83. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/drives/models/pydantic/responses.py +17 -0
  84. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/errors.py +44 -0
  85. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/games/__init__.py +2 -0
  86. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/games/models/pydantic/__init__.py +2 -0
  87. cfb_data-0.5.0/cfb_data/cfb_data/games/models/pydantic/identity.py +27 -0
  88. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/games/models/pydantic/responses.py +159 -0
  89. cfb_data-0.5.0/cfb_data/cfb_data/identities/__init__.py +5 -0
  90. cfb_data-0.5.0/cfb_data/cfb_data/identities/_normalization.py +28 -0
  91. cfb_data-0.5.0/cfb_data/cfb_data/identities/contracts.py +35 -0
  92. cfb_data-0.5.0/cfb_data/cfb_data/identities/resource.py +986 -0
  93. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/metrics/models/pydantic/responses.py +68 -0
  94. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/players/__init__.py +2 -0
  95. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/players/models/pydantic/__init__.py +2 -0
  96. cfb_data-0.5.0/cfb_data/cfb_data/players/models/pydantic/identity.py +16 -0
  97. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/players/models/pydantic/responses.py +69 -0
  98. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/playoffs/models/pydantic/responses.py +67 -0
  99. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/plays/models/pydantic/responses.py +122 -0
  100. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/rankings/models/pydantic/responses.py +10 -0
  101. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/recruiting/models/pydantic/responses.py +25 -0
  102. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/stats/models/pydantic/responses.py +90 -2
  103. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/stats/resource.py +3 -2
  104. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/teams/__init__.py +2 -0
  105. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/teams/models/pydantic/__init__.py +2 -0
  106. cfb_data-0.5.0/cfb_data/cfb_data/teams/models/pydantic/identity.py +17 -0
  107. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/teams/models/pydantic/responses.py +102 -5
  108. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/venues/__init__.py +2 -2
  109. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/venues/models/pydantic/__init__.py +2 -1
  110. cfb_data-0.5.0/cfb_data/cfb_data/venues/models/pydantic/identity.py +17 -0
  111. cfb_data-0.5.0/cfb_data/cfb_data/venues/models/pydantic/responses.py +64 -0
  112. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data.egg-info/PKG-INFO +77 -9
  113. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data.egg-info/SOURCES.txt +77 -0
  114. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data.egg-info/requires.txt +5 -0
  115. {cfb_data-0.4.1 → cfb_data-0.5.0}/pyproject.toml +11 -2
  116. cfb_data-0.4.1/cfb_data/cfb_data/conferences/models/pydantic/responses.py +0 -64
  117. cfb_data-0.4.1/cfb_data/cfb_data/venues/models/pydantic/responses.py +0 -24
  118. {cfb_data-0.4.1 → cfb_data-0.5.0}/LICENSE +0 -0
  119. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/_dataframes.py +0 -0
  120. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/_parquet.py +0 -0
  121. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/_request_rules.py +0 -0
  122. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/_requests.py +0 -0
  123. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/_tabular.py +0 -0
  124. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/adjusted_metrics/__init__.py +0 -0
  125. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/adjusted_metrics/models/__init__.py +0 -0
  126. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/adjusted_metrics/models/pydantic/__init__.py +0 -0
  127. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/adjusted_metrics/models/pydantic/requests.py +0 -0
  128. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/adjusted_metrics/resource.py +0 -0
  129. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/base/__init__.py +0 -0
  130. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/base/types.py +0 -0
  131. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/betting/__init__.py +0 -0
  132. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/betting/models/__init__.py +0 -0
  133. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/betting/models/pydantic/__init__.py +0 -0
  134. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/betting/models/pydantic/requests.py +0 -0
  135. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/betting/resource.py +0 -0
  136. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/coaches/__init__.py +0 -0
  137. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/coaches/models/__init__.py +0 -0
  138. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/coaches/models/pydantic/__init__.py +0 -0
  139. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/coaches/models/pydantic/requests.py +0 -0
  140. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/coaches/resource.py +0 -0
  141. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/conferences/models/__init__.py +0 -0
  142. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/conferences/models/pydantic/requests.py +0 -0
  143. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/conferences/resource.py +0 -0
  144. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/draft/__init__.py +0 -0
  145. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/draft/models/__init__.py +0 -0
  146. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/draft/models/pydantic/__init__.py +0 -0
  147. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/draft/models/pydantic/requests.py +0 -0
  148. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/draft/resource.py +0 -0
  149. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/drives/__init__.py +0 -0
  150. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/drives/models/__init__.py +0 -0
  151. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/drives/models/pydantic/__init__.py +0 -0
  152. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/drives/models/pydantic/requests.py +0 -0
  153. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/drives/resource.py +0 -0
  154. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/enums.py +0 -0
  155. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/games/models/__init__.py +0 -0
  156. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/games/models/pydantic/requests.py +0 -0
  157. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/games/resource.py +0 -0
  158. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/info/__init__.py +0 -0
  159. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/info/models/__init__.py +0 -0
  160. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/info/models/pydantic/__init__.py +0 -0
  161. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/info/models/pydantic/requests.py +0 -0
  162. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/info/models/pydantic/responses.py +0 -0
  163. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/info/resource.py +0 -0
  164. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/metrics/__init__.py +0 -0
  165. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/metrics/models/__init__.py +0 -0
  166. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/metrics/models/pydantic/__init__.py +0 -0
  167. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/metrics/models/pydantic/requests.py +0 -0
  168. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/metrics/resource.py +0 -0
  169. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/players/models/__init__.py +0 -0
  170. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/players/models/pydantic/requests.py +0 -0
  171. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/players/resource.py +0 -0
  172. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/playoffs/__init__.py +0 -0
  173. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/playoffs/models/__init__.py +0 -0
  174. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/playoffs/models/pydantic/__init__.py +0 -0
  175. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/playoffs/models/pydantic/requests.py +0 -0
  176. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/playoffs/resource.py +0 -0
  177. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/plays/__init__.py +0 -0
  178. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/plays/models/__init__.py +0 -0
  179. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/plays/models/pydantic/__init__.py +0 -0
  180. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/plays/models/pydantic/requests.py +0 -0
  181. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/plays/resource.py +0 -0
  182. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/py.typed +0 -0
  183. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/rankings/__init__.py +0 -0
  184. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/rankings/models/__init__.py +0 -0
  185. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/rankings/models/pydantic/__init__.py +0 -0
  186. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/rankings/models/pydantic/requests.py +0 -0
  187. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/rankings/resource.py +0 -0
  188. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/ratings/__init__.py +0 -0
  189. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/ratings/models/__init__.py +0 -0
  190. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/ratings/models/pydantic/__init__.py +0 -0
  191. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/ratings/models/pydantic/requests.py +0 -0
  192. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/ratings/models/pydantic/responses.py +0 -0
  193. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/ratings/resource.py +0 -0
  194. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/recruiting/__init__.py +0 -0
  195. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/recruiting/models/__init__.py +0 -0
  196. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/recruiting/models/pydantic/__init__.py +0 -0
  197. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/recruiting/models/pydantic/requests.py +0 -0
  198. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/recruiting/resource.py +0 -0
  199. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/retry.py +0 -0
  200. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/stats/__init__.py +0 -0
  201. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/stats/models/__init__.py +0 -0
  202. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/stats/models/pydantic/__init__.py +0 -0
  203. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/stats/models/pydantic/requests.py +0 -0
  204. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/teams/models/__init__.py +0 -0
  205. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/teams/models/pydantic/requests.py +0 -0
  206. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/teams/resource.py +0 -0
  207. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/venues/models/__init__.py +0 -0
  208. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data/venues/resource.py +0 -0
  209. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data.egg-info/dependency_links.txt +0 -0
  210. {cfb_data-0.4.1 → cfb_data-0.5.0}/cfb_data/cfb_data.egg-info/top_level.txt +0 -0
  211. {cfb_data-0.4.1 → cfb_data-0.5.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cfb-data
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: Async validated CollegeFootballData access for pandas and Polars
5
5
  Author: Ryan Anderson
6
6
  License-Expression: MIT
@@ -16,11 +16,15 @@ Requires-Python: >=3.12
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: aiohttp<4,>=3.13
19
+ Requires-Dist: aiosqlite<1,>=0.22
20
+ Requires-Dist: jinja2<4,>=3.1
19
21
  Requires-Dist: pandas<4,>=3.0
20
22
  Requires-Dist: pydantic<3,>=2.12
21
23
  Requires-Dist: pyarrow<26,>=25
22
24
  Provides-Extra: polars
23
25
  Requires-Dist: polars<2,>=1.43.2; extra == "polars"
26
+ Provides-Extra: redis
27
+ Requires-Dist: redis<8,>=7.1; extra == "redis"
24
28
  Provides-Extra: dev
25
29
  Requires-Dist: build<2,>=1.5; extra == "dev"
26
30
  Requires-Dist: furo>=2025.12.19; extra == "dev"
@@ -37,7 +41,7 @@ Dynamic: license-file
37
41
 
38
42
  # College Football Data Python Toolkit
39
43
 
40
- `cfb-data` 0.4.1 is an asynchronous, validated client for the public
44
+ `cfb-data` 0.5.0 is an asynchronous, validated client for the public
41
45
  [CollegeFootballData API](https://collegefootballdata.com/) REST endpoint
42
46
  groups. It returns eager pandas DataFrames by default and can return the same
43
47
  logical tables as Polars DataFrames. Irreducibly nested analytical responses
@@ -72,8 +76,15 @@ Install the optional Polars backend with:
72
76
  python -m pip install "cfb-data[polars]"
73
77
  ```
74
78
 
79
+ SQLite caching and identity lookup are available in the base installation.
80
+ Install the optional shared Redis backend with:
81
+
82
+ ```sh
83
+ python -m pip install "cfb-data[redis]"
84
+ ```
85
+
75
86
  Python 3.12 and 3.13 are supported. DataFrames are eager; Polars
76
- `LazyFrame` results are not part of the 0.4.1 contract.
87
+ `LazyFrame` results are not part of the 0.5.0 contract.
77
88
 
78
89
  ## Authentication and lifecycle
79
90
 
@@ -143,6 +154,56 @@ async with CFBDClient(dataframe_backend="polars") as client:
143
154
  Type checkers infer `pandas.DataFrame` for the default client and
144
155
  `polars.DataFrame` when the literal backend is `"polars"`.
145
156
 
157
+ ## Response caching and identities
158
+
159
+ Caching is opt-in. `SQLiteCacheConfig()` provides a private per-user local
160
+ response cache and durable identity catalog without another service:
161
+
162
+ ```python
163
+ from cfb_data import CFBDClient, FreshnessMode, SQLiteCacheConfig
164
+
165
+ async with CFBDClient(cache=SQLiteCacheConfig()) as client:
166
+ games = await client.games.list(year=2025)
167
+ repeated = await client.games.list(year=2025) # validated exact cache hit
168
+
169
+ team = await client.identities.teams.resolve("MICH")
170
+ game = await client.identities.games.resolve(game_id=401628347)
171
+
172
+ with client.cache_mode("local_only"):
173
+ offline = await client.games.list(year=2025)
174
+
175
+ retained = await client.identities.teams.resolve(
176
+ "Michigan",
177
+ freshness=FreshnessMode.allow_stale,
178
+ )
179
+ ```
180
+
181
+ Use `RedisCacheConfig(url=...)` for multiple workers or hosts. Validated
182
+ response records expire according to college-football-specific TTL profiles;
183
+ normalized identity facts and their coverage ledger remain until explicitly
184
+ pruned or rebuilt. Operational account and usage routes are never cached.
185
+ Cache hits are decoded and revalidated through the current Pydantic response
186
+ contract, and cache/backend failures fail open for ordinary API calls without
187
+ silently changing backend types.
188
+
189
+ Source-domain Pydantic models own the upstream fields and typed declarations
190
+ that produce catalog facts. The catalog owns only normalized merge, coverage,
191
+ provenance, persistence, and query semantics. The SQLite schema and queries
192
+ live in packaged `.sql` resources rendered through a strict Jinja handler;
193
+ runtime data continues to use bound SQLite parameters. With persistence
194
+ disabled, the same projection path writes a client-local transient catalog;
195
+ there is no separate response-to-identity fallback. Compact identity result
196
+ types live in their team, conference, venue, game, and player domains, while
197
+ `client.identities` remains the query and hydration facade.
198
+
199
+ Per-operation modes are `default`, `refresh`, `bypass`, and `local_only`.
200
+ `client.identities.hydrate(...)` supports dry-run, bounded-concurrency,
201
+ resumable canonical hydration at `4 + 2S` calls for `S` seasons, or `7 + 2S`
202
+ with vocabularies, with an optional division-classification scope. See the complete
203
+ [response caching and identity guide](docs/guides/cache-and-identities.md) for
204
+ TTL defaults, ambiguity behavior, maintenance, Redis Docker/Compose setup, and
205
+ hosted-service security requirements.
206
+
146
207
  ## Endpoints
147
208
 
148
209
  Each method accepts either one positional request model or explicit snake-case
@@ -298,10 +359,12 @@ original string, integer, or float value for DataFrames.
298
359
  Direct pandas and Polars Parquet methods are not the cfb-data persistence
299
360
  compatibility contract: pandas object inference and Polars `Object` columns do
300
361
  not provide identical empty- and mixed-value behavior. Version 0.2.0 keeps the
301
- codec internal so future caching can use it without prematurely committing to
302
- a public save/load API. See
362
+ codec internal so future workflow checkpoints and tabular artifacts can use it
363
+ without prematurely committing to a public save/load API. See
303
364
  [`ADR 0003`](docs/architecture/0003-canonical-arrow-parquet.md) for the format,
304
- validation, and compatibility decisions.
365
+ validation, and compatibility decisions. API response caching and its durable
366
+ identity catalog are a separate accepted architecture in
367
+ [`ADR 0004`](docs/architecture/0004-api-cache-identity-catalog.md).
305
368
 
306
369
  ## Retries and errors
307
370
 
@@ -371,7 +434,7 @@ Use the typed namespace method and either its request model or keyword filters.
371
434
 
372
435
  ## Datasets and workflows
373
436
 
374
- Version 0.4.1 does not expose `client.datasets` or `client.workflows`. The
437
+ Version 0.5.0 does not expose `client.datasets` or `client.workflows`. The
375
438
  accepted architecture reserves two higher layers:
376
439
 
377
440
  - datasets compose validated endpoint results and validated subdatasets
@@ -396,8 +459,13 @@ make docs
396
459
  make check
397
460
  ```
398
461
 
399
- `make install` creates `.venv` and installs `.[dev,polars]`, giving
400
- contributors the complete Arrow/Parquet and two-backend test contract.
462
+ `make install` creates `.venv` and installs `.[dev,polars,redis]`, giving
463
+ contributors the complete Arrow/Parquet, DataFrame, and Redis-client test
464
+ contract. `make redis-up` and `make test-redis` exercise the local shared
465
+ backend; `make test-live` is an explicit credentialed check using untracked
466
+ `.env` configuration. `make test-live-all` is the separately opted-in,
467
+ quota-ledgered 74-route SQLite/Redis matrix and keeps the transport's normal
468
+ bounded retries enabled.
401
469
  `make check` runs Ruff, strict mypy, a warning-free Sphinx build, and pytest
402
470
  under the same contract as CI. `make docs` writes the local HTML site to
403
471
  `docs/_build/html`. Package metadata and all dependency groups live only in
@@ -1,6 +1,6 @@
1
1
  # College Football Data Python Toolkit
2
2
 
3
- `cfb-data` 0.4.1 is an asynchronous, validated client for the public
3
+ `cfb-data` 0.5.0 is an asynchronous, validated client for the public
4
4
  [CollegeFootballData API](https://collegefootballdata.com/) REST endpoint
5
5
  groups. It returns eager pandas DataFrames by default and can return the same
6
6
  logical tables as Polars DataFrames. Irreducibly nested analytical responses
@@ -35,8 +35,15 @@ Install the optional Polars backend with:
35
35
  python -m pip install "cfb-data[polars]"
36
36
  ```
37
37
 
38
+ SQLite caching and identity lookup are available in the base installation.
39
+ Install the optional shared Redis backend with:
40
+
41
+ ```sh
42
+ python -m pip install "cfb-data[redis]"
43
+ ```
44
+
38
45
  Python 3.12 and 3.13 are supported. DataFrames are eager; Polars
39
- `LazyFrame` results are not part of the 0.4.1 contract.
46
+ `LazyFrame` results are not part of the 0.5.0 contract.
40
47
 
41
48
  ## Authentication and lifecycle
42
49
 
@@ -106,6 +113,56 @@ async with CFBDClient(dataframe_backend="polars") as client:
106
113
  Type checkers infer `pandas.DataFrame` for the default client and
107
114
  `polars.DataFrame` when the literal backend is `"polars"`.
108
115
 
116
+ ## Response caching and identities
117
+
118
+ Caching is opt-in. `SQLiteCacheConfig()` provides a private per-user local
119
+ response cache and durable identity catalog without another service:
120
+
121
+ ```python
122
+ from cfb_data import CFBDClient, FreshnessMode, SQLiteCacheConfig
123
+
124
+ async with CFBDClient(cache=SQLiteCacheConfig()) as client:
125
+ games = await client.games.list(year=2025)
126
+ repeated = await client.games.list(year=2025) # validated exact cache hit
127
+
128
+ team = await client.identities.teams.resolve("MICH")
129
+ game = await client.identities.games.resolve(game_id=401628347)
130
+
131
+ with client.cache_mode("local_only"):
132
+ offline = await client.games.list(year=2025)
133
+
134
+ retained = await client.identities.teams.resolve(
135
+ "Michigan",
136
+ freshness=FreshnessMode.allow_stale,
137
+ )
138
+ ```
139
+
140
+ Use `RedisCacheConfig(url=...)` for multiple workers or hosts. Validated
141
+ response records expire according to college-football-specific TTL profiles;
142
+ normalized identity facts and their coverage ledger remain until explicitly
143
+ pruned or rebuilt. Operational account and usage routes are never cached.
144
+ Cache hits are decoded and revalidated through the current Pydantic response
145
+ contract, and cache/backend failures fail open for ordinary API calls without
146
+ silently changing backend types.
147
+
148
+ Source-domain Pydantic models own the upstream fields and typed declarations
149
+ that produce catalog facts. The catalog owns only normalized merge, coverage,
150
+ provenance, persistence, and query semantics. The SQLite schema and queries
151
+ live in packaged `.sql` resources rendered through a strict Jinja handler;
152
+ runtime data continues to use bound SQLite parameters. With persistence
153
+ disabled, the same projection path writes a client-local transient catalog;
154
+ there is no separate response-to-identity fallback. Compact identity result
155
+ types live in their team, conference, venue, game, and player domains, while
156
+ `client.identities` remains the query and hydration facade.
157
+
158
+ Per-operation modes are `default`, `refresh`, `bypass`, and `local_only`.
159
+ `client.identities.hydrate(...)` supports dry-run, bounded-concurrency,
160
+ resumable canonical hydration at `4 + 2S` calls for `S` seasons, or `7 + 2S`
161
+ with vocabularies, with an optional division-classification scope. See the complete
162
+ [response caching and identity guide](docs/guides/cache-and-identities.md) for
163
+ TTL defaults, ambiguity behavior, maintenance, Redis Docker/Compose setup, and
164
+ hosted-service security requirements.
165
+
109
166
  ## Endpoints
110
167
 
111
168
  Each method accepts either one positional request model or explicit snake-case
@@ -261,10 +318,12 @@ original string, integer, or float value for DataFrames.
261
318
  Direct pandas and Polars Parquet methods are not the cfb-data persistence
262
319
  compatibility contract: pandas object inference and Polars `Object` columns do
263
320
  not provide identical empty- and mixed-value behavior. Version 0.2.0 keeps the
264
- codec internal so future caching can use it without prematurely committing to
265
- a public save/load API. See
321
+ codec internal so future workflow checkpoints and tabular artifacts can use it
322
+ without prematurely committing to a public save/load API. See
266
323
  [`ADR 0003`](docs/architecture/0003-canonical-arrow-parquet.md) for the format,
267
- validation, and compatibility decisions.
324
+ validation, and compatibility decisions. API response caching and its durable
325
+ identity catalog are a separate accepted architecture in
326
+ [`ADR 0004`](docs/architecture/0004-api-cache-identity-catalog.md).
268
327
 
269
328
  ## Retries and errors
270
329
 
@@ -334,7 +393,7 @@ Use the typed namespace method and either its request model or keyword filters.
334
393
 
335
394
  ## Datasets and workflows
336
395
 
337
- Version 0.4.1 does not expose `client.datasets` or `client.workflows`. The
396
+ Version 0.5.0 does not expose `client.datasets` or `client.workflows`. The
338
397
  accepted architecture reserves two higher layers:
339
398
 
340
399
  - datasets compose validated endpoint results and validated subdatasets
@@ -359,8 +418,13 @@ make docs
359
418
  make check
360
419
  ```
361
420
 
362
- `make install` creates `.venv` and installs `.[dev,polars]`, giving
363
- contributors the complete Arrow/Parquet and two-backend test contract.
421
+ `make install` creates `.venv` and installs `.[dev,polars,redis]`, giving
422
+ contributors the complete Arrow/Parquet, DataFrame, and Redis-client test
423
+ contract. `make redis-up` and `make test-redis` exercise the local shared
424
+ backend; `make test-live` is an explicit credentialed check using untracked
425
+ `.env` configuration. `make test-live-all` is the separately opted-in,
426
+ quota-ledgered 74-route SQLite/Redis matrix and keeps the transport's normal
427
+ bounded retries enabled.
364
428
  `make check` runs Ruff, strict mypy, a warning-free Sphinx build, and pytest
365
429
  under the same contract as CI. `make docs` writes the local HTML site to
366
430
  `docs/_build/html`. Package metadata and all dependency groups live only in
@@ -7,6 +7,15 @@ from .adjusted_metrics.models.pydantic.requests import (
7
7
  KickerPAARRequest,
8
8
  )
9
9
  from .betting.models.pydantic.requests import BettingLinesRequest
10
+ from .cache import (
11
+ CacheConfig,
12
+ CacheMode,
13
+ CachePolicyConfig,
14
+ CacheProfile,
15
+ CacheTTL,
16
+ RedisCacheConfig,
17
+ SQLiteCacheConfig,
18
+ )
10
19
  from .client import CFBDClient, DataFrameBackend
11
20
  from .coaches.models.pydantic.requests import (
12
21
  CoachesRequest,
@@ -14,6 +23,7 @@ from .coaches.models.pydantic.requests import (
14
23
  CoachSeasonsRequest,
15
24
  CoachTenuresRequest,
16
25
  )
26
+ from .conferences.models.pydantic.identity import ConferenceIdentity
17
27
  from .conferences.models.pydantic.requests import (
18
28
  ConferenceAffiliationsRequest,
19
29
  ConferenceChangesRequest,
@@ -36,11 +46,17 @@ from .enums import (
36
46
  from .errors import (
37
47
  CFBDAuthenticationError,
38
48
  CFBDAuthorizationError,
49
+ CFBDCacheBackendError,
50
+ CFBDCacheError,
51
+ CFBDCacheMissError,
39
52
  CFBDClientStateError,
40
53
  CFBDConfigurationError,
41
54
  CFBDDataFrameConversionError,
42
55
  CFBDError,
43
56
  CFBDHTTPError,
57
+ CFBDIdentityAmbiguityError,
58
+ CFBDIdentityNotFoundError,
59
+ CFBDNoContentError,
44
60
  CFBDOptionalDependencyError,
45
61
  CFBDRateLimitError,
46
62
  CFBDRequestValidationError,
@@ -51,6 +67,7 @@ from .errors import (
51
67
  CFBDTLSError,
52
68
  CFBDTransportError,
53
69
  )
70
+ from .games.models.pydantic.identity import GameIdentity
54
71
  from .games.models.pydantic.requests import (
55
72
  AdvancedBoxScoreRequest,
56
73
  CalendarRequest,
@@ -63,6 +80,7 @@ from .games.models.pydantic.requests import (
63
80
  TeamGameStatsRequest,
64
81
  )
65
82
  from .games.models.pydantic.responses import AdvancedBoxScore
83
+ from .identities import FreshnessMode, HydrationPlan
66
84
  from .info.models.pydantic.requests import InfoUsageRequest
67
85
  from .info.models.pydantic.responses import UserInfo, UserUsage
68
86
  from .metrics.models.pydantic.requests import (
@@ -74,6 +92,7 @@ from .metrics.models.pydantic.requests import (
74
92
  TeamSeasonPPARequest,
75
93
  WinProbabilityRequest,
76
94
  )
95
+ from .players.models.pydantic.identity import AthleteIdentity
77
96
  from .players.models.pydantic.requests import (
78
97
  PlayerSearchRequest,
79
98
  PlayerSeasonOverviewRequest,
@@ -118,6 +137,7 @@ from .stats.models.pydantic.requests import (
118
137
  PlayerSeasonSuccessRequest,
119
138
  TeamSeasonStatsRequest,
120
139
  )
140
+ from .teams.models.pydantic.identity import TeamIdentity
121
141
  from .teams.models.pydantic.requests import (
122
142
  FBSTeamsRequest,
123
143
  RosterRequest,
@@ -126,29 +146,42 @@ from .teams.models.pydantic.requests import (
126
146
  TeamMatchupRequest,
127
147
  TeamsRequest,
128
148
  )
149
+ from .venues.models.pydantic.identity import VenueIdentity
129
150
 
130
151
  __all__ = [
131
152
  "AdjustedPlayerPassingRequest",
132
153
  "AdjustedPlayerRushingRequest",
133
154
  "AdjustedTeamMetricsRequest",
155
+ "AthleteIdentity",
134
156
  "AdvancedBoxScoreRequest",
135
157
  "AdvancedBoxScore",
136
158
  "AdvancedGameStatsRequest",
137
159
  "AdvancedSeasonStatsRequest",
138
160
  "BettingLinesRequest",
139
161
  "CalendarRequest",
162
+ "CacheConfig",
163
+ "CacheMode",
164
+ "CachePolicyConfig",
165
+ "CacheProfile",
166
+ "CacheTTL",
140
167
  "CfpGamesRequest",
141
168
  "CfpParticipantsRequest",
142
169
  "CfpPlayoff",
143
170
  "CfpPlayoffRequest",
144
171
  "CFBDAuthenticationError",
145
172
  "CFBDAuthorizationError",
173
+ "CFBDCacheBackendError",
174
+ "CFBDCacheError",
175
+ "CFBDCacheMissError",
146
176
  "CFBDClient",
147
177
  "CFBDClientStateError",
148
178
  "CFBDConfigurationError",
149
179
  "CFBDDataFrameConversionError",
150
180
  "CFBDError",
151
181
  "CFBDHTTPError",
182
+ "CFBDIdentityAmbiguityError",
183
+ "CFBDIdentityNotFoundError",
184
+ "CFBDNoContentError",
152
185
  "CFBDOptionalDependencyError",
153
186
  "CFBDRateLimitError",
154
187
  "CFBDRequestValidationError",
@@ -166,6 +199,7 @@ __all__ = [
166
199
  "ConferenceAffiliationsRequest",
167
200
  "ConferenceChangesRequest",
168
201
  "ConferenceClassification",
202
+ "ConferenceIdentity",
169
203
  "ConferencesRequest",
170
204
  "ConferenceSPRatingsRequest",
171
205
  "CoreRatingsRequest",
@@ -175,12 +209,15 @@ __all__ = [
175
209
  "EloRatingsRequest",
176
210
  "ExpandedSRSRatingsRequest",
177
211
  "FPIRatingsRequest",
212
+ "FreshnessMode",
178
213
  "GameMediaRequest",
214
+ "GameIdentity",
179
215
  "GamesRequest",
180
216
  "GameWeatherRequest",
181
217
  "GameHavocRequest",
182
218
  "FBSTeamsRequest",
183
219
  "HomeAway",
220
+ "HydrationPlan",
184
221
  "InfoUsageRequest",
185
222
  "KickerPAARRequest",
186
223
  "LiveGame",
@@ -208,6 +245,7 @@ __all__ = [
208
245
  "RecruitingGroupsRequest",
209
246
  "RecruitingPlayersRequest",
210
247
  "RecruitingTeamsRequest",
248
+ "RedisCacheConfig",
211
249
  "RetryPolicy",
212
250
  "RosterRequest",
213
251
  "ReturningProductionRequest",
@@ -216,7 +254,9 @@ __all__ = [
216
254
  "SeasonType",
217
255
  "SPRatingsRequest",
218
256
  "SRSRatingsRequest",
257
+ "SQLiteCacheConfig",
219
258
  "TeamGamePPARequest",
259
+ "TeamIdentity",
220
260
  "TeamGameStatsRequest",
221
261
  "TeamSeasonStatsRequest",
222
262
  "TeamSeasonPPARequest",
@@ -229,6 +269,7 @@ __all__ = [
229
269
  "UserInfo",
230
270
  "UserUsage",
231
271
  "UserUsageApi",
272
+ "VenueIdentity",
232
273
  "WinProbabilityRequest",
233
274
  "DownType",
234
275
  ]
@@ -0,0 +1 @@
1
+ """Provide internal normalized catalog contracts."""
@@ -0,0 +1,104 @@
1
+ """Merge canonical observations independently of storage backends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import fields, replace
6
+ from datetime import datetime
7
+ from typing import Protocol, cast
8
+
9
+ from cfb_data._catalog.models import (
10
+ CatalogFact,
11
+ CatalogObservation,
12
+ FieldObservation,
13
+ ObservationState,
14
+ )
15
+ from cfb_data._catalog.projection import catalog_fact_key
16
+
17
+
18
+ class _FactConstructor(Protocol):
19
+ """Construct a canonical fact from merge-validated field values."""
20
+
21
+ def __call__(self, **values: object) -> CatalogFact:
22
+ """Return one canonical fact."""
23
+ ...
24
+
25
+
26
+ def merge_catalog_observations(
27
+ current: CatalogObservation | None,
28
+ candidate: CatalogObservation,
29
+ ) -> CatalogObservation:
30
+ """Merge observations using authority, time, and stable source precedence.
31
+
32
+ :param current: Previously selected field observations, if any.
33
+ :param candidate: Newly encountered field observations for the same grain.
34
+ :return: A deterministic merged observation.
35
+ :raises ValueError: If the observations describe different fact grains.
36
+ """
37
+ if current is None:
38
+ return (
39
+ candidate
40
+ if candidate.first_observed_at is not None
41
+ else replace(candidate, first_observed_at=_earliest_evidence(candidate))
42
+ )
43
+ if type(current.fact) is not type(candidate.fact) or catalog_fact_key(
44
+ current.fact
45
+ ) != catalog_fact_key(candidate.fact):
46
+ raise ValueError("Catalog observations must share a type and grain")
47
+
48
+ current_fields = {field.field: field for field in current.fields}
49
+ candidate_fields = {field.field: field for field in candidate.fields}
50
+ merged_fields: list[FieldObservation] = []
51
+ values: dict[str, object] = {}
52
+ for fact_field in fields(current.fact):
53
+ existing = current_fields[fact_field.name]
54
+ incoming = candidate_fields[fact_field.name]
55
+ selected = _select_field(existing, incoming)
56
+ merged_fields.append(selected)
57
+ if selected.value.state is ObservationState.value:
58
+ values[fact_field.name] = selected.value.value
59
+ elif selected.value.state is ObservationState.null:
60
+ values[fact_field.name] = None
61
+ else:
62
+ values[fact_field.name] = getattr(current.fact, fact_field.name)
63
+ constructor = cast(_FactConstructor, type(current.fact))
64
+ return CatalogObservation(
65
+ constructor(**values),
66
+ tuple(merged_fields),
67
+ min(_first_observed_at(current), _first_observed_at(candidate)),
68
+ )
69
+
70
+
71
+ def _first_observed_at(observation: CatalogObservation) -> datetime:
72
+ """Return explicit or field-derived first observation time."""
73
+ return observation.first_observed_at or _earliest_evidence(observation)
74
+
75
+
76
+ def _earliest_evidence(observation: CatalogObservation) -> datetime:
77
+ """Return the earliest timestamp carried by one observation's fields."""
78
+ observed = [
79
+ field.observed_at for field in observation.fields if field.authority > 0
80
+ ]
81
+ if not observed:
82
+ observed = [field.observed_at for field in observation.fields]
83
+ return min(observed)
84
+
85
+
86
+ def _select_field(
87
+ current: FieldObservation, candidate: FieldObservation
88
+ ) -> FieldObservation:
89
+ """Return the field observation that wins deterministic precedence."""
90
+ if candidate.value.state is ObservationState.unobserved:
91
+ return current
92
+ if current.value.state is ObservationState.unobserved:
93
+ return candidate
94
+ current_precedence = (
95
+ current.authority,
96
+ current.observed_at,
97
+ current.source,
98
+ )
99
+ candidate_precedence = (
100
+ candidate.authority,
101
+ candidate.observed_at,
102
+ candidate.source,
103
+ )
104
+ return candidate if candidate_precedence >= current_precedence else current