scale-forecasting 1.0.0__py3-none-any.whl

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 (186) hide show
  1. scale_forecasting/__init__.py +275 -0
  2. scale_forecasting/_entry.py +110 -0
  3. scale_forecasting/_infra_args.py +91 -0
  4. scale_forecasting/agent_surfaces.py +1571 -0
  5. scale_forecasting/airflow_emit.py +325 -0
  6. scale_forecasting/airflow_tasks.py +414 -0
  7. scale_forecasting/automl_submit.py +383 -0
  8. scale_forecasting/backtest.py +1153 -0
  9. scale_forecasting/batch_infra.py +322 -0
  10. scale_forecasting/batch_telemetry.py +241 -0
  11. scale_forecasting/calibration.py +900 -0
  12. scale_forecasting/capacity.py +659 -0
  13. scale_forecasting/cluster_deps.py +113 -0
  14. scale_forecasting/cluster_submit.py +383 -0
  15. scale_forecasting/cluster_telemetry.py +108 -0
  16. scale_forecasting/code_delivery.py +196 -0
  17. scale_forecasting/commands.py +459 -0
  18. scale_forecasting/compute_fallback.py +190 -0
  19. scale_forecasting/config.py +2530 -0
  20. scale_forecasting/dag.py +551 -0
  21. scale_forecasting/data_gen/__init__.py +3 -0
  22. scale_forecasting/data_gen/generator.py +415 -0
  23. scale_forecasting/data_gen/seed_spark.py +505 -0
  24. scale_forecasting/dataproc_cluster.py +869 -0
  25. scale_forecasting/device_audit.py +219 -0
  26. scale_forecasting/diagnostics.py +1510 -0
  27. scale_forecasting/engines/__init__.py +6 -0
  28. scale_forecasting/engines/automl_engine.py +2162 -0
  29. scale_forecasting/engines/bigquery_engine.py +623 -0
  30. scale_forecasting/engines/bigquery_names.py +101 -0
  31. scale_forecasting/engines/bigquery_sql.py +783 -0
  32. scale_forecasting/engines/ray_engine.py +809 -0
  33. scale_forecasting/engines/ray_io.py +1007 -0
  34. scale_forecasting/engines/spark_explode.py +358 -0
  35. scale_forecasting/engines/spark_io.py +763 -0
  36. scale_forecasting/engines/vertex_engine.py +1171 -0
  37. scale_forecasting/ensemble_run.py +1102 -0
  38. scale_forecasting/ensembler.py +1279 -0
  39. scale_forecasting/errors.py +188 -0
  40. scale_forecasting/features.py +1352 -0
  41. scale_forecasting/gce_submit.py +1028 -0
  42. scale_forecasting/gke_submit.py +1587 -0
  43. scale_forecasting/hardware.py +265 -0
  44. scale_forecasting/hpo.py +424 -0
  45. scale_forecasting/identity.py +94 -0
  46. scale_forecasting/job_launch.py +768 -0
  47. scale_forecasting/job_outcome.py +241 -0
  48. scale_forecasting/job_wait.py +111 -0
  49. scale_forecasting/launch_plan.py +1107 -0
  50. scale_forecasting/main.py +799 -0
  51. scale_forecasting/mcp.py +1385 -0
  52. scale_forecasting/metrics/__init__.py +253 -0
  53. scale_forecasting/metrics/_intervals.py +19 -0
  54. scale_forecasting/metrics/base_metric.py +217 -0
  55. scale_forecasting/metrics/bias.py +34 -0
  56. scale_forecasting/metrics/coverage.py +40 -0
  57. scale_forecasting/metrics/cv.py +24 -0
  58. scale_forecasting/metrics/interval_score.py +42 -0
  59. scale_forecasting/metrics/interval_width.py +38 -0
  60. scale_forecasting/metrics/maape.py +38 -0
  61. scale_forecasting/metrics/mae.py +30 -0
  62. scale_forecasting/metrics/mape.py +33 -0
  63. scale_forecasting/metrics/mase.py +45 -0
  64. scale_forecasting/metrics/mase_seasonal.py +49 -0
  65. scale_forecasting/metrics/mse.py +32 -0
  66. scale_forecasting/metrics/msis.py +30 -0
  67. scale_forecasting/metrics/msse.py +28 -0
  68. scale_forecasting/metrics/ope.py +24 -0
  69. scale_forecasting/metrics/pinball.py +49 -0
  70. scale_forecasting/metrics/r2.py +27 -0
  71. scale_forecasting/metrics/rmse.py +30 -0
  72. scale_forecasting/metrics/rmsle.py +25 -0
  73. scale_forecasting/metrics/rmsse.py +41 -0
  74. scale_forecasting/metrics/smape.py +33 -0
  75. scale_forecasting/metrics/wape.py +34 -0
  76. scale_forecasting/models/__init__.py +112 -0
  77. scale_forecasting/models/_lag_forecaster.py +407 -0
  78. scale_forecasting/models/_neuralforecast_base.py +605 -0
  79. scale_forecasting/models/_vertex_automl_base.py +268 -0
  80. scale_forecasting/models/auto_arima.py +142 -0
  81. scale_forecasting/models/auto_ces.py +103 -0
  82. scale_forecasting/models/auto_theta.py +113 -0
  83. scale_forecasting/models/autoets.py +106 -0
  84. scale_forecasting/models/base_model.py +600 -0
  85. scale_forecasting/models/bigquery_native.py +177 -0
  86. scale_forecasting/models/catboost_model.py +159 -0
  87. scale_forecasting/models/croston.py +137 -0
  88. scale_forecasting/models/fft.py +108 -0
  89. scale_forecasting/models/holtwinters.py +85 -0
  90. scale_forecasting/models/kalman.py +95 -0
  91. scale_forecasting/models/lightgbm_model.py +160 -0
  92. scale_forecasting/models/naive_drift.py +69 -0
  93. scale_forecasting/models/naive_mean.py +60 -0
  94. scale_forecasting/models/naive_moving_average.py +93 -0
  95. scale_forecasting/models/naive_seasonal.py +72 -0
  96. scale_forecasting/models/neuralprophet_model.py +421 -0
  97. scale_forecasting/models/patchtst.py +51 -0
  98. scale_forecasting/models/prophet_model.py +121 -0
  99. scale_forecasting/models/random_forest.py +153 -0
  100. scale_forecasting/models/regression_lags.py +171 -0
  101. scale_forecasting/models/sarimax.py +92 -0
  102. scale_forecasting/models/stl_bagging.py +109 -0
  103. scale_forecasting/models/tbats_model.py +102 -0
  104. scale_forecasting/models/tft.py +46 -0
  105. scale_forecasting/models/theta.py +89 -0
  106. scale_forecasting/models/tide.py +47 -0
  107. scale_forecasting/models/tsmixer.py +48 -0
  108. scale_forecasting/models/ucm.py +91 -0
  109. scale_forecasting/models/vertex_l2l.py +31 -0
  110. scale_forecasting/models/vertex_seq2seq.py +30 -0
  111. scale_forecasting/models/vertex_tft.py +32 -0
  112. scale_forecasting/models/vertex_tide.py +32 -0
  113. scale_forecasting/models/xgboost_model.py +164 -0
  114. scale_forecasting/notebook_acceptance.py +755 -0
  115. scale_forecasting/playground.py +793 -0
  116. scale_forecasting/probes/__init__.py +3 -0
  117. scale_forecasting/probes/cancel.py +408 -0
  118. scale_forecasting/probes/reconcile.py +577 -0
  119. scale_forecasting/probes/runtimes.py +941 -0
  120. scale_forecasting/probes/settle.py +466 -0
  121. scale_forecasting/probes/vocabulary.py +245 -0
  122. scale_forecasting/profiling/__init__.py +3 -0
  123. scale_forecasting/profiling/baseline.py +240 -0
  124. scale_forecasting/profiling/cost.py +705 -0
  125. scale_forecasting/profiling/measure.py +440 -0
  126. scale_forecasting/profiling/numeric.py +73 -0
  127. scale_forecasting/profiling/sampling.py +290 -0
  128. scale_forecasting/profiling/signature.py +136 -0
  129. scale_forecasting/profiling/source.py +473 -0
  130. scale_forecasting/profiling/stats.py +221 -0
  131. scale_forecasting/py.typed +0 -0
  132. scale_forecasting/quota.py +1767 -0
  133. scale_forecasting/ray_cluster.py +767 -0
  134. scale_forecasting/ray_entry.py +46 -0
  135. scale_forecasting/ray_infra.py +121 -0
  136. scale_forecasting/ray_jobs.py +462 -0
  137. scale_forecasting/ray_reaper.py +392 -0
  138. scale_forecasting/ray_submit.py +323 -0
  139. scale_forecasting/ray_telemetry.py +106 -0
  140. scale_forecasting/reconciliation.py +929 -0
  141. scale_forecasting/registry/__init__.py +3 -0
  142. scale_forecasting/registry/artifacts.py +223 -0
  143. scale_forecasting/registry/cells.py +115 -0
  144. scale_forecasting/registry/ddl.py +408 -0
  145. scale_forecasting/registry/harvest.py +252 -0
  146. scale_forecasting/registry/header.py +368 -0
  147. scale_forecasting/registry/ids.py +556 -0
  148. scale_forecasting/registry/jobs.py +191 -0
  149. scale_forecasting/registry/lifecycle.py +284 -0
  150. scale_forecasting/registry/ops.py +1666 -0
  151. scale_forecasting/registry/params.py +150 -0
  152. scale_forecasting/registry/reads.py +971 -0
  153. scale_forecasting/registry/rows.py +556 -0
  154. scale_forecasting/registry/tables.py +150 -0
  155. scale_forecasting/registry/views.py +399 -0
  156. scale_forecasting/registry/write_api.py +386 -0
  157. scale_forecasting/report.py +932 -0
  158. scale_forecasting/resources/__init__.py +3 -0
  159. scale_forecasting/resources/audit.py +64 -0
  160. scale_forecasting/resources/catalog.py +263 -0
  161. scale_forecasting/resources/cluster.py +352 -0
  162. scale_forecasting/resources/fleet.py +613 -0
  163. scale_forecasting/resources/serverless.py +428 -0
  164. scale_forecasting/resources/slot.py +295 -0
  165. scale_forecasting/retry_policy.py +378 -0
  166. scale_forecasting/retry_run.py +535 -0
  167. scale_forecasting/review.py +2689 -0
  168. scale_forecasting/router.py +41 -0
  169. scale_forecasting/scenarios.py +564 -0
  170. scale_forecasting/sdk.py +2073 -0
  171. scale_forecasting/seasonality.py +60 -0
  172. scale_forecasting/settings.py +171 -0
  173. scale_forecasting/shared_clusters.py +348 -0
  174. scale_forecasting/smoke_run.py +83 -0
  175. scale_forecasting/spark_entry.py +47 -0
  176. scale_forecasting/staging.py +121 -0
  177. scale_forecasting/submit.py +667 -0
  178. scale_forecasting/submitters.py +515 -0
  179. scale_forecasting/validation.py +201 -0
  180. scale_forecasting/vertex_entry.py +81 -0
  181. scale_forecasting/vertex_submit.py +680 -0
  182. scale_forecasting/worker.py +1432 -0
  183. scale_forecasting-1.0.0.dist-info/METADATA +361 -0
  184. scale_forecasting-1.0.0.dist-info/RECORD +186 -0
  185. scale_forecasting-1.0.0.dist-info/WHEEL +4 -0
  186. scale_forecasting-1.0.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,275 @@
