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.
- {dojozero-0.2.2 → dojozero-0.3.0}/PKG-INFO +1 -1
- {dojozero-0.2.2 → dojozero-0.3.0}/pyproject.toml +1 -1
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_cache.py +74 -4
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_config.py +2 -1
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_endpoints.py +147 -9
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_models.py +49 -1
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_redis_reader.py +25 -1
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_server.py +232 -14
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_utils.py +285 -27
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/betting/_metadata.py +13 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/betting/_models.py +8 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/cli.py +293 -15
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_models.py +2 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_tracing.py +45 -9
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_trial_orchestrator.py +26 -8
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_scheduler.py +180 -45
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_server.py +252 -56
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_trial_manager.py +203 -14
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_types.py +1 -1
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/__init__.py +3 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_config.py +1 -1
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_hub.py +15 -1
- dojozero-0.3.0/src/dojozero/data/_sls_source.py +372 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_stores.py +14 -7
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_subscriptions.py +46 -10
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/gateway/__init__.py +2 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/gateway/_models.py +12 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/gateway/_server.py +29 -40
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nba/_trial.py +1 -1
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/ncaa/_trial.py +1 -1
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nfl/_trial.py +1 -1
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/sync_service/_redis_client.py +110 -14
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/sync_service/_sync.py +231 -22
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/utils/oss.py +20 -1
- dojozero-0.3.0/tests/test_backtest_cache_naming.py +157 -0
- dojozero-0.3.0/tests/test_cli_dev_span_start.py +23 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_oss.py +318 -1
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_scheduler.py +2 -2
- dojozero-0.3.0/tests/test_sls_source.py +788 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/.gitignore +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/README.md +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/_optional_alicloud.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/agents/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/agents/_config.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/agents/_social_board.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/agents/_toolkit.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/agents/_trial_utils.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/_constants.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/arena_server/get_snapshot.sh +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/betting/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/betting/_agent.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/betting/_broker.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/betting/_config.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/betting/_formatters.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_actors.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_base.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_credentials.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_filesystem_orchestrator_store.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_metadata.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_registry.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_runtime.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/core/_types.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_cluster.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_game_discovery.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_gateway_routing.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/dashboard_server/_jsonl_utils.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_backtest.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_context.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_factory.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_game_info.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_models.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_processors.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_streams.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/_utils.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/espn/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/espn/_api.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/espn/_state_tracker.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/espn/_stats_events.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/espn/_stats_fetcher.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/espn/_utils.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nba/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nba/_api.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nba/_events.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nba/_factory.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nba/_state_tracker.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nba/_store.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nba/_utils.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/ncaa/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/ncaa/_api.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/ncaa/_events.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/ncaa/_factory.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/ncaa/_state_tracker.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/ncaa/_store.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/ncaa/_utils.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nfl/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nfl/_api.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nfl/_events.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nfl/_factory.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nfl/_state_tracker.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nfl/_store.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/nfl/_utils.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/polymarket/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/polymarket/_api.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/polymarket/_events.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/polymarket/_factory.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/polymarket/_models.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/polymarket/_store.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/socialmedia/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/socialmedia/_api.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/socialmedia/_events.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/socialmedia/_factory.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/socialmedia/_formatters.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/socialmedia/_store.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/socialmedia/_watchlist.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/websearch/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/websearch/_api.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/websearch/_events.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/websearch/_factory.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/websearch/_formatters.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/data/websearch/_store.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/gateway/_adapter.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/gateway/_auth.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/gateway/_rate_limit.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/gateway/_sse.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nba/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nba/_agent.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nba/_datastream.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nba/_formatters.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/ncaa/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/ncaa/_agent.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/ncaa/_datastream.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/ncaa/_formatters.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nfl/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nfl/_agent.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nfl/_datastream.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/nfl/_formatters.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/ray_runtime/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/ray_runtime/_impl.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/sync_service/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/sync_service/main.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/utils/__init__.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/src/dojozero/utils/time.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/conftest.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_agent_configs.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_agent_event_throttle.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_arena_event_deserialization.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_arena_replay_seek.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_arena_span_grouping.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_betting_formatters.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_broker.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_cli_agents.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_cluster.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_dashboard.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_data_hub.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_data_nba.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_data_nfl.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_data_polymarket.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_game_state_tracker.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_gateway.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_metadata.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_nba_formatters.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_nba_moneyline_agent.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_nfl_formatters.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_nfl_moneyline_agent.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_registry.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_social_board.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_socialmedia_events.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_span_models.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_stats_insight_events.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_subscriptions.py +0 -0
- {dojozero-0.2.2 → dojozero-0.3.0}/tests/test_websearch_events.py +0 -0
- {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.
|
|
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.
|
|
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,
|
|
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=
|
|
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
|
|
397
|
+
"""Get agent leaderboard ranked by specified field.
|
|
368
398
|
|
|
369
|
-
Returns agents sorted by
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
#
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
452
|
+
# 3. Limit
|
|
383
453
|
if limit is not None:
|
|
384
454
|
leaderboard = leaderboard[:limit]
|
|
385
455
|
|
|
386
|
-
|
|
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()
|