dojozero 0.2.2__tar.gz → 0.3.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 (176) hide show
  1. {dojozero-0.2.2 → dojozero-0.3.0}/PKG-INFO +1 -1
  2. {dojozero-0.2.2 → dojozero-0.3.0}/pyproject.toml +1 -1
  3. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_cache.py +74 -4
  4. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_config.py +2 -1
  5. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_endpoints.py +147 -9
  6. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_models.py +49 -1
  7. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_redis_reader.py +25 -1
  8. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_server.py +232 -14
  9. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_utils.py +285 -27
  10. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/betting/_metadata.py +13 -0
  11. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/betting/_models.py +8 -0
  12. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/cli.py +293 -15
  13. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_models.py +2 -0
  14. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_tracing.py +45 -9
  15. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_trial_orchestrator.py +26 -8
  16. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_scheduler.py +180 -45
  17. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_server.py +252 -56
  18. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_trial_manager.py +203 -14
  19. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_types.py +1 -1
  20. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/__init__.py +3 -0
  21. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_config.py +1 -1
  22. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_hub.py +15 -1
  23. dojozero-0.3.0/src/dojozero/data/_sls_source.py +372 -0
  24. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_stores.py +14 -7
  25. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_subscriptions.py +46 -10
  26. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/gateway/__init__.py +2 -0
  27. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/gateway/_models.py +12 -0
  28. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/gateway/_server.py +29 -40
  29. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nba/_trial.py +1 -1
  30. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/ncaa/_trial.py +1 -1
  31. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nfl/_trial.py +1 -1
  32. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/sync_service/_redis_client.py +110 -14
  33. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/sync_service/_sync.py +231 -22
  34. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/utils/oss.py +20 -1
  35. dojozero-0.3.0/tests/test_backtest_cache_naming.py +157 -0
  36. dojozero-0.3.0/tests/test_cli_dev_span_start.py +23 -0
  37. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_oss.py +318 -1
  38. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_scheduler.py +2 -2
  39. dojozero-0.3.0/tests/test_sls_source.py +788 -0
  40. {dojozero-0.2.2 → dojozero-0.3.0}/.gitignore +0 -0
  41. {dojozero-0.2.2 → dojozero-0.3.0}/README.md +0 -0
  42. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/__init__.py +0 -0
  43. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/_optional_alicloud.py +0 -0
  44. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/agents/__init__.py +0 -0
  45. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/agents/_config.py +0 -0
  46. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/agents/_social_board.py +0 -0
  47. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/agents/_toolkit.py +0 -0
  48. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/agents/_trial_utils.py +0 -0
  49. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/__init__.py +0 -0
  50. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_constants.py +0 -0
  51. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/get_snapshot.sh +0 -0
  52. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/betting/__init__.py +0 -0
  53. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/betting/_agent.py +0 -0
  54. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/betting/_broker.py +0 -0
  55. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/betting/_config.py +0 -0
  56. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/betting/_formatters.py +0 -0
  57. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/__init__.py +0 -0
  58. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_actors.py +0 -0
  59. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_base.py +0 -0
  60. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_credentials.py +0 -0
  61. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_filesystem_orchestrator_store.py +0 -0
  62. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_metadata.py +0 -0
  63. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_registry.py +0 -0
  64. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_runtime.py +0 -0
  65. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_types.py +0 -0
  66. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/__init__.py +0 -0
  67. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_cluster.py +0 -0
  68. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_game_discovery.py +0 -0
  69. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_gateway_routing.py +0 -0
  70. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_jsonl_utils.py +0 -0
  71. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_backtest.py +0 -0
  72. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_context.py +0 -0
  73. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_factory.py +0 -0
  74. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_game_info.py +0 -0
  75. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_models.py +0 -0
  76. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_processors.py +0 -0
  77. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_streams.py +0 -0
  78. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_utils.py +0 -0
  79. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/espn/__init__.py +0 -0
  80. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/espn/_api.py +0 -0
  81. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/espn/_state_tracker.py +0 -0
  82. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/espn/_stats_events.py +0 -0
  83. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/espn/_stats_fetcher.py +0 -0
  84. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/espn/_utils.py +0 -0
  85. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nba/__init__.py +0 -0
  86. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nba/_api.py +0 -0
  87. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nba/_events.py +0 -0
  88. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nba/_factory.py +0 -0
  89. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nba/_state_tracker.py +0 -0
  90. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nba/_store.py +0 -0
  91. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nba/_utils.py +0 -0
  92. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/ncaa/__init__.py +0 -0
  93. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/ncaa/_api.py +0 -0
  94. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/ncaa/_events.py +0 -0
  95. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/ncaa/_factory.py +0 -0
  96. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/ncaa/_state_tracker.py +0 -0
  97. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/ncaa/_store.py +0 -0
  98. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/ncaa/_utils.py +0 -0
  99. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nfl/__init__.py +0 -0
  100. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nfl/_api.py +0 -0
  101. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nfl/_events.py +0 -0
  102. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nfl/_factory.py +0 -0
  103. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nfl/_state_tracker.py +0 -0
  104. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nfl/_store.py +0 -0
  105. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nfl/_utils.py +0 -0
  106. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/polymarket/__init__.py +0 -0
  107. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/polymarket/_api.py +0 -0
  108. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/polymarket/_events.py +0 -0
  109. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/polymarket/_factory.py +0 -0
  110. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/polymarket/_models.py +0 -0
  111. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/polymarket/_store.py +0 -0
  112. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/socialmedia/__init__.py +0 -0
  113. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/socialmedia/_api.py +0 -0
  114. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/socialmedia/_events.py +0 -0
  115. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/socialmedia/_factory.py +0 -0
  116. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/socialmedia/_formatters.py +0 -0
  117. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/socialmedia/_store.py +0 -0
  118. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/socialmedia/_watchlist.py +0 -0
  119. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/websearch/__init__.py +0 -0
  120. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/websearch/_api.py +0 -0
  121. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/websearch/_events.py +0 -0
  122. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/websearch/_factory.py +0 -0
  123. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/websearch/_formatters.py +0 -0
  124. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/websearch/_store.py +0 -0
  125. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/gateway/_adapter.py +0 -0
  126. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/gateway/_auth.py +0 -0
  127. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/gateway/_rate_limit.py +0 -0
  128. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/gateway/_sse.py +0 -0
  129. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nba/__init__.py +0 -0
  130. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nba/_agent.py +0 -0
  131. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nba/_datastream.py +0 -0
  132. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nba/_formatters.py +0 -0
  133. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/ncaa/__init__.py +0 -0
  134. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/ncaa/_agent.py +0 -0
  135. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/ncaa/_datastream.py +0 -0
  136. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/ncaa/_formatters.py +0 -0
  137. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nfl/__init__.py +0 -0
  138. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nfl/_agent.py +0 -0
  139. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nfl/_datastream.py +0 -0
  140. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nfl/_formatters.py +0 -0
  141. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/ray_runtime/__init__.py +0 -0
  142. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/ray_runtime/_impl.py +0 -0
  143. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/sync_service/__init__.py +0 -0
  144. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/sync_service/main.py +0 -0
  145. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/utils/__init__.py +0 -0
  146. {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/utils/time.py +0 -0
  147. {dojozero-0.2.2 → dojozero-0.3.0}/tests/conftest.py +0 -0
  148. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_agent_configs.py +0 -0
  149. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_agent_event_throttle.py +0 -0
  150. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_arena_event_deserialization.py +0 -0
  151. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_arena_replay_seek.py +0 -0
  152. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_arena_span_grouping.py +0 -0
  153. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_betting_formatters.py +0 -0
  154. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_broker.py +0 -0
  155. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_cli_agents.py +0 -0
  156. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_cluster.py +0 -0
  157. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_dashboard.py +0 -0
  158. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_data_hub.py +0 -0
  159. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_data_nba.py +0 -0
  160. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_data_nfl.py +0 -0
  161. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_data_polymarket.py +0 -0
  162. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_game_state_tracker.py +0 -0
  163. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_gateway.py +0 -0
  164. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_metadata.py +0 -0
  165. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_nba_formatters.py +0 -0
  166. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_nba_moneyline_agent.py +0 -0
  167. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_nfl_formatters.py +0 -0
  168. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_nfl_moneyline_agent.py +0 -0
  169. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_registry.py +0 -0
  170. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_social_board.py +0 -0
  171. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_socialmedia_events.py +0 -0
  172. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_span_models.py +0 -0
  173. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_stats_insight_events.py +0 -0
  174. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_subscriptions.py +0 -0
  175. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_websearch_events.py +0 -0
  176. {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_websearch_formatters.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dojozero
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: A platform for running AI agents on realtime sport data and make predictions about game outcomes.
5
5
  Project-URL: Homepage, https://github.com/agentscope-ai/DojoZero
6
6
  Project-URL: Repository, https://github.com/agentscope-ai/DojoZero
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dojozero"
7
- version = "0.2.2"
7
+ version = "0.3.0"
8
8
  description = "A platform for running AI agents on realtime sport data and make predictions about game outcomes."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -63,6 +63,7 @@ from dojozero.arena_server._config import ArenaServerConfig # noqa: E402
63
63
 
64
64
  DEFAULT_CACHE_CONFIG = CacheConfig()
65
65
  CACHEABLE_LEAGUES: frozenset[str] = frozenset({"NBA", "NFL"})
66
+ LEADERBOARD_PERIODS: tuple[str, ...] = ("7d", "14d", "30d")
66
67
 
67
68
 
68
69
  @dataclass
@@ -124,8 +125,13 @@ class LandingPageCache:
124
125
  _stats_by_league: dict[str, CacheEntry] = field(default_factory=dict)
125
126
  _games_by_league: dict[str, CacheEntry] = field(default_factory=dict)
126
127
  _leaderboard_by_league: dict[str, CacheEntry] = field(default_factory=dict)
128
+ _leaderboard_by_period: dict[str, CacheEntry] = field(default_factory=dict)
129
+ _leaderboard_by_league_period: dict[str, CacheEntry] = field(default_factory=dict)
127
130
  _agent_actions_by_league: dict[str, CacheEntry] = field(default_factory=dict)
128
131
 
132
+ # Agent bets index - per-agent bet records (agent_id -> list[BetRecord])
133
+ _agent_bets_index: dict[str, list] = field(default_factory=dict)
134
+
129
135
  # Agent info cache - single source of truth (agent_id -> AgentInfo)
130
136
  _agent_info: dict[str, AgentInfo] = field(default_factory=dict)
131
137
 
@@ -185,9 +191,32 @@ class LandingPageCache:
185
191
  return None
186
192
 
187
193
  def get_leaderboard(
188
- self, league: str | None = None
194
+ self, league: str | None = None, period: str | None = None
189
195
  ) -> list[LeaderboardEntry] | None:
190
- """Get cached leaderboard. Returns None if not cached."""
196
+ """Get cached leaderboard. Returns None if not cached.
197
+
198
+ Args:
199
+ league: Optional league filter (e.g. "NBA", "NFL").
200
+ period: Optional period filter ("7d", "14d", "30d").
201
+ None or "all" returns the full leaderboard.
202
+ """
203
+ # Normalize period: None and "all" both mean full leaderboard
204
+ effective_period = period if period and period != "all" else None
205
+
206
+ if effective_period:
207
+ if league:
208
+ # Per-league + per-period
209
+ key = f"{league.upper()}:{effective_period}"
210
+ entry = self._leaderboard_by_league_period.get(key)
211
+ if entry is not None and entry.is_valid():
212
+ return entry.data
213
+ return None
214
+ # Global + per-period
215
+ entry = self._leaderboard_by_period.get(effective_period)
216
+ if entry is not None and entry.is_valid():
217
+ return entry.data
218
+ return None
219
+
191
220
  if league:
192
221
  league_key = league.upper()
193
222
  if league_key in CACHEABLE_LEAGUES:
@@ -247,6 +276,15 @@ class LandingPageCache:
247
276
  """Get all cached agent info (for batch operations)."""
248
277
  return self._agent_info
249
278
 
279
+ def get_agent_bets_index(self) -> dict[str, list]:
280
+ """Get cached agent bets index (agent_id -> sorted list[BetRecord])."""
281
+ return self._agent_bets_index
282
+
283
+ def set_agent_bets_index(self, data: dict[str, list]) -> None:
284
+ """Set agent bets index (overwrites previous)."""
285
+ self._agent_bets_index = data
286
+ LOGGER.debug("Cache SET: agent_bets_index (%d agents)", len(data))
287
+
250
288
  def get_total_agents(self) -> int:
251
289
  """Get total number of cached agents."""
252
290
  return len(self._agent_info)
@@ -372,13 +410,37 @@ class LandingPageCache:
372
410
  LOGGER.debug("Cache SET: games (global)")
373
411
 
374
412
  def set_leaderboard(
375
- self, data: list[LeaderboardEntry], league: str | None = None
413
+ self,
414
+ data: list[LeaderboardEntry],
415
+ league: str | None = None,
416
+ period: str | None = None,
376
417
  ) -> None:
377
- """Set leaderboard cache (overwrites previous)."""
418
+ """Set leaderboard cache (overwrites previous).
419
+
420
+ Args:
421
+ data: Leaderboard entries.
422
+ league: Optional league filter.
423
+ period: Optional period filter ("7d", "14d", "30d").
424
+ None or "all" sets the full leaderboard.
425
+ """
378
426
  entry = CacheEntry(
379
427
  data=data,
380
428
  expires_at=time.time() + self.config.max_cache_ttl,
381
429
  )
430
+ effective_period = period if period and period != "all" else None
431
+
432
+ if effective_period:
433
+ if league:
434
+ key = f"{league.upper()}:{effective_period}"
435
+ self._leaderboard_by_league_period[key] = entry
436
+ LOGGER.debug(
437
+ "Cache SET: leaderboard[%s][%s]", league.upper(), effective_period
438
+ )
439
+ return
440
+ self._leaderboard_by_period[effective_period] = entry
441
+ LOGGER.debug("Cache SET: leaderboard[period=%s]", effective_period)
442
+ return
443
+
382
444
  if league:
383
445
  league_key = league.upper()
384
446
  if league_key in CACHEABLE_LEAGUES:
@@ -436,6 +498,8 @@ class LandingPageCache:
436
498
  self._stats_by_league.clear()
437
499
  self._games_by_league.clear()
438
500
  self._leaderboard_by_league.clear()
501
+ self._leaderboard_by_period.clear()
502
+ self._leaderboard_by_league_period.clear()
439
503
  self._agent_actions_by_league.clear()
440
504
  # Clear agent info cache
441
505
  self._agent_info.clear()
@@ -485,6 +549,12 @@ class LandingPageCache:
485
549
  "leaderboard": _league_cache_info(self._leaderboard_by_league),
486
550
  "agent_actions": _league_cache_info(self._agent_actions_by_league),
487
551
  },
552
+ "caches_by_period": {
553
+ "leaderboard": _league_cache_info(self._leaderboard_by_period),
554
+ "leaderboard_by_league": _league_cache_info(
555
+ self._leaderboard_by_league_period
556
+ ),
557
+ },
488
558
  }