1
+ """scale-forecasting — massively-parallel time-series forecasting on Google Cloud.
2
+
3
+ The package is deliberately flat and readable: one capability per file. Start with the
4
+ architecture overview in the README, then read ``config.py`` (the run contract),
5
+ ``worker.py`` (the unit of work), and one file under ``models/`` to see the whole shape.
6
+
7
+ Public surface — two ways in, one engine underneath:
8
+
9
+ * **The easy path.** `Forecaster` — point it at a config and call
10
+ `dry_run` / `run`
11
+ / `review`. Thin wrapper over `run`; no logic of its
12
+ own.
13
+ * **The manage path.** `Registry` — the operator surface over the registry a deployment writes
14
+ to: `doctor` (read-only health), `drop_run`, `sweep_orphans`, `snapshot`, `export`, `init`.
15
+ Same verbs as ``python -m scale_forecasting.registry.ops``.
16
+ * **The direct path.** Drive Spark or Ray yourself and reuse the *same* model machinery:
17
+ `run_group` (pure, per-bucket), `make_group_runner` / `make_chunk_runner` (the
18
+ writer-attached ``applyInPandas`` / Ray-task closures), `chunk_cells`, and the unit of work
19
+ `run_cell`. Both paths run byte-identical cell code. See ``docs/using_the_sdk.md``.
20
+
21
+ Import cost: ``import scale_forecasting`` is near-instant. Light names (config, settings, errors)
22
+ are eager; the heavy names above load the model modules and are therefore **lazy** — resolved on
23
+ first attribute access via PEP 562. Touching ``Forecaster``/``run``/``run_cell`` pays the model
24
+ import cost; touching ``RunConfig``/``Settings`` does not. ``test_sdk.py`` guards this contract.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from importlib.metadata import PackageNotFoundError
30
+ from importlib.metadata import version as _pkg_version
31
+ from typing import TYPE_CHECKING
32
+
33
+ # Eager (light): the config contract, infra identity, and the error hierarchy. None of these pull
34
+ # the model modules, so they keep ``import scale_forecasting`` cheap.
35
+ from .config import (
36
+ Fanout,
37
+ RunConfig,
38
+ Workload,
39
+ estimate_fanout,
40
+ estimate_workload,
41
+ load_config,
42
+ )
43
+ from .errors import (
44
+ ConfigError,
45
+ DataError,
46
+ EngineError,
47
+ ModelError,
48
+ RegistryError,
49
+ ScaleForecastError,
50
+ )
51
+ from .settings import Settings
52
+
53
+ try:
54
+ __version__: str = _pkg_version("scale-forecasting")
55
+ except PackageNotFoundError: # pragma: no cover - zip-staged cloud worker without .dist-info
56
+ __version__ = "1.0.0"
57
+
58
+ # Lazy (heavy): name -> (module, attribute). Resolved on first access by ``__getattr__`` (PEP 562)
59
+ # so the model modules (statsmodels et al., ~5s) load only when a caller actually reaches for the
60
+ # run/model surface — never merely to ``import scale_forecasting``.
61
+ _LAZY: dict[str, tuple[str, str]] = {
62
+ # Easy path.
63
+ "Forecaster": (".sdk", "Forecaster"),
64
+ "Registry": (".sdk", "Registry"),
65
+ "DryRunResult": (".sdk", "DryRunResult"),
66
+ "RunResult": (".sdk", "RunResult"),
67
+ "ModelResult": (".sdk", "ModelResult"),
68
+ "JobTrace": (".sdk", "JobTrace"),
69
+ "build_explain_frame": (".sdk", "build_explain_frame"),
70
+ "build_best_params_frame": (".sdk", "build_best_params_frame"),
71
+ "build_trace_frame": (".sdk", "build_trace_frame"),
72
+ "plot_trace": (".sdk", "plot_trace"),
73
+ # Run inspection — monitor a running run, review a finished one.
74
+ "monitor_run": (".review", "monitor_run"),
75
+ "review_run": (".review", "review_run"),
76
+ "RunProgress": (".review", "RunProgress"),
77
+ "RunReview": (".review", "RunReview"),
78
+ "calibration_report": (".review", "calibration_report"),
79
+ "CalibrationReport": (".review", "CalibrationReport"),
80
+ "build_leaderboard_frame": (".review", "build_leaderboard_frame"),
81
+ "build_predictions_frame": (".review", "build_predictions_frame"),
82
+ "build_hierarchy_frame": (".review", "build_hierarchy_frame"),
83
+ "build_cohorts_frame": (".review", "build_cohorts_frame"),
84
+ "build_calibration_frames": (".review", "build_calibration_frames"),
85
+ "build_ensemble_weights_frame": (".review", "build_ensemble_weights_frame"),
86
+ "build_attributions_frame": (".review", "build_attributions_frame"),
87
+ "explain_forecast_frame": (".review", "explain_forecast_frame"),
88
+ "plot_progress": (".review", "plot_progress"),
89
+ "plot_leaderboard": (".review", "plot_leaderboard"),
90
+ "plot_metric_distribution": (".review", "plot_metric_distribution"),
91
+ "plot_forecasts_frame": (".review", "plot_forecasts_frame"),
92
+ "plot_hierarchy_frame": (".review", "plot_hierarchy_frame"),
93
+ "plot_calibration": (".review", "plot_calibration"),
94
+ "plot_ensemble_weights": (".review", "plot_ensemble_weights"),
95
+ "plot_attributions": (".review", "plot_attributions"),
96
+ "plot_forecast_explanation": (".review", "plot_forecast_explanation"),
97
+ "DagNode": (".dag", "DagNode"),
98
+ # Cross-run ensembling.
99
+ "merge_configs_for_ensemble": (".ensemble_run", "merge_configs_for_ensemble"),
100
+ "ensemble_cross_runs": (".ensemble_run", "ensemble_cross_runs"),
101
+ # Orchestration.
102
+ "run": (".main", "run"),
103
+ # Direct path — pure core + writer-attached runners + unit of work.
104
+ "run_group": (".engines.spark_io", "run_group"),
105
+ "make_group_runner": (".engines.spark_io", "make_group_runner"),
106
+ "RunOutcome": (".engines.spark_io", "RunOutcome"),
107
+ "aggregate_status": (".engines.spark_io", "aggregate_status"),
108
+ "make_chunk_runner": (".engines.ray_io", "make_chunk_runner"),
109
+ "chunk_cells": (".engines.ray_io", "chunk_cells"),
110
+ "run_cell": (".worker", "run_cell"),
111
+ "CellResult": (".worker", "CellResult"),
112
+ # Model registry + runtime routing + catalogs.
113
+ "get_model": (".models", "get_model"),
114
+ "list_models": (".models", "list_models"),
115
+ "model_catalog": (".playground", "model_catalog"),
116
+ "metric_catalog": (".playground", "metric_catalog"),
117
+ "split_by_runtime": (".router", "split_by_runtime"),
118
+ # Hierarchical aggregation + coherent reconciliation.
119
+ "HierarchySpec": (".reconciliation", "HierarchySpec"),
120
+ "build_hierarchy": (".reconciliation", "build_hierarchy"),
121
+ "reconcile_matrix": (".reconciliation", "reconcile_matrix"),
122
+ "reconcile_forecasts": (".reconciliation", "reconcile_forecasts"),
123
+ "reconcile_oof": (".reconciliation", "reconcile_oof"),
124
+ "reconcile_cells": (".reconciliation", "reconcile_cells"),
125
+ "verify_coherence": (".reconciliation", "verify_coherence"),
126
+ # AI agent surfaces & MCP server.
127
+ "probe_environment": (".agent_surfaces", "probe_environment"),
128
+ "McpServer": (".mcp", "McpServer"),
129
+ # Forecaster Workbench diagnostics, scenario simulation & self-contained reports.
130
+ "compute_demand_profile": (".features", "compute_demand_profile"),
131
+ "compute_fva": (".diagnostics", "compute_fva"),
132
+ "diebold_mariano_test": (".diagnostics", "diebold_mariano_test"),
133
+ "wilcoxon_pvalue": (".diagnostics", "wilcoxon_pvalue"),
134
+ "compute_horizon_decay": (".diagnostics", "compute_horizon_decay"),
135
+ "compute_bias_report": (".diagnostics", "compute_bias_report"),
136
+ "compute_realized_accuracy": (".diagnostics", "compute_realized_accuracy"),
137
+ "compute_revision_stability": (".diagnostics", "compute_revision_stability"),
138
+ "compute_exceptions_queue": (".diagnostics", "compute_exceptions_queue"),
139
+ "rollup_time": (".diagnostics", "rollup_time"),
140
+ "plot_fva": (".diagnostics", "plot_fva"),
141
+ "plot_horizon_decay": (".diagnostics", "plot_horizon_decay"),
142
+ "apply_scenario_shocks": (".scenarios", "apply_scenario_shocks"),
143
+ "simulate_scenarios": (".scenarios", "simulate_scenarios"),
144
+ "summarize_scenarios": (".scenarios", "summarize_scenarios"),
145
+ "plot_scenarios": (".scenarios", "plot_scenarios"),
146
+ "build_report_payload": (".report", "build_report_payload"),
147
+ "render_html_report": (".report", "render_html_report"),
148
+ "export_report": (".report", "export_report"),
149
+ }
150
+
151
+ __all__ = [
152
+ "__version__",
153
+ # Eager.
154
+ "RunConfig",
155
+ "load_config",
156
+ "estimate_workload",
157
+ "Workload",
158
+ "estimate_fanout",
159
+ "Fanout",
160
+ "Settings",
161
+ "ScaleForecastError",
162
+ "ConfigError",
163
+ "DataError",
164
+ "ModelError",
165
+ "RegistryError",
166
+ "EngineError",
167
+ # Lazy.
168
+ *_LAZY,
169
+ ]
170
+
171
+ if TYPE_CHECKING: # so IDEs / type-checkers see the lazy names as real imports (re-exported below)
172
+ from .agent_surfaces import probe_environment # noqa: F401
173
+ from .dag import DagNode # noqa: F401
174
+ from .diagnostics import ( # noqa: F401
175
+ compute_bias_report,
176
+ compute_exceptions_queue,
177
+ compute_fva,
178
+ compute_horizon_decay,
179
+ compute_realized_accuracy,
180
+ compute_revision_stability,
181
+ diebold_mariano_test,
182
+ plot_fva,
183
+ plot_horizon_decay,
184
+ rollup_time,
185
+ wilcoxon_pvalue,
186
+ )
187
+ from .engines.ray_io import chunk_cells, make_chunk_runner # noqa: F401
188
+ from .engines.spark_io import ( # noqa: F401
189
+ RunOutcome,
190
+ aggregate_status,
191
+ make_group_runner,
192
+ run_group,
193
+ )
194
+ from .ensemble_run import ensemble_cross_runs, merge_configs_for_ensemble # noqa: F401
195
+ from .features import compute_demand_profile # noqa: F401
196
+ from .main import run # noqa: F401
197
+ from .mcp import McpServer # noqa: F401
198
+ from .models import get_model, list_models # noqa: F401
199
+ from .playground import metric_catalog, model_catalog # noqa: F401
200
+ from .reconciliation import ( # noqa: F401
201
+ HierarchySpec,
202
+ build_hierarchy,
203
+ reconcile_cells,
204
+ reconcile_forecasts,
205
+ reconcile_matrix,
206
+ reconcile_oof,
207
+ verify_coherence,
208
+ )
209
+ from .report import ( # noqa: F401
210
+ build_report_payload,
211
+ export_report,
212
+ render_html_report,
213
+ )
214
+ from .review import ( # noqa: F401
215
+ CalibrationReport,
216
+ RunProgress,
217
+ RunReview,
218
+ build_attributions_frame,
219
+ build_calibration_frames,
220
+ build_cohorts_frame,
221
+ build_ensemble_weights_frame,
222
+ build_hierarchy_frame,
223
+ build_leaderboard_frame,
224
+ build_predictions_frame,
225
+ calibration_report,
226
+ explain_forecast_frame,
227
+ monitor_run,
228
+ plot_attributions,
229
+ plot_calibration,
230
+ plot_ensemble_weights,
231
+ plot_forecast_explanation,
232
+ plot_forecasts_frame,
233
+ plot_hierarchy_frame,
234
+ plot_leaderboard,
235
+ plot_metric_distribution,
236
+ plot_progress,
237
+ review_run,
238
+ )
239
+ from .router import split_by_runtime # noqa: F401
240
+ from .scenarios import ( # noqa: F401
241
+ apply_scenario_shocks,
242
+ plot_scenarios,
243
+ simulate_scenarios,
244
+ summarize_scenarios,
245
+ )
246
+ from .sdk import ( # noqa: F401
247
+ DryRunResult,
248
+ Forecaster,
249
+ JobTrace,
250
+ ModelResult,
251
+ Registry,
252
+ RunResult,
253
+ build_best_params_frame,
254
+ build_explain_frame,
255
+ build_trace_frame,
256
+ plot_trace,
257
+ )
258
+ from .worker import CellResult, run_cell # noqa: F401
259
+
260
+
261
+ def __getattr__(name: str) -> object:
262
+ """PEP 562 lazy loader: resolve a heavy public name on first access, then cache it."""
263
+ target = _LAZY.get(name)
264
+ if target is None:
265
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
266
+ from importlib import import_module
267
+
268
+ module_name, attr = target
269
+ value = getattr(import_module(module_name, __name__), attr)
270
+ globals()[name] = value # cache so subsequent lookups skip __getattr__
271
+ return value
272
+
273
+
274
+ def __dir__() -> list[str]:
275
+ return sorted(__all__)
@@ -0,0 +1,110 @@
1
+ """Shared on-cluster entrypoint core for the Dataproc and Ray forecast launchers.
2
+
3
+ Both `spark_entry` and `ray_entry` parse the same on-cluster contract — ``--config-uri`` (the run
4
+ config, staged to GCS as JSON — the JSON *is* the reproducibility record) + the executed
5
+ ``--models`` subset + ``--manage-header`` + the ``--sf-*`` infra args, which `export_infra_env`
6
+ promotes to ``os.environ`` before anything resolves `Settings` — then load the staged config and
7
+ run the runtime's engine ``run(cfg, models=, manage_header=)``. This module is that shared skeleton,
8
+ parametrized by a resolver so each launcher differs only in which engine callable it selects (Spark
9
+ and Ray each have exactly one — there is no method switch).
10
+
11
+ ``--models m1,m2`` restricts the executed subset (the staged config's ``run_id`` is unchanged, so
12
+ both runtimes share it) and ``--manage-header false`` puts the engine in contributor mode (``main``
13
+ owns the single shared header). Both are optional — absent, the engine runs its standalone
14
+ lifecycle over ``cfg.models``.
15
+
16
+ ``--provisioned-hardware gpu`` states what the submitter actually bought, so a model can select a
17
+ device explicitly rather than asking its library to guess (see `hardware`). Emitted only by GPU
18
+ jobs; absent everywhere else, which leaves device selection at ``auto`` — today's behaviour.
19
+
20
+ The GCP/engine imports stay lazy so the launchers import cleanly offline.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ from collections.abc import Callable
27
+ from typing import TYPE_CHECKING
28
+
29
+ from ._infra_args import add_infra_args, export_infra_env
30
+ from .errors import configure_cli_logging, get_logger
31
+ from .hardware import add_hardware_arg, export_hardware_env
32
+
33
+ if TYPE_CHECKING:
34
+ from .config import RunConfig
35
+
36
+ _log = get_logger(__name__)
37
+
38
+ # A resolved (engine_run, label) pair: the callable to run and a short name for logging. The
39
+ # callable takes ``(cfg, models=, manage_header=)`` — the shared engine contract.
40
+ EngineResolver = Callable[[argparse.Namespace], "tuple[Callable[..., object], str]"]
41
+
42
+
43
+ def parse_models(raw: str | None) -> list[str] | None:
44
+ """Parse the optional ``--models m1,m2`` CSV into a subset list (``None`` → run ``cfg.models``).
45
+
46
+ Empty/whitespace-only tokens are dropped so a trailing comma is harmless; an all-empty value
47
+ collapses to ``None`` (standalone) rather than an empty subset (which would run nothing).
48
+ """
49
+ if raw is None:
50
+ return None
51
+ names = [tok.strip() for tok in raw.split(",") if tok.strip()]
52
+ return names or None
53
+
54
+
55
+ def build_parser(prog: str, description: str) -> argparse.ArgumentParser:
56
+ """The shared launcher parser: ``--config-uri`` + ``--models`` + ``--manage-header`` + infra."""
57
+ p = argparse.ArgumentParser(prog=prog, description=description)
58
+ p.add_argument("--config-uri", required=True, help="gs:// (or local) path to run config JSON")
59
+ p.add_argument(
60
+ "--models",
61
+ default=None,
62
+ help="optional comma-separated executed subset; absent runs all cfg.models",
63
+ )
64
+ p.add_argument(
65
+ "--manage-header",
66
+ default="true",
67
+ choices=("true", "false"),
68
+ help="false = contributor mode; main.run owns the shared header",
69
+ )
70
+ add_infra_args(p)
71
+ add_hardware_arg(p)
72
+ return p
73
+
74
+
75
+ def run_entry(
76
+ argv: list[str] | None,
77
+ *,
78
+ prog: str,
79
+ description: str,
80
+ resolve_engine: EngineResolver,
81
+ ) -> None:
82
+ """Parse the shared launcher args, load the staged config, and run the resolved engine.
83
+
84
+ The one on-cluster driver skeleton for both runtimes: parse → export infra env → load the
85
+ staged config (`load_config_uri`, which reads a ``gs://`` URI directly or a local path) →
86
+ ``resolve_engine(ns)`` picks the engine callable → run it with the parsed subset + header mode.
87
+ """
88
+ from .config import load_config_uri
89
+
90
+ configure_cli_logging()
91
+ p = build_parser(prog, description)
92
+ ns = p.parse_args(argv)
93
+ export_infra_env(ns)
94
+ # The driver half of the device carrier. The executor half rides the job spec
95
+ # (`hardware.spark_executor_env` / `hardware.ray_env_vars`) because a driver's environment does
96
+ # not reach a Spark executor or a Ray task worker.
97
+ export_hardware_env(ns)
98
+ models = parse_models(ns.models)
99
+ manage_header = ns.manage_header == "true"
100
+ engine_run, label = resolve_engine(ns)
101
+ _log.info(
102
+ "%s: runtime=%s config_uri=%s models=%s manage_header=%s",
103
+ prog,
104
+ label,
105
+ ns.config_uri,
106
+ models,
107
+ manage_header,
108
+ )
109
+ cfg: RunConfig = load_config_uri(ns.config_uri)
110
+ engine_run(cfg, models=models, manage_header=manage_header)
@@ -0,0 +1,91 @@
1
+ """Infra-identity CLI args ↔ ``SF_*`` environment — the one delivery mapping.
2
+
3
+ Dataproc Serverless allowlists Spark property prefixes and rejects driver-env
4
+ (``spark.kubernetes.driverEnv.*`` → "unsupported properties"), so a batch cannot hand the
5
+ infra identity to the driver as environment variables. Instead every entrypoint accepts the
6
+ identity as ``--sf-*`` job args and exports them into ``os.environ`` before
7
+ `resolve` runs — keeping env-based ``Settings`` the
8
+ single local/cloud seam rather than forking a "resolve from args" path.
9
+
10
+ This module owns that mapping in ONE place so the seed job, the Spark forecast launcher
11
+ (``spark_entry``), the local submit helper (``submit``), and the Terraform modules that build the
12
+ ``--sf-*`` arg list all stay in agreement. Local runs pass no ``--sf-*`` and use the ambient
13
+ ``SF_*`` environment untouched.
14
+
15
+ Public surface: ``INFRA_ARG_ENV``, ``add_infra_args``, ``export_infra_env``, ``infra_args_from``.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ from typing import TYPE_CHECKING
22
+
23
+ if TYPE_CHECKING:
24
+ import argparse
25
+
26
+ from .settings import Settings
27
+
28
+ # The (argparse dest → SF_* env var) mapping. ``add_infra_args`` registers ``--sf-*`` flags whose
29
+ # argparse dests are these first elements; ``export_infra_env`` copies each provided value into the
30
+ # matching env var before Settings.resolve().
31
+ INFRA_ARG_ENV: tuple[tuple[str, str], ...] = (
32
+ ("sf_project_id", "SF_PROJECT_ID"),
33
+ ("sf_connection", "SF_CONNECTION"),
34
+ ("sf_warehouse_uri", "SF_WAREHOUSE_URI"),
35
+ ("sf_dataset_id", "SF_DATASET_ID"),
36
+ # Only emitted when the deployment actually splits the registry from the source panel (the
37
+ # value is "" otherwise and `infra_args_from` skips empty values), so an existing deployment's
38
+ # submitted command is byte-identical. It has to travel, though: without it a cluster driver
39
+ # would re-resolve to the *source* dataset and write the run's rows to the wrong registry.
40
+ ("sf_registry_dataset_id", "SF_REGISTRY_DATASET_ID"),
41
+ ("sf_region", "SF_REGION"),
42
+ )
43
+
44
+
45
+ def add_infra_args(parser: argparse.ArgumentParser) -> None:
46
+ """Register the ``--sf-*`` infra-identity flags on a parser (all default ``None``).
47
+
48
+ When unset (local runs) the ambient ``SF_*`` environment is used as-is; when set (a cluster
49
+ batch) `export_infra_env` promotes them to ``os.environ`` before resolution.
50
+ """
51
+ for dest, env_name in INFRA_ARG_ENV:
52
+ flag = "--" + dest.replace("_", "-")
53
+ parser.add_argument(
54
+ flag, type=str, default=None, help=f"sets {env_name} in the environment"
55
+ )
56
+
57
+
58
+ def export_infra_env(ns: argparse.Namespace) -> None:
59
+ """Copy any provided ``--sf-*`` args from ``ns`` into ``os.environ`` (only when set).
60
+
61
+ Keeps env-based ``Settings.resolve()`` the single local/cloud seam without a parallel
62
+ "resolve from args" path. Values left unset are skipped, so a local run's ambient environment
63
+ is untouched.
64
+ """
65
+ for dest, env_name in INFRA_ARG_ENV:
66
+ value = getattr(ns, dest, None)
67
+ if value:
68
+ os.environ[env_name] = value
69
+
70
+
71
+ def infra_args_from(settings: Settings) -> list[str]:
72
+ """Build the ``--sf-*`` arg list carrying ``settings`` to a cluster batch (submit side).
73
+
74
+ The inverse of `add_infra_args` / `export_infra_env`: turns a resolved
75
+ `Settings` into the flat ``["--sf-project-id", ...]`` list a
76
+ Dataproc batch passes so the driver re-materializes the same identity via env.
77
+ """
78
+ values = {
79
+ "sf_project_id": settings.project_id,
80
+ "sf_connection": settings.connection,
81
+ "sf_warehouse_uri": settings.warehouse_uri,
82
+ "sf_dataset_id": settings.dataset_id,
83
+ "sf_registry_dataset_id": settings.registry_dataset_id_override,
84
+ "sf_region": settings.region,
85
+ }
86
+ args: list[str] = []
87
+ for dest, _env_name in INFRA_ARG_ENV:
88
+ value = values[dest]
89
+ if value:
90
+ args += ["--" + dest.replace("_", "-"), value]
91
+ return args