coordinator-node 0.1.2__tar.gz → 0.1.4__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 (125) hide show
  1. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/PKG-INFO +1 -1
  2. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/Makefile +7 -1
  3. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/Dockerfile +1 -1
  4. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/Makefile +7 -1
  5. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/docker-compose.yml +37 -15
  6. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/db/init_db.py +41 -9
  7. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/db/repositories.py +1 -0
  8. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/db/session.py +7 -0
  9. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/services/score.py +8 -1
  10. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/pyproject.toml +1 -1
  11. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/tests/test_node_template_predict_service.py +66 -2
  12. coordinator_node-0.1.4/tests/test_node_template_repositories.py +86 -0
  13. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/uv.lock +1 -1
  14. coordinator_node-0.1.2/tests/test_node_template_repositories.py +0 -39
  15. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/.dev.env +0 -0
  16. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/.dockerignore +0 -0
  17. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/.gitignore +0 -0
  18. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/.local.env +0 -0
  19. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/.production.env +0 -0
  20. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/.python-version +0 -0
  21. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/Dockerfile +0 -0
  22. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/Makefile +0 -0
  23. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/README.md +0 -0
  24. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/SKILL.md +0 -0
  25. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/README.md +0 -0
  26. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/SKILL.md +0 -0
  27. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/challenge/README.md +0 -0
  28. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/challenge/SKILL.md +0 -0
  29. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/challenge/pyproject.toml +0 -0
  30. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/challenge/starter_challenge/__init__.py +0 -0
  31. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/challenge/starter_challenge/examples/README.md +0 -0
  32. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/challenge/starter_challenge/examples/__init__.py +0 -0
  33. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/challenge/starter_challenge/examples/mean_reversion_tracker.py +0 -0
  34. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/challenge/starter_challenge/examples/trend_following_tracker.py +0 -0
  35. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/challenge/starter_challenge/examples/volatility_regime_tracker.py +0 -0
  36. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/challenge/starter_challenge/schemas/README.md +0 -0
  37. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/challenge/starter_challenge/scoring.py +0 -0
  38. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/challenge/starter_challenge/tracker.py +0 -0
  39. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/.local.env +0 -0
  40. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/.local.env.example +0 -0
  41. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/.production.env.example +0 -0
  42. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/README.md +0 -0
  43. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/RUNBOOK.md +0 -0
  44. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/SKILL.md +0 -0
  45. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/config/README.md +0 -0
  46. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/config/callables.env +0 -0
  47. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/config/scheduled_prediction_configs.json +0 -0
  48. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/deployment/README.md +0 -0
  49. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/deployment/model-orchestrator-local/config/docker-entrypoint.sh +0 -0
  50. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/deployment/model-orchestrator-local/config/models.dev.yml +0 -0
  51. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/deployment/model-orchestrator-local/config/orchestrator.dev.yml +0 -0
  52. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/deployment/model-orchestrator-local/config/starter-submission/main.py +0 -0
  53. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/deployment/model-orchestrator-local/config/starter-submission/requirements.txt +0 -0
  54. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/deployment/model-orchestrator-local/config/starter-submission/tracker.py +0 -0
  55. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/deployment/report-ui/config/global-settings.json +0 -0
  56. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/deployment/report-ui/config/leaderboard-columns.json +0 -0
  57. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/deployment/report-ui/config/metrics-widgets.json +0 -0
  58. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/extensions/README.md +0 -0
  59. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/plugins/README.md +0 -0
  60. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/pyproject.toml +0 -0
  61. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/runtime_definitions/__init__.py +0 -0
  62. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/runtime_definitions/contracts.py +0 -0
  63. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/scripts/backfill.py +0 -0
  64. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/scripts/capture_runtime_logs.py +0 -0
  65. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/scripts/check_models.py +0 -0
  66. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/base/node/scripts/verify_e2e.py +0 -0
  67. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/clean-data.sh +0 -0
  68. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/__init__.py +0 -0
  69. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/config/__init__.py +0 -0
  70. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/config/extensions.py +0 -0
  71. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/config/runtime.py +0 -0
  72. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/contracts.py +0 -0
  73. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/db/__init__.py +0 -0
  74. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/db/feed_records.py +0 -0
  75. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/db/pg_notify.py +0 -0
  76. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/db/tables/__init__.py +0 -0
  77. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/db/tables/feed.py +0 -0
  78. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/db/tables/models.py +0 -0
  79. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/db/tables/pipeline.py +0 -0
  80. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/entities/__init__.py +0 -0
  81. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/entities/feed_record.py +0 -0
  82. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/entities/model.py +0 -0
  83. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/entities/prediction.py +0 -0
  84. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/extensions/__init__.py +0 -0
  85. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/extensions/callable_resolver.py +0 -0
  86. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/extensions/default_callables.py +0 -0
  87. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/feeds/__init__.py +0 -0
  88. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/feeds/base.py +0 -0
  89. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/feeds/contracts.py +0 -0
  90. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/feeds/providers/__init__.py +0 -0
  91. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/feeds/providers/binance.py +0 -0
  92. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/feeds/providers/pyth.py +0 -0
  93. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/feeds/registry.py +0 -0
  94. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/schemas/__init__.py +0 -0
  95. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/schemas/payload_contracts.py +0 -0
  96. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/services/__init__.py +0 -0
  97. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/services/backfill.py +0 -0
  98. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/services/feed_data.py +0 -0
  99. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/services/feed_reader.py +0 -0
  100. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/services/predict.py +0 -0
  101. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/services/realtime_predict.py +0 -0
  102. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/workers/__init__.py +0 -0
  103. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/workers/checkpoint_worker.py +0 -0
  104. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/workers/feed_data_worker.py +0 -0
  105. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/workers/predict_worker.py +0 -0
  106. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/workers/report_worker.py +0 -0
  107. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/coordinator_node/workers/score_worker.py +0 -0
  108. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/docker-compose.yml +0 -0
  109. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/docs/plans/2026-02-10-coordinator-restructure-design.md +0 -0
  110. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/docs/plans/2026-02-11-contract-based-architecture.md +0 -0
  111. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/docs/plans/2026-02-11-scoring-snapshots-checkpoints.md +0 -0
  112. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/docs/plans/2026-02-11-thin-node-cli-implementation-plan.md +0 -0
  113. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/docs/plans/2026-02-11-thin-node-cli-onboarding-design.md +0 -0
  114. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/mkdocs.yml +0 -0
  115. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/packs/README.md +0 -0
  116. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/scripts/verify_e2e.py +0 -0
  117. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/tests/test_backfill.py +0 -0
  118. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/tests/test_callable_resolver.py +0 -0
  119. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/tests/test_checkpoint_worker.py +0 -0
  120. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/tests/test_coordinator_core_schema.py +0 -0
  121. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/tests/test_coordinator_runtime_data_feeds.py +0 -0
  122. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/tests/test_core_entities.py +0 -0
  123. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/tests/test_node_template_report_worker.py +0 -0
  124. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/tests/test_node_template_score_service.py +0 -0
  125. {coordinator_node-0.1.2 → coordinator_node-0.1.4}/tests/test_prediction_lifecycle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coordinator-node
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Runtime engine for Crunch coordinator nodes
5
5
  Project-URL: Homepage, https://github.com/crunchdao/coordinator-node-starter