489
559
 
490
560
 
@@ -81,7 +81,8 @@ class SLSConfig(BaseModel):
81
81
 
82
82
  page_size: int = Field(default=100, description="Page size for SLS queries")
83
83
  max_total: int = Field(
84
- default=100000, description="Safety limit for total rows fetched"
84
+ default=1000000,
85
+ description="Safety limit for total rows fetched per paginated query",
85
86
  )
86
87
 
87
88
 
@@ -40,6 +40,9 @@ from ._server import (
40
40
  TrialReplayController,
41
41
  get_server_state,
42
42
  )
43
+ from ._utils import (
44
+ _compute_agent_profile,
45
+ )
43
46
  from ._utils import _load_replay_data
44
47
 
45
48
  LOGGER = logging.getLogger("dojozero.arena_server.endpoints")
@@ -363,31 +366,166 @@ def register_rest_endpoints(app: FastAPI) -> None:
363
366
  ge=1,
364
367
  le=1000,
365
368
  ),
369
+ page: int = Query(
370
+ default=1,
371
+ description="Page number (1-based).",
372
+ ge=1,
373
+ ),
374
+ page_size: int = Query(
375
+ default=20,
376
+ description="Number of entries per page.",
377
+ ge=1,
378
+ le=100,
379
+ ),
380
+ period: str = Query(
381
+ default="all",
382
+ description="Time period filter: 'all', '7d', '14d', '30d'.",
383
+ ),
384
+ agent_type: str | None = Query(
385
+ default=None,
386
+ description="Agent type filter: 'built_in' or 'external'.",
387
+ ),
388
+ sort_by: str = Query(
389
+ default="winnings",
390
+ description="Sort field: 'winnings', 'win_rate', 'roi', 'total_bets'.",
391
+ ),
392
+ sort_order: str = Query(
393
+ default="desc",
394
+ description="Sort direction: 'asc' or 'desc'.",
395
+ ),
366
396
  ) -> JSONResponse:
