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.
Files changed (212) hide show
  1. {cfb_data-0.5.1 → cfb_data-0.7.0}/PKG-INFO +26 -4
  2. {cfb_data-0.5.1 → cfb_data-0.7.0}/README.md +24 -2
  3. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/__init__.py +12 -0
  4. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_executor.py +101 -7
  5. cfb_data-0.7.0/cfb_data/cfb_data/_observability.py +111 -0
  6. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_transport.py +166 -13
  7. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_coordinator.py +494 -33
  8. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_redis.py +62 -28
  9. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/client.py +13 -1
  10. cfb_data-0.7.0/cfb_data/cfb_data/observability.py +632 -0
  11. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data.egg-info/PKG-INFO +26 -4
  12. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data.egg-info/SOURCES.txt +2 -0
  13. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data.egg-info/requires.txt +1 -1
  14. {cfb_data-0.5.1 → cfb_data-0.7.0}/pyproject.toml +2 -2
  15. {cfb_data-0.5.1 → cfb_data-0.7.0}/LICENSE +0 -0
  16. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_catalog/__init__.py +0 -0
  17. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_catalog/merge.py +0 -0
  18. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_catalog/models.py +0 -0
  19. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_catalog/projection.py +0 -0
  20. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_catalog/sources.py +0 -0
  21. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_dataframes.py +0 -0
  22. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_parquet.py +0 -0
  23. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_reference_enums.py +0 -0
  24. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_request_rules.py +0 -0
  25. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_requests.py +0 -0
  26. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/_tabular.py +0 -0
  27. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/adjusted_metrics/__init__.py +0 -0
  28. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/adjusted_metrics/models/__init__.py +0 -0
  29. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/adjusted_metrics/models/pydantic/__init__.py +0 -0
  30. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/adjusted_metrics/models/pydantic/requests.py +0 -0
  31. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/adjusted_metrics/models/pydantic/responses.py +0 -0
  32. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/adjusted_metrics/resource.py +0 -0
  33. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/base/__init__.py +0 -0
  34. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/base/types.py +0 -0
  35. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/betting/__init__.py +0 -0
  36. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/betting/models/__init__.py +0 -0
  37. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/betting/models/pydantic/__init__.py +0 -0
  38. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/betting/models/pydantic/requests.py +0 -0
  39. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/betting/models/pydantic/responses.py +0 -0
  40. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/betting/resource.py +0 -0
  41. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/__init__.py +0 -0
  42. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_backend.py +0 -0
  43. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_catalog.py +0 -0
  44. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_catalog_codecs.py +0 -0
  45. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_identity_codecs.py +0 -0
  46. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_key.py +0 -0
  47. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_models.py +0 -0
  48. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_null.py +0 -0
  49. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_sqlite.py +0 -0
  50. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/_sqlite_sql.py +0 -0
  51. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/config.py +0 -0
  52. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/policy.py +0 -0
  53. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/acquire_refresh_lease.sql +0 -0
  54. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/begin_immediate.sql +0 -0
  55. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/catalog_counts.sql +0 -0
  56. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/cleanup_responses.sql +0 -0
  57. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/delete_coverage_failure.sql +0 -0
  58. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/delete_response.sql +0 -0
  59. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/delete_team_aliases.sql +0 -0
  60. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/enable_foreign_keys.sql +0 -0
  61. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/enable_wal.sql +0 -0
  62. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_athletes.sql +0 -0
  63. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_conference_by_id.sql +0 -0
  64. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_conference_by_name.sql +0 -0
  65. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_fresh_coverage.sql +0 -0
  66. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_game_by_id.sql +0 -0
  67. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_games.sql +0 -0
  68. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_team_by_id.sql +0 -0
  69. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_team_by_name.sql +0 -0
  70. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_venue_by_id.sql +0 -0
  71. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/find_venue_by_name.sql +0 -0
  72. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/get_response.sql +0 -0
  73. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/get_response_size.sql +0 -0
  74. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/get_schema_version.sql +0 -0
  75. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/initialize_schema_version.sql +0 -0
  76. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/release_refresh_lease.sql +0 -0
  77. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/renew_refresh_lease.sql +0 -0
  78. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/schema.sql +0 -0
  79. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/select_catalog_observations.sql +0 -0
  80. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/set_busy_timeout.sql +0 -0
  81. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/set_synchronous_normal.sql +0 -0
  82. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_athlete.sql +0 -0
  83. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_athlete_team_season.sql +0 -0
  84. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_catalog_observation.sql +0 -0
  85. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_coach.sql +0 -0
  86. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_coach_team_season.sql +0 -0
  87. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_conference.sql +0 -0
  88. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_conference_affiliation.sql +0 -0
  89. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_coverage.sql +0 -0
  90. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_coverage_failure.sql +0 -0
  91. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_drive.sql +0 -0
  92. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_game.sql +0 -0
  93. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_play.sql +0 -0
  94. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_playoff_matchup.sql +0 -0
  95. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_recruit.sql +0 -0
  96. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_response.sql +0 -0
  97. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_team.sql +0 -0
  98. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_team_alias.sql +0 -0
  99. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_team_season.sql +0 -0
  100. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_venue.sql +0 -0
  101. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/cache/sql/upsert_vocabulary.sql +0 -0
  102. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/coaches/__init__.py +0 -0
  103. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/coaches/models/__init__.py +0 -0
  104. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/coaches/models/pydantic/__init__.py +0 -0
  105. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/coaches/models/pydantic/requests.py +0 -0
  106. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/coaches/models/pydantic/responses.py +0 -0
  107. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/coaches/resource.py +0 -0
  108. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/conferences/__init__.py +0 -0
  109. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/conferences/models/__init__.py +0 -0
  110. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/conferences/models/pydantic/__init__.py +0 -0
  111. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/conferences/models/pydantic/identity.py +0 -0
  112. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/conferences/models/pydantic/requests.py +0 -0
  113. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/conferences/models/pydantic/responses.py +0 -0
  114. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/conferences/resource.py +0 -0
  115. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/draft/__init__.py +0 -0
  116. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/draft/models/__init__.py +0 -0
  117. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/draft/models/pydantic/__init__.py +0 -0
  118. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/draft/models/pydantic/requests.py +0 -0
  119. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/draft/models/pydantic/responses.py +0 -0
  120. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/draft/resource.py +0 -0
  121. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/drives/__init__.py +0 -0
  122. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/drives/models/__init__.py +0 -0
  123. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/drives/models/pydantic/__init__.py +0 -0
  124. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/drives/models/pydantic/requests.py +0 -0
  125. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/drives/models/pydantic/responses.py +0 -0
  126. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/drives/resource.py +0 -0
  127. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/enums.py +0 -0
  128. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/errors.py +0 -0
  129. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/games/__init__.py +0 -0
  130. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/games/models/__init__.py +0 -0
  131. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/games/models/pydantic/__init__.py +0 -0
  132. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/games/models/pydantic/identity.py +0 -0
  133. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/games/models/pydantic/requests.py +0 -0
  134. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/games/models/pydantic/responses.py +0 -0
  135. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/games/resource.py +0 -0
  136. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/identities/__init__.py +0 -0
  137. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/identities/_normalization.py +0 -0
  138. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/identities/contracts.py +0 -0
  139. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/identities/resource.py +0 -0
  140. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/info/__init__.py +0 -0
  141. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/info/models/__init__.py +0 -0
  142. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/info/models/pydantic/__init__.py +0 -0
  143. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/info/models/pydantic/requests.py +0 -0
  144. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/info/models/pydantic/responses.py +0 -0
  145. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/info/resource.py +0 -0
  146. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/metrics/__init__.py +0 -0
  147. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/metrics/models/__init__.py +0 -0
  148. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/metrics/models/pydantic/__init__.py +0 -0
  149. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/metrics/models/pydantic/requests.py +0 -0
  150. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/metrics/models/pydantic/responses.py +0 -0
  151. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/metrics/resource.py +0 -0
  152. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/players/__init__.py +0 -0
  153. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/players/models/__init__.py +0 -0
  154. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/players/models/pydantic/__init__.py +0 -0
  155. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/players/models/pydantic/identity.py +0 -0
  156. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/players/models/pydantic/requests.py +0 -0
  157. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/players/models/pydantic/responses.py +0 -0
  158. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/players/resource.py +0 -0
  159. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/playoffs/__init__.py +0 -0
  160. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/playoffs/models/__init__.py +0 -0
  161. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/playoffs/models/pydantic/__init__.py +0 -0
  162. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/playoffs/models/pydantic/requests.py +0 -0
  163. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/playoffs/models/pydantic/responses.py +0 -0
  164. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/playoffs/resource.py +0 -0
  165. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/plays/__init__.py +0 -0
  166. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/plays/models/__init__.py +0 -0
  167. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/plays/models/pydantic/__init__.py +0 -0
  168. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/plays/models/pydantic/requests.py +0 -0
  169. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/plays/models/pydantic/responses.py +0 -0
  170. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/plays/resource.py +0 -0
  171. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/py.typed +0 -0
  172. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/rankings/__init__.py +0 -0
  173. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/rankings/models/__init__.py +0 -0
  174. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/rankings/models/pydantic/__init__.py +0 -0
  175. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/rankings/models/pydantic/requests.py +0 -0
  176. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/rankings/models/pydantic/responses.py +0 -0
  177. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/rankings/resource.py +0 -0
  178. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/ratings/__init__.py +0 -0
  179. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/ratings/models/__init__.py +0 -0
  180. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/ratings/models/pydantic/__init__.py +0 -0
  181. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/ratings/models/pydantic/requests.py +0 -0
  182. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/ratings/models/pydantic/responses.py +0 -0
  183. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/ratings/resource.py +0 -0
  184. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/recruiting/__init__.py +0 -0
  185. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/recruiting/models/__init__.py +0 -0
  186. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/recruiting/models/pydantic/__init__.py +0 -0
  187. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/recruiting/models/pydantic/requests.py +0 -0
  188. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/recruiting/models/pydantic/responses.py +0 -0
  189. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/recruiting/resource.py +0 -0
  190. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/retry.py +0 -0
  191. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/stats/__init__.py +0 -0
  192. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/stats/models/__init__.py +0 -0
  193. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/stats/models/pydantic/__init__.py +0 -0
  194. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/stats/models/pydantic/requests.py +0 -0
  195. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/stats/models/pydantic/responses.py +0 -0
  196. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/stats/resource.py +0 -0
  197. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/teams/__init__.py +0 -0
  198. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/teams/models/__init__.py +0 -0
  199. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/teams/models/pydantic/__init__.py +0 -0
  200. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/teams/models/pydantic/identity.py +0 -0
  201. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/teams/models/pydantic/requests.py +0 -0
  202. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/teams/models/pydantic/responses.py +0 -0
  203. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/teams/resource.py +0 -0
  204. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/venues/__init__.py +0 -0
  205. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/venues/models/__init__.py +0 -0
  206. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/venues/models/pydantic/__init__.py +0 -0
  207. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/venues/models/pydantic/identity.py +0 -0
  208. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/venues/models/pydantic/responses.py +0 -0
  209. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data/venues/resource.py +0 -0
  210. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data.egg-info/dependency_links.txt +0 -0
  211. {cfb_data-0.5.1 → cfb_data-0.7.0}/cfb_data/cfb_data.egg-info/top_level.txt +0 -0
  212. {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.5.1
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<8,>=7.1; extra == "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.5.1 is a pre-alpha Python toolkit for exploring the public
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 troubleshooting.
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.5.1 is a pre-alpha Python toolkit for exploring the public
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 troubleshooting.
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, transport: _HTTPTransport, cache_coordinator: CacheCoordinator
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._cache_coordinator.execute(
76
+ return await self._execute(
58
77
  endpoint=endpoint,
59
- parameters=_serialize_request(endpoint, request),
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._cache_coordinator.execute(
98
+ return await self._execute(
80
99
  endpoint=endpoint,
81
- parameters=_serialize_request(endpoint, request),
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._cache_coordinator.execute(
120
+ return await self._execute(
102
121
  endpoint=endpoint,
103
- parameters=_serialize_request(endpoint, request),
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
- await self._retry_after_failure(
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
- category=failure.category,
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
- async def _retry_after_failure(
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
- category: str,
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
- """Log and apply one client-selected full-jitter delay."""
393
- delay = self._backoff_delay(attempt)
394
- _LOGGER.debug(
395
- "Retrying CFBD GET endpoint=%s category=%s attempt=%d delay=%.3f",
396
- endpoint,
397
- category,
398
- attempt,
399
- delay,
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."""