6
6
  Project-URL: Repository, https://github.com/crunchdao/coordinator-node-starter
@@ -1,5 +1,5 @@
1
1
  # Workspace-level convenience — proxies to node/
2
- .PHONY: deploy down logs verify-e2e check-models
2
+ .PHONY: deploy down logs verify-e2e check-models init-db reset-db
3
3
 
4
4
  deploy:
5
5
  cd node && make deploy
@@ -15,3 +15,9 @@ verify-e2e:
15
15
 
16
16
  check-models:
17
17
  cd node && make check-models
18
+
19
+ init-db:
20
+ cd node && make init-db
21
+
22
+ reset-db:
23
+ cd node && make reset-db
@@ -8,7 +8,7 @@ WORKDIR /app
8
8
 
9
9
  # Coordinator engine (from PyPI)
10
10
  # Bump this to force Docker to re-pull the latest package version
11
- ARG COORDINATOR_NODE_VERSION=0.1.2
11
+ ARG COORDINATOR_NODE_VERSION=0.1.4
12
12
  RUN pip install --no-cache-dir "coordinator-node>=${COORDINATOR_NODE_VERSION}"
13
13
 
14
14
  # Challenge package
@@ -1,10 +1,16 @@
1
1
  COMPOSE := docker compose -f docker-compose.yml --env-file .local.env