367
- """Get agent leaderboard ranked by winnings.
397
+ """Get agent leaderboard ranked by specified field.
368
398
 
369
- Returns agents sorted by total winnings with win rate and ROI.
370
- Data is served from cache (background refresh keeps it fresh).
371
- On cache miss, triggers on-demand refresh.
399
+ Returns agents sorted by the requested field with win rate and ROI.
400
+ Supports period-based filtering (pre-computed: all/7d/14d/30d),
401
+ agent type filtering, custom sorting, and pagination.
372
402
  """
373
403
  state = get_server_state()
374
404
  refresher = state.refresher
375
405
  assert refresher is not None, "BackgroundRefresher not initialized"
376
406
 
377
- # Get leaderboard from cache, or refresh on demand
378
- leaderboard = state.cache.get_leaderboard(league=league)
407
+ # Validate period
408
+ valid_periods = {"all", "7d", "14d", "30d"}
409
+ if period not in valid_periods:
410
+ return JSONResponse(
411
+ status_code=400,
412
+ content={
413
+ "error": f"Invalid period '{period}'. Must be one of: {', '.join(sorted(valid_periods))}"
414
+ },
415
+ )
416
+
417
+ # All paths now read from pre-computed cache
418
+ effective_period = period if period != "all" else None
419
+ leaderboard = state.cache.get_leaderboard(
420
+ league=league,
421
+ period=effective_period,
422
+ )
379
423
  if leaderboard is None:
380
- leaderboard = await refresher.refresh_leaderboard_on_demand(league=league)
424
+ # Fallback: try on-demand refresh for the base (all) leaderboard
425
+ if effective_period is None:
426
+ leaderboard = await refresher.refresh_leaderboard_on_demand(
427
+ league=league
428
+ )
429
+ else:
430
+ leaderboard = []
431
+
432
+ # --- Unified post-processing: filter → sort → rank → limit → paginate ---
433
+
434
+ # 1. Filter by agent_type
435
+ if agent_type == "external":
436
+ leaderboard = [e for e in leaderboard if e.agent.is_external]
437
+ elif agent_type == "built_in":
438
+ leaderboard = [e for e in leaderboard if not e.agent.is_external]
439
+
440
+ # 2. Sort (cached path is already sorted by winnings desc; re-sort only when needed)
441
+ if sort_by != "winnings" or sort_order != "desc":
442
+ sort_key_map = {
443
+ "winnings": lambda x: x.winnings,
444
+ "win_rate": lambda x: x.win_rate,
445
+ "roi": lambda x: x.roi,
446
+ "sharpe": lambda x: x.sharpe,
447
+ "total_bets": lambda x: x.total_bets,
448
+ }
449
+ key_fn = sort_key_map.get(sort_by, sort_key_map["winnings"])
450
+ leaderboard = sorted(leaderboard, key=key_fn, reverse=(sort_order != "asc"))
381
451
 
382
- # Apply limit only if specified
452
+ # 3. Limit
383
453
  if limit is not None:
384
454
  leaderboard = leaderboard[:limit]
385
455
 
386
- response = LeaderboardResponse(leaderboard=leaderboard)
456
+ total = len(leaderboard)
457
+
458
+ # 4. Paginate + assign global rank (once)
459
+ start_idx = (page - 1) * page_size
460
+ page_entries = [
461
+ e.model_copy(update={"rank": start_idx + i + 1})
462
+ for i, e in enumerate(leaderboard[start_idx : start_idx + page_size])
463
+ ]
464
+
465
+ response = LeaderboardResponse(
466
+ leaderboard=page_entries,
467
+ total=total,
468
+ page=page,
469
+ page_size=page_size,
470
+ )
387
471
  return JSONResponse(
388
472
  content=response.model_dump(by_alias=state.by_alias),
389
473
  )
390
474
 
475
+ @app.get("/api/agent/{agent_id}/profile")
476
+ async def get_agent_profile(
477
+ agent_id: str,
478
+ page: int = Query(
479
+ default=1,
480
+ description="Page number for bet history (1-based).",
481
+ ge=1,
482
+ ),
483
+ page_size: int = Query(
484
+ default=20,
485
+ description="Number of bet records per page.",
486
+ ge=1,
487
+ le=100,
488
+ ),
489
+ ) -> JSONResponse:
490
+ """Get agent profile with stats and paginated bet history.
491
+
492
+ Returns agent info, aggregate stats, and a paginated list of
493
+ individual bets sorted by game_date descending.
494
+ """
495
+ state = get_server_state()
496
+ refresher = state.refresher
497
+ assert refresher is not None, "BackgroundRefresher not initialized"
498
+
499
+ # Stats from leaderboard cache, bets from bets index — zero recomputation
500
+ leaderboard = state.cache.get_leaderboard() or []
501
+ agent_bets_index = state.cache.get_agent_bets_index()
502
+ agent_info_cache = state.cache.get_all_agent_info()
503
+
504
+ # Check if agent exists in any cache; return 404 if unknown
505
+ agent_known = (
506
+ agent_id in agent_info_cache
507
+ or agent_id in agent_bets_index
508
+ or any(e.agent.agent_id == agent_id for e in leaderboard)
509
+ )
510
+ if not agent_known:
511
+ return JSONResponse(
512
+ status_code=404,
513
+ content={"detail": f"Agent '{agent_id}' not found"},
514
+ )
515
+
516
+ profile = _compute_agent_profile(
517
+ agent_id=agent_id,
518
+ leaderboard=leaderboard,
519
+ agent_bets_index=agent_bets_index,
520
+ agent_info_cache=agent_info_cache,
521
+ page=page,
522
+ page_size=page_size,
523
+ )
524
+
525
+ return JSONResponse(
526
+ content=profile.model_dump(by_alias=state.by_alias),
527
+ )
528
+
391
529
  @app.get("/api/agent-actions")
392
530
  async def get_agent_actions(
393
531
  limit: int = Query(
@@ -16,6 +16,48 @@ from dojozero.betting._models import AgentInfo
16
16
  from dojozero.core._models import AgentAction, LeaderboardEntry
17
17
  from dojozero.data._models import TeamIdentity
18
18
 
19
+
20
+ class BetRecord(BaseModel):
21
+ """A single bet record for agent profile display."""
22
+
23
+ model_config = ConfigDict(frozen=True, populate_by_name=True)
24
+
25
+ trial_id: str = Field(serialization_alias="trialId")
26
+ league: str = ""
27
+ home_team: str = Field(default="", serialization_alias="homeTeam")
28
+ away_team: str = Field(default="", serialization_alias="awayTeam")
29
+ game_date: str = Field(default="", serialization_alias="gameDate")
30
+ selection: str = ""
31
+ amount: float = 0.0
32
+ result: str = "pending" # "win", "loss", "pending"
33
+ payout: float = 0.0
34
+
35
+
36
+ class AgentProfileStats(BaseModel):
37
+ """Aggregate stats for an agent profile."""
38
+
39
+ model_config = ConfigDict(frozen=True, populate_by_name=True)
40
+
41
+ winnings: float = 0.0
42
+ win_rate: float = Field(default=0.0, serialization_alias="winRate")
43
+ total_bets: int = Field(default=0, serialization_alias="totalBets")
44
+ roi: float = 0.0
45
+
46
+
47
+ class AgentProfileResponse(BaseModel):
48
+ """Response for GET /api/agent/{agent_id}/profile."""
49
+
50
+ model_config = ConfigDict(frozen=True, populate_by_name=True)
51
+
52
+ agent: AgentInfo
53
+ stats: AgentProfileStats
54
+ created_at: str | None = Field(default=None, serialization_alias="createdAt")
55
+ bets: list[BetRecord] = Field(default_factory=list)
56
+ total_bets_count: int = Field(default=0, serialization_alias="totalBetsCount")
57
+ page: int = 1
58
+ page_size: int = Field(default=20, serialization_alias="pageSize")
59
+
60
+
19
61
  # ============================================================================
20
62
  # Type Aliases
21
63
  # ============================================================================
@@ -123,9 +165,12 @@ class LandingResponse(BaseModel):
123
165
  class LeaderboardResponse(BaseModel):
124
166
  """Response for /api/leaderboard."""
125
167
 
126
- model_config = ConfigDict(frozen=True)
168
+ model_config = ConfigDict(frozen=True, populate_by_name=True)
127
169
 
128
170
  leaderboard: list[LeaderboardEntry] = Field(default_factory=list)
171
+ total: int = 0
172
+ page: int = 1
173
+ page_size: int = Field(default=20, serialization_alias="pageSize")
129
174
 
130
175
 
131
176
  class BetSummary(BaseModel):
@@ -261,6 +306,9 @@ class ReplayResponse(BaseModel):
261
306
  __all__ = [
262
307
  # API Response Models
263
308
  "AgentActionsResponse",
309
+ "AgentProfileResponse",
310
+ "AgentProfileStats",
311
+ "BetRecord",
264
312
  "BetSummary",
265
313
  "GameCardData",
266
314
  "GamesResponse",
@@ -17,9 +17,10 @@ from datetime import datetime
17
17
 
18
18
  from dojozero.arena_server._cache import (
19
19
  CACHEABLE_LEAGUES,
20
+ LEADERBOARD_PERIODS,
20
21
  LandingPageCache,
21
22
  )
22
- from dojozero.arena_server._models import GamesResponse, StatsResponse
23
+ from dojozero.arena_server._models import BetRecord, GamesResponse, StatsResponse
23
24
  from dojozero.betting import AgentInfo
24
25
  from dojozero.core import AgentAction, LeaderboardEntry
25
26
  from dojozero.core._tracing import SpanData
@@ -161,6 +162,21 @@ class RedisReader:
161
162
  lb = [LeaderboardEntry.model_validate(e) for e in lb_data]
162
163
  self.cache.set_leaderboard(lb, league=league)
163
164
 
165
+ # Period leaderboards (global + per-league)
166
+ for period in LEADERBOARD_PERIODS:
167
+ p_data = await self.redis_client.get_leaderboard(period=period)
168
+ if p_data:
169
+ p_lb = [LeaderboardEntry.model_validate(e) for e in p_data]
170
+ self.cache.set_leaderboard(p_lb, period=period)
171
+ for league in CACHEABLE_LEAGUES:
172
+ lp_data = await self.redis_client.get_leaderboard(
173
+ league=league,
174
+ period=period,
175
+ )
176
+ if lp_data:
177
+ lp_lb = [LeaderboardEntry.model_validate(e) for e in lp_data]
178
+ self.cache.set_leaderboard(lp_lb, league=league, period=period)
179
+
164
180
  # Agent actions (global + per-league)
165
181
  actions_data = await self.redis_client.get_agent_actions(league=None)
166
182
  if actions_data:
@@ -197,6 +213,14 @@ class RedisReader:
197
213
  g = GamesResponse.model_validate(g_data)
198
214
  self.cache.set_games(g, league=league)
199
215
 
216
+ # Agent bets index
217
+ bets_index_data = await self.redis_client.get_agent_bets_index()
218
+ if bets_index_data:
219
+ agent_bets_index: dict[str, list[BetRecord]] = {}
220
+ for aid, bets_list in bets_index_data.items():
221
+ agent_bets_index[aid] = [BetRecord.model_validate(b) for b in bets_list]
222
+ self.cache.set_agent_bets_index(agent_bets_index)
223
+
200
224
  # Live trials are derived from trial_info, not stored separately
201
225
  # Redis provides them for convenience but cache derives from trial_info
202
226
  await self.redis_client.get_live_trials()