2
2
 
3
- .PHONY: deploy down logs logs-capture verify-e2e check-models starter platform backfill
3
+ .PHONY: deploy down logs logs-capture verify-e2e check-models starter platform backfill init-db reset-db
4
4
 
5
5
  deploy:
6
6
  $(COMPOSE) up -d --build
7
7
 
8
+ init-db:
9
+ $(COMPOSE) run --rm init-db
10
+
11
+ reset-db:
12
+ $(COMPOSE) run --rm reset-db
13
+
8
14
  down:
9
15
  $(COMPOSE) down
10
16
 
@@ -27,6 +27,28 @@ services:
27
27
  dockerfile: node/Dockerfile
28
28
  container_name: starter-challenge-init-db
29
29
  command: ["python", "-m", "coordinator_node.db.init_db"]
30
+ profiles: [init]
31
+ environment:
32
+ POSTGRES_HOST: ${POSTGRES_HOST:-postgres}
33
+ POSTGRES_PORT: ${POSTGRES_PORT:-5432}
34
+ POSTGRES_USER: ${POSTGRES_USER:-starter}
35
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-starter}
36
+ POSTGRES_DB: ${POSTGRES_DB:-starter}
37
+ SCHEDULED_PREDICTION_CONFIGS_PATH: /app/config/scheduled_prediction_configs.json
38
+ volumes:
39
+ - ./config/scheduled_prediction_configs.json:/app/config/scheduled_prediction_configs.json:ro
40
+ depends_on:
41
+ postgres:
42
+ condition: service_healthy
43
+ networks: [coordinator-net]
44
+
45
+ reset-db:
46
+ build:
47
+ context: ..
48
+ dockerfile: node/Dockerfile
49
+ container_name: starter-challenge-reset-db
50
+ command: ["python", "-m", "coordinator_node.db.init_db", "--reset"]
51
+ profiles: [reset]
30
52
  environment:
31
53
  POSTGRES_HOST: ${POSTGRES_HOST:-postgres}
32
54
  POSTGRES_PORT: ${POSTGRES_PORT:-5432}
@@ -62,12 +84,12 @@ services:
62
84
  FEED_BACKFILL_MINUTES: ${FEED_BACKFILL_MINUTES:-180}
63
85
  FEED_RECORD_TTL_DAYS: ${FEED_RECORD_TTL_DAYS:-90}
64
86
  FEED_RETENTION_CHECK_SECONDS: ${FEED_RETENTION_CHECK_SECONDS:-3600}
65
- PYTHONPATH: /app/challenge
87
+ PYTHONPATH: /app:/app/challenge
66
88
  volumes:
67
89
  - ../challenge:/app/challenge
68
90
  depends_on:
69
- init-db:
70
- condition: service_completed_successfully
91
+ postgres:
92
+ condition: service_healthy
71
93
  networks: [coordinator-net]
72
94
 
73
95
  predict-worker:
@@ -93,12 +115,12 @@ services:
93
115
  FEED_SUBJECTS: ${FEED_SUBJECTS:-BTC}
94
116
  FEED_KIND: ${FEED_KIND:-tick}
95
117
  FEED_GRANULARITY: ${FEED_GRANULARITY:-1s}
96
- PYTHONPATH: /app/challenge
118
+ PYTHONPATH: /app:/app/challenge
97
119
  volumes:
98
120
  - ../challenge:/app/challenge
99
121
  depends_on:
100
- init-db:
101
- condition: service_completed_successfully
122
+ postgres:
123
+ condition: service_healthy
102
124
  model-orchestrator:
103
125
  condition: service_started
104
126
  feed-data-worker:
@@ -124,12 +146,12 @@ services:
124
146
  FEED_KIND: ${FEED_KIND:-tick}
125
147
  FEED_GRANULARITY: ${FEED_GRANULARITY:-1s}
126
148
  SCORING_FUNCTION: ${SCORING_FUNCTION}
127
- PYTHONPATH: /app/challenge
149
+ PYTHONPATH: /app:/app/challenge
128
150
  volumes:
129
151
  - ../challenge:/app/challenge
130
152
  depends_on:
131
- init-db:
132
- condition: service_completed_successfully
153
+ postgres:
154
+ condition: service_healthy
133
155
  feed-data-worker:
134
156
  condition: service_started
135
157
  networks: [coordinator-net]
@@ -148,12 +170,12 @@ services:
148
170
  POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-starter}
149
171
  POSTGRES_DB: ${POSTGRES_DB:-starter}
150
172
  CHECKPOINT_INTERVAL_SECONDS: ${CHECKPOINT_INTERVAL_SECONDS:-604800}
151
- PYTHONPATH: /app/challenge
173
+ PYTHONPATH: /app:/app/challenge
152
174
  volumes:
153
175
  - ../challenge:/app/challenge
154
176
  depends_on:
155
- init-db:
156
- condition: service_completed_successfully
177
+ postgres:
178
+ condition: service_healthy
157
179
  networks: [coordinator-net]
158
180
 
159
181
  report-worker:
@@ -171,12 +193,12 @@ services:
171
193
  POSTGRES_USER: ${POSTGRES_USER:-starter}
172
194
  POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-starter}
173
195
  POSTGRES_DB: ${POSTGRES_DB:-starter}
174
- PYTHONPATH: /app/challenge
196
+ PYTHONPATH: /app:/app/challenge
175
197
  volumes:
176
198
  - ../challenge:/app/challenge
177
199
  depends_on:
178
- init-db:
179
- condition: service_completed_successfully
200
+ postgres:
201
+ condition: service_healthy
180
202
  networks: [coordinator-net]
181
203
 
182
204
  model-orchestrator:
@@ -57,15 +57,13 @@ def load_scheduled_prediction_configs() -> list[dict[str, Any]]:
57
57
  return payload
58
58
 
59
59
 
60
- def init_db() -> None:
61
- print("➡️ Resetting canonical tables...")
62
- with engine.begin() as conn:
63
- for table in tables_to_reset():
64
- conn.execute(text(f"DROP TABLE IF EXISTS {table} CASCADE"))
65
-
66
- print("➡️ Creating coordinator core tables...")
60
+ def migrate() -> None:
61
+ """Create tables if they don't exist and upsert prediction configs.
62
+ Safe to run on every boot — never drops data."""
63
+ print("➡️ Creating tables (if not exist)...")
67
64
  SQLModel.metadata.create_all(engine)
68
65
 
66
+ print("➡️ Upserting scheduled prediction configs...")
69
67
  with create_session() as session:
70
68
  session.exec(delete(PredictionConfigRow))
71
69
  for idx, config in enumerate(load_scheduled_prediction_configs(), start=1):
@@ -83,8 +81,42 @@ def init_db() -> None:
83
81
  )
84
82
  session.commit()
85
83
 
86
- print("✅ Node-template database initialization complete.")
84
+ print("✅ Database migration complete.")
85
+
86
+
87
+ def reset_db() -> None:
88
+ """Drop all tables and recreate from scratch. Destroys all data."""
89
+ print("⚠️ Dropping all tables...")
90
+ with engine.begin() as conn:
91
+ for table in tables_to_reset():
92
+ conn.execute(text(f"DROP TABLE IF EXISTS {table} CASCADE"))
93
+
94
+ migrate()
95
+ print("✅ Database reset complete.")
96
+
97
+
98
+ # Keep backward compat
99
+ init_db = reset_db
100
+
101
+
102
+ def auto_migrate() -> None:
103
+ """Run migrate if tables don't exist yet. Called by workers on boot."""
104
+ try:
105
+ from sqlalchemy import inspect as sa_inspect
106
+ inspector = sa_inspect(engine)
107
+ if not inspector.has_table("models"):
108
+ migrate()
109
+ except Exception:
110
+ # First boot or connection issue — try migrate anyway
111
+ try:
112
+ migrate()
113
+ except Exception:
114
+ pass
87
115
 
88
116
 
89
117
  if __name__ == "__main__":
90
- init_db()
118
+ import sys
119
+ if "--reset" in sys.argv:
120
+ reset_db()
121
+ else:
122
+ migrate()
@@ -168,6 +168,7 @@ class DBPredictionRepository:
168
168
  existing.meta_jsonb = row.meta_jsonb
169
169
  existing.scope_key = row.scope_key
170
170
  existing.scope_jsonb = row.scope_jsonb
171
+ existing.resolvable_at = row.resolvable_at
171
172
  self._session.commit()
172
173
 
173
174
  def save_all(self, predictions: Iterable[PredictionRecord]) -> None:
@@ -16,6 +16,13 @@ def database_url() -> str:
16
16
 
17
17
  engine = create_engine(database_url())
18
18
 
19
+ _migrated = False
20
+
19
21
 
20
22
  def create_session() -> Session:
23
+ global _migrated
24
+ if not _migrated:
25
+ from coordinator_node.db.init_db import auto_migrate
26
+ auto_migrate()
27
+ _migrated = True
21
28
  return Session(engine)
@@ -224,7 +224,7 @@ class ScoreService:
224
224
  metrics: dict[str, float] = {}
225
225
  for window_name, window in aggregation.windows.items():
226
226
  cutoff = now - timedelta(hours=window.hours)
227
- window_snaps = [s for s in model_snapshots if s.period_end >= cutoff]
227
+ window_snaps = [s for s in model_snapshots if self._ensure_utc(s.period_end) >= cutoff]
228
228
  if window_snaps:
229
229
  vals = [float(s.result_summary.get(aggregation.ranking_key, 0)) for s in window_snaps]
230
230
  metrics[window_name] = sum(vals) / len(vals)
@@ -270,6 +270,13 @@ class ScoreService:
270
270
 
271
271
  _rank_leaderboard = _rank
272
272
 
273
+ @staticmethod
274
+ def _ensure_utc(dt: datetime) -> datetime:
275
+ """Ensure a datetime is timezone-aware (assume UTC if naive)."""
276
+ if dt.tzinfo is None:
277
+ return dt.replace(tzinfo=timezone.utc)
278
+ return dt
279
+
273
280
  def _rollback_repositories(self) -> None:
274
281
  for name, repo in [("input", self.input_repository),
275
282
  ("prediction", self.prediction_repository),
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "coordinator-node"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "Runtime engine for Crunch coordinator nodes"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -2,7 +2,7 @@ import unittest
2
2
  from datetime import datetime, timezone
3
3
 
4
4
  from coordinator_node.entities.model import Model
5
- from coordinator_node.entities.prediction import PredictionRecord
5
+ from coordinator_node.entities.prediction import InputRecord, PredictionRecord
6
6
  from coordinator_node.contracts import CrunchContract
7
7
  from coordinator_node.services.realtime_predict import RealtimePredictService
8
8
 
@@ -111,16 +111,33 @@ class InMemoryPredictionRepository:
111
111
  ]
112
112
 
113
113
 
114
+ class InMemoryInputRepository:
115
+ def __init__(self):
116
+ self.records: list[InputRecord] = []
117
+
118
+ def save(self, record: InputRecord):
119
+ for i, r in enumerate(self.records):
120
+ if r.id == record.id:
121
+ self.records[i] = record
122
+ return
123
+ self.records.append(record)
124
+
125
+ def find(self, **kwargs):
126
+ return list(self.records)
127
+
128
+
114
129
  class NoConfigPredictionRepository(InMemoryPredictionRepository):
115
130
  def fetch_active_configs(self):
116
131
  return []
117
132
 
118
133
 
119
- def _make_service(feed_reader=None, prediction_repo=None, runner=None, contract=None):
134
+ def _make_service(feed_reader=None, prediction_repo=None, input_repo=None,
135
+ runner=None, contract=None):
120
136
  return RealtimePredictService(
121
137
  checkpoint_interval_seconds=60,
122
138
  feed_reader=feed_reader or FakeFeedReader(),
123
139
  contract=contract or CrunchContract(),
140
+ input_repository=input_repo,
124
141
  model_repository=InMemoryModelRepository(),
125
142
  prediction_repository=prediction_repo or InMemoryPredictionRepository(),
126
143
  runner=runner or FakeRunner(),
@@ -191,5 +208,52 @@ class TestRealtimePredictService(unittest.IsolatedAsyncioTestCase):
191
208
  self.assertTrue(any("INFERENCE_OUTPUT_VALIDATION_ERROR" in line for line in logs.output))
192
209
 
193
210
 
211
+ async def test_run_once_sets_input_scope_with_feed_dimensions(self):
212
+ """Regression: input scope must include source/subject/kind/granularity
213
+ so the score worker can query matching feed records for ground truth."""
214
+ input_repo = InMemoryInputRepository()
215
+ pred_repo = InMemoryPredictionRepository()
216
+ feed_reader = FakeFeedReader({"symbol": "BTC"})
217
+ feed_reader.source = "binance"
218
+ feed_reader.subject = "BTC"
219
+ feed_reader.kind = "candle"
220
+ feed_reader.granularity = "1m"
221
+
222
+ service = _make_service(
223
+ feed_reader=feed_reader,
224
+ prediction_repo=pred_repo,
225
+ input_repo=input_repo,
226
+ )
227
+
228
+ await service.run_once(raw_input={"symbol": "BTC"}, now=datetime.now(timezone.utc))
229
+
230
+ self.assertEqual(len(input_repo.records), 1)
231
+ inp = input_repo.records[0]
232
+ # Feed dimensions must be in scope for score worker to query feed records
233
+ self.assertEqual(inp.scope.get("source"), "binance")
234
+ self.assertEqual(inp.scope.get("kind"), "candle")
235
+ self.assertEqual(inp.scope.get("granularity"), "1m")
236
+ # subject comes from config scope_template, which may override feed_reader
237
+ self.assertIn("subject", inp.scope)
238
+
239
+ async def test_run_once_sets_input_resolvable_at(self):
240
+ """Regression: input resolvable_at must be set so the score worker
241
+ can find inputs that are ready for ground truth resolution."""
242
+ input_repo = InMemoryInputRepository()
243
+ pred_repo = InMemoryPredictionRepository()
244
+ service = _make_service(
245
+ prediction_repo=pred_repo,
246
+ input_repo=input_repo,
247
+ )
248
+
249
+ now = datetime.now(timezone.utc)
250
+ await service.run_once(raw_input={"symbol": "BTC"}, now=now)
251
+
252
+ self.assertEqual(len(input_repo.records), 1)
253
+ inp = input_repo.records[0]
254
+ self.assertIsNotNone(inp.resolvable_at)
255
+ self.assertGreater(inp.resolvable_at, now)
256
+
257
+
194
258
  if __name__ == "__main__":
195
259
  unittest.main()
@@ -0,0 +1,86 @@
1
+ import inspect
2
+ import unittest
3
+
4
+ from coordinator_node.db.repositories import (
5
+ DBInputRepository,
6
+ DBLeaderboardRepository,
7
+ DBModelRepository,
8
+ DBPredictionRepository,
9
+ DBScoreRepository,
10
+ )
11
+
12
+
13
+ class TestRepositoryAPIs(unittest.TestCase):
14
+ def test_model_repository_has_required_methods(self):
15
+ self.assertTrue(callable(getattr(DBModelRepository, "fetch_all", None)))
16
+ self.assertTrue(callable(getattr(DBModelRepository, "save", None)))
17
+
18
+ def test_input_repository_has_required_methods(self):
19
+ self.assertTrue(callable(getattr(DBInputRepository, "save", None)))
20
+ self.assertTrue(callable(getattr(DBInputRepository, "find", None)))
21
+
22
+ def test_prediction_repository_has_required_methods(self):
23
+ self.assertTrue(callable(getattr(DBPredictionRepository, "save", None)))
24
+ self.assertTrue(callable(getattr(DBPredictionRepository, "save_all", None)))
25
+ self.assertTrue(callable(getattr(DBPredictionRepository, "find", None)))
26
+
27
+ def test_score_repository_has_required_methods(self):
28
+ self.assertTrue(callable(getattr(DBScoreRepository, "save", None)))
29
+ self.assertTrue(callable(getattr(DBScoreRepository, "find", None)))
30
+
31
+ def test_prediction_repository_has_query_scores_method(self):
32
+ self.assertTrue(callable(getattr(DBPredictionRepository, "query_scores", None)))
33
+
34
+ def test_leaderboard_repository_has_required_methods(self):
35
+ self.assertTrue(callable(getattr(DBLeaderboardRepository, "save", None)))
36
+ self.assertTrue(callable(getattr(DBLeaderboardRepository, "get_latest", None)))
37
+
38
+ def test_input_repository_save_updates_scope_and_resolvable_at(self):
39
+ """Regression: DBInputRepository.save() must update scope_jsonb and
40
+ resolvable_at on existing records, not just status/actuals/meta."""
41
+ source = inspect.getsource(DBInputRepository.save)
42
+ self.assertIn("scope_jsonb", source,
43
+ "save() must update scope_jsonb on existing records")
44
+ self.assertIn("resolvable_at", source,
45
+ "save() must update resolvable_at on existing records")
46
+
47
+ def test_prediction_repository_save_updates_resolvable_at(self):
48
+ """Regression: DBPredictionRepository.save() must update resolvable_at
49
+ on existing records."""
50
+ source = inspect.getsource(DBPredictionRepository.save)
51
+ self.assertIn("existing.resolvable_at", source,
52
+ "save() must update resolvable_at on existing records")
53
+
54
+ def test_all_repository_save_methods_update_all_constructor_fields(self):
55
+ """Regression: every save() method must update all fields it constructs,
56
+ except the primary key (id). Catches field omission bugs like the
57
+ scope_jsonb/resolvable_at issue."""
58
+ import re
59
+ from coordinator_node.db.repositories import (
60
+ DBCheckpointRepository, DBSnapshotRepository,
61
+ )
62
+ repos = [
63
+ ("DBModelRepository", DBModelRepository),
64
+ ("DBInputRepository", DBInputRepository),
65
+ ("DBPredictionRepository", DBPredictionRepository),
66
+ ("DBScoreRepository", DBScoreRepository),
67
+ ("DBSnapshotRepository", DBSnapshotRepository),
68
+ ("DBCheckpointRepository", DBCheckpointRepository),
69
+ ]
70
+ for name, cls in repos:
71
+ source = inspect.getsource(cls.save)
72
+ # Fields assigned via row.X in constructor
73
+ row_fields = set(re.findall(r'(\w+)=row\.(\w+)', source))
74
+ constructor_fields = {f[0] for f in row_fields if f[0] != 'id'}
75
+ # Fields updated via existing.X = row.X
76
+ existing_fields = set(re.findall(r'existing\.(\w+)\s*=\s*row\.(\w+)', source))
77
+ update_fields = {f[0] for f in existing_fields}
78
+
79
+ missing = constructor_fields - update_fields
80
+ self.assertEqual(missing, set(),
81
+ f"{name}.save() creates fields {sorted(missing)} "
82
+ f"but doesn't update them on existing records")
83
+
84
+
85
+ if __name__ == "__main__":
86
+ unittest.main()
@@ -304,7 +304,7 @@ wheels = [
304
304
 
305
305
  [[package]]
306
306
  name = "coordinator-node"
307
- version = "0.1.1"
307
+ version = "0.1.3"
308
308
  source = { editable = "." }
309
309
  dependencies = [
310
310
  { name = "fastapi" },
@@ -1,39 +0,0 @@
1
- import unittest
2
-
3
- from coordinator_node.db.repositories import (
4
- DBInputRepository,
5
- DBLeaderboardRepository,
6
- DBModelRepository,
7
- DBPredictionRepository,
8
- DBScoreRepository,
9
- )
10
-
11
-
12
- class TestRepositoryAPIs(unittest.TestCase):
13
- def test_model_repository_has_required_methods(self):
14
- self.assertTrue(callable(getattr(DBModelRepository, "fetch_all", None)))
15
- self.assertTrue(callable(getattr(DBModelRepository, "save", None)))
16
-
17
- def test_input_repository_has_required_methods(self):
18
- self.assertTrue(callable(getattr(DBInputRepository, "save", None)))
19
- self.assertTrue(callable(getattr(DBInputRepository, "find", None)))
20
-
21
- def test_prediction_repository_has_required_methods(self):
22
- self.assertTrue(callable(getattr(DBPredictionRepository, "save", None)))
23
- self.assertTrue(callable(getattr(DBPredictionRepository, "save_all", None)))
24
- self.assertTrue(callable(getattr(DBPredictionRepository, "find", None)))
25
-
26
- def test_score_repository_has_required_methods(self):
27
- self.assertTrue(callable(getattr(DBScoreRepository, "save", None)))
28
- self.assertTrue(callable(getattr(DBScoreRepository, "find", None)))
29
-
30
- def test_prediction_repository_has_query_scores_method(self):
31
- self.assertTrue(callable(getattr(DBPredictionRepository, "query_scores", None)))
32
-
33
- def test_leaderboard_repository_has_required_methods(self):
34
- self.assertTrue(callable(getattr(DBLeaderboardRepository, "save", None)))
35
- self.assertTrue(callable(getattr(DBLeaderboardRepository, "get_latest", None)))
36
-
37
-
38
- if __name__ == "__main__":
39
- unittest.main()