scale-forecasting 1.2.0__tar.gz → 1.2.1__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 (192) hide show
  1. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/CHANGELOG.md +21 -0
  2. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/PKG-INFO +1 -1
  3. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/pyproject.toml +1 -1
  4. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/__init__.py +1 -1
  5. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/agent_surfaces.py +146 -0
  6. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/automl_submit.py +15 -0
  7. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/batch_telemetry.py +39 -8
  8. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/cluster_submit.py +23 -0
  9. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/code_delivery.py +42 -0
  10. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/ray_engine.py +3 -1
  11. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/spark_explode.py +7 -1
  12. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/gce_submit.py +44 -2
  13. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/gke_submit.py +26 -0
  14. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/probes/runtimes.py +48 -8
  15. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ray_submit.py +21 -0
  16. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/.gitignore +0 -0
  17. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/LICENSE +0 -0
  18. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/README.md +0 -0
  19. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/_entry.py +0 -0
  20. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/_infra_args.py +0 -0
  21. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/airflow_emit.py +0 -0
  22. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/airflow_tasks.py +0 -0
  23. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/backtest.py +0 -0
  24. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/batch_infra.py +0 -0
  25. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/calibration.py +0 -0
  26. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/capacity.py +0 -0
  27. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/cluster_deps.py +0 -0
  28. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/cluster_telemetry.py +0 -0
  29. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/commands.py +0 -0
  30. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/compute_fallback.py +0 -0
  31. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/config.py +0 -0
  32. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/dag.py +0 -0
  33. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/data_gen/__init__.py +0 -0
  34. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/data_gen/generator.py +0 -0
  35. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/data_gen/seed_spark.py +0 -0
  36. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/dataproc_cluster.py +0 -0
  37. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/device_audit.py +0 -0
  38. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/diagnostics.py +0 -0
  39. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/__init__.py +0 -0
  40. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/automl_engine.py +0 -0
  41. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/bigquery_engine.py +0 -0
  42. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/bigquery_names.py +0 -0
  43. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/bigquery_sql.py +0 -0
  44. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/ray_io.py +0 -0
  45. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/spark_io.py +0 -0
  46. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/vertex_engine.py +0 -0
  47. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ensemble_run.py +0 -0
  48. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ensembler.py +0 -0
  49. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/errors.py +0 -0
  50. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/features.py +0 -0
  51. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/hardware.py +0 -0
  52. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/hpo.py +0 -0
  53. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/identity.py +0 -0
  54. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/job_launch.py +0 -0
  55. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/job_outcome.py +0 -0
  56. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/job_wait.py +0 -0
  57. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/launch_plan.py +0 -0
  58. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/main.py +0 -0
  59. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/mcp.py +0 -0
  60. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/__init__.py +0 -0
  61. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/_intervals.py +0 -0
  62. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/base_metric.py +0 -0
  63. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/bias.py +0 -0
  64. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/coverage.py +0 -0
  65. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/cv.py +0 -0
  66. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/interval_score.py +0 -0
  67. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/interval_width.py +0 -0
  68. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/maape.py +0 -0
  69. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/mae.py +0 -0
  70. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/mape.py +0 -0
  71. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/mase.py +0 -0
  72. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/mase_seasonal.py +0 -0
  73. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/mse.py +0 -0
  74. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/msis.py +0 -0
  75. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/msse.py +0 -0
  76. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/ope.py +0 -0
  77. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/pinball.py +0 -0
  78. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/r2.py +0 -0
  79. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/rmse.py +0 -0
  80. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/rmsle.py +0 -0
  81. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/rmsse.py +0 -0
  82. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/smape.py +0 -0
  83. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/wape.py +0 -0
  84. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/__init__.py +0 -0
  85. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/_foundation_base.py +0 -0
  86. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/_lag_forecaster.py +0 -0
  87. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/_neuralforecast_base.py +0 -0
  88. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/_vertex_automl_base.py +0 -0
  89. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/auto_arima.py +0 -0
  90. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/auto_ces.py +0 -0
  91. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/auto_theta.py +0 -0
  92. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/autoets.py +0 -0
  93. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/base_model.py +0 -0
  94. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/bigquery_native.py +0 -0
  95. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/catboost_model.py +0 -0
  96. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/chronos2.py +0 -0
  97. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/chronos_bolt.py +0 -0
  98. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/croston.py +0 -0
  99. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/fft.py +0 -0
  100. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/holtwinters.py +0 -0
  101. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/kalman.py +0 -0
  102. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/lightgbm_model.py +0 -0
  103. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/naive_drift.py +0 -0
  104. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/naive_mean.py +0 -0
  105. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/naive_moving_average.py +0 -0
  106. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/naive_seasonal.py +0 -0
  107. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/neuralprophet_model.py +0 -0
  108. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/patchtst.py +0 -0
  109. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/prophet_model.py +0 -0
  110. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/random_forest.py +0 -0
  111. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/regression_lags.py +0 -0
  112. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/sarimax.py +0 -0
  113. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/stl_bagging.py +0 -0
  114. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/tbats_model.py +0 -0
  115. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/tft.py +0 -0
  116. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/theta.py +0 -0
  117. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/tide.py +0 -0
  118. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/timesfm_torch.py +0 -0
  119. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/tsmixer.py +0 -0
  120. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/ucm.py +0 -0
  121. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/vertex_l2l.py +0 -0
  122. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/vertex_seq2seq.py +0 -0
  123. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/vertex_tft.py +0 -0
  124. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/vertex_tide.py +0 -0
  125. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/xgboost_model.py +0 -0
  126. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/notebook_acceptance.py +0 -0
  127. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/playground.py +0 -0
  128. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/probes/__init__.py +0 -0
  129. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/probes/cancel.py +0 -0
  130. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/probes/reconcile.py +0 -0
  131. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/probes/settle.py +0 -0
  132. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/probes/vocabulary.py +0 -0
  133. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/__init__.py +0 -0
  134. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/baseline.py +0 -0
  135. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/cost.py +0 -0
  136. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/measure.py +0 -0
  137. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/numeric.py +0 -0
  138. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/sampling.py +0 -0
  139. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/signature.py +0 -0
  140. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/source.py +0 -0
  141. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/stats.py +0 -0
  142. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/py.typed +0 -0
  143. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/quota.py +0 -0
  144. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ray_cluster.py +0 -0
  145. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ray_entry.py +0 -0
  146. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ray_infra.py +0 -0
  147. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ray_jobs.py +0 -0
  148. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ray_reaper.py +0 -0
  149. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ray_telemetry.py +0 -0
  150. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/reconciliation.py +0 -0
  151. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/__init__.py +0 -0
  152. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/artifacts.py +0 -0
  153. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/cells.py +0 -0
  154. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/ddl.py +0 -0
  155. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/harvest.py +0 -0
  156. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/header.py +0 -0
  157. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/ids.py +0 -0
  158. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/jobs.py +0 -0
  159. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/lifecycle.py +0 -0
  160. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/ops.py +0 -0
  161. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/params.py +0 -0
  162. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/reads.py +0 -0
  163. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/rows.py +0 -0
  164. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/tables.py +0 -0
  165. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/views.py +0 -0
  166. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/write_api.py +0 -0
  167. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/report.py +0 -0
  168. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/resources/__init__.py +0 -0
  169. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/resources/audit.py +0 -0
  170. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/resources/catalog.py +0 -0
  171. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/resources/cluster.py +0 -0
  172. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/resources/fleet.py +0 -0
  173. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/resources/serverless.py +0 -0
  174. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/resources/slot.py +0 -0
  175. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/retry_policy.py +0 -0
  176. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/retry_run.py +0 -0
  177. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/review.py +0 -0
  178. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/router.py +0 -0
  179. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/scenarios.py +0 -0
  180. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/sdk.py +0 -0
  181. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/seasonality.py +0 -0
  182. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/settings.py +0 -0
  183. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/shared_clusters.py +0 -0
  184. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/smoke_run.py +0 -0
  185. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/spark_entry.py +0 -0
  186. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/staging.py +0 -0
  187. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/submit.py +0 -0
  188. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/submitters.py +0 -0
  189. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/validation.py +0 -0
  190. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/vertex_entry.py +0 -0
  191. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/vertex_submit.py +0 -0
  192. {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/worker.py +0 -0
@@ -11,6 +11,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ---
13
13
 
14
+ ## [1.2.1] - 2026-10-14
15
+
16
+ ### Added
17
+
18
+ - **Atomic Version Bump Automation (`make bump VERSION=X.Y.Z`):**
19
+ - Added [`bump_version()`](./src/scale_forecasting/agent_surfaces.py) (`python -m scale_forecasting.agent_surfaces --bump-version X.Y.Z`) and `make bump VERSION=X.Y.Z` in [`Makefile`](./Makefile) to atomically update all 7 version-locked files ([`pyproject.toml`](./pyproject.toml), [`src/scale_forecasting/__init__.py`](./src/scale_forecasting/__init__.py), [`plugin.json`](./plugin.json), [`skills/scale-forecasting/SKILL.md`](./skills/scale-forecasting/SKILL.md), [`CHANGELOG.md`](./CHANGELOG.md), [`CITATION.cff`](./CITATION.cff), and [`docker/cloudbuild.yaml`](./docker/cloudbuild.yaml)), refresh `uv.lock` and `docker/requirements.txt`, sync the editable `.venv` `.dist-info`, and regenerate all 6 AI agent surface files.
20
+ - **Version-Tagged Runtime Containers (`:vX.Y.Z`) & Launcher $\leftrightarrow$ Worker Version Parity Guard (`code_delivery.py`):**
21
+ - [`docker/cloudbuild.yaml`](./docker/cloudbuild.yaml) and `make image` in [`Makefile`](./Makefile) now tag and push both `:latest` and `:v<version>` (`:v1.2.1`) to Artifact Registry alongside the content-addressed `venv_archive` tarball.
22
+ - Added [`verify_version_parity()`](./src/scale_forecasting/code_delivery.py), wired into `spark_explode._stamp_executed_fanout` and `ray_engine._stamp_executed_sizing` (shared across `ray`, `vertex`, `gce`, and `gke`), stamping `launcher` and `worker` `scale_forecasting.__version__` (`$.version`) into `run_registry.job_telemetry` and raising `ConfigError` on mismatch when `SF_STRICT_VERSION_PARITY=1`.
23
+ - **Complete 7-Runtime Provisioning vs. Execution Telemetry (`$.platform`):**
24
+ - Extended [`batch_telemetry.platform_phases()`](./src/scale_forecasting/batch_telemetry.py) (`_to_epoch_seconds` / `_rfc3339_seconds`) to accept protobuf `Timestamp`, `datetime`, epoch seconds/milliseconds, and RFC 3339 strings, and wired `$.platform` (`provision_s`, `execute_s`, `total_wall_s`) into `RayProbe`, `GceProbe`, `GkeProbe`, and `VertexAutoMLProbe` in [`src/scale_forecasting/probes/runtimes.py`](./src/scale_forecasting/probes/runtimes.py) as well as ephemeral submitters ([`gce_submit.py`](./src/scale_forecasting/gce_submit.py), [`gke_submit.py`](./src/scale_forecasting/gke_submit.py), [`ray_submit.py`](./src/scale_forecasting/ray_submit.py), [`cluster_submit.py`](./src/scale_forecasting/cluster_submit.py), [`automl_submit.py`](./src/scale_forecasting/automl_submit.py)) so `v_run_jobs.provision_seconds` and `execute_seconds` are populated across all compute runtimes.
25
+ - **Cross-Version BigQuery Registry Compatibility Tripwire ([`tests/unit/test_cross_version_resilience.py`](./tests/unit/test_cross_version_resilience.py)):**
26
+ - Verifies backward and forward compatibility of `Forecaster.from_run_id()`, `ops.match_reusable_models()` & `render_borrow_model_sql()`, `review._assemble_review()` & DataFrame builders, and `diagnostics.py` (`compute_fva`, `compute_inventory_policy`, `compute_anomalies`, `compute_change_points`) across historical `v1.0.0`, `v1.1.0`, and `v1.2.0` registry payloads.
27
+
28
+ ### Changed
29
+
30
+ - **Interactive Showcase Notebook Refresh ([`notebooks/01_bigquery_native_sql.ipynb`](./notebooks/01_bigquery_native_sql.ipynb) & [`notebooks/08_multi_engine_master_workflow.ipynb`](./notebooks/08_multi_engine_master_workflow.ipynb)):**
31
+ - Re-executed and persisted `01_bigquery_native_sql.ipynb` (`arima_plus`, `arima_plus_xreg`, `timesfm` `TimesFM 3.0` multivariate, `timesfm@univariate` `TimesFM 2.5`, Covariate FVA, Two-Tier `ML.EXPLAIN_FORECAST` attributions, anomaly/change-point detection, and $(s, S)$ inventory replenishment policy) and `08_multi_engine_master_workflow.ipynb` (4-family parallel DAG with `[models-foundation]` `chronos_bolt`, FVA, TreeSHAP attributions, `v_run_jobs` provisioning telemetry, anomaly/change-point detection, and inventory replenishment policy) against the `v1.2.1` runtime container.
32
+
33
+ ---
34
+
14
35
  ## [1.2.0] - 2026-10-14
15
36
 
16
37
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: scale-forecasting
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Time-series forecasting on Google Cloud at any scale: one JSON configuration runs every model family on Dataproc Serverless, Ray, Vertex AI, GKE, Compute Engine, or BigQuery ML and writes forecasts and leaderboards back to BigQuery.
5
5
  Project-URL: Homepage, https://github.com/statmike/scale-forecasting
6
6
  Project-URL: Documentation, https://statmike.github.io/scale-forecasting/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scale-forecasting"
3
- version = "1.2.0"
3
+ version = "1.2.1"
4
4
  description = "Time-series forecasting on Google Cloud at any scale: one JSON configuration runs every model family on Dataproc Serverless, Ray, Vertex AI, GKE, Compute Engine, or BigQuery ML and writes forecasts and leaderboards back to BigQuery."
5
5
  dynamic = ["readme"]
6
6
  # Two different pins, on purpose. `requires-python` is what *the package* installs on: the pure
@@ -53,7 +53,7 @@ from .settings import Settings
53
53
  try:
54
54
  __version__: str = _pkg_version("scale-forecasting")
55
55
  except PackageNotFoundError: # pragma: no cover - zip-staged cloud worker without .dist-info
56
- __version__ = "1.2.0"
56
+ __version__ = "1.2.1"
57
57
 
58
58
  # Lazy (heavy): name -> (module, attribute). Resolved on first access by ``__getattr__`` (PEP 562)
59
59
  # so the model modules (statsmodels et al., ~5s) load only when a caller actually reaches for the
@@ -23,6 +23,7 @@ CLI usage::
23
23
  python -m scale_forecasting.agent_surfaces --write
24
24
  python -m scale_forecasting.agent_surfaces --check
25
25
  python -m scale_forecasting.agent_surfaces --probe-env
26
+ python -m scale_forecasting.agent_surfaces --bump-version X.Y.Z
26
27
  """
27
28
 
28
29
  from __future__ import annotations
@@ -30,6 +31,7 @@ from __future__ import annotations
30
31
  import argparse
31
32
  import json
32
33
  import os
34
+ import re
33
35
  import sys
34
36
  from importlib.metadata import version as _pkg_version
35
37
  from pathlib import Path
@@ -73,6 +75,7 @@ __all__ = [
73
75
  "GPU_CATALOG",
74
76
  "RUNTIME_CATALOG",
75
77
  "build_run_config_json_schema",
78
+ "bump_version",
76
79
  "check_agent_surfaces",
77
80
  "expected_agent_surface_files",
78
81
  "main",
@@ -1530,6 +1533,138 @@ def check_agent_surfaces(repo_root: Path | None = None) -> list[str]:
1530
1533
  return drifted
1531
1534
 
1532
1535
 
1536
+ _SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+(?:[a-zA-Z0-9.\-+]+)?$")
1537
+
1538
+
1539
+ def bump_version(
1540
+ new_version: str,
1541
+ repo_root: Path | None = None,
1542
+ *,
1543
+ release_date: str | None = None,
1544
+ ) -> list[Path]:
1545
+ """Atomically update the package version across all version-locked repository files.
1546
+
1547
+ Updates:
1548
+ 1. ``pyproject.toml`` (``[project].version``)
1549
+ 2. ``src/scale_forecasting/__init__.py`` (``__version__`` fallback)
1550
+ 3. ``plugin.json`` (``"version"``)
1551
+ 4. ``skills/scale-forecasting/SKILL.md`` (``metadata.version``)
1552
+ 5. ``CITATION.cff`` (``version`` and ``date-released``)
1553
+ 6. ``CHANGELOG.md`` (inserts ``## [<version>] - <date>`` section if not already present)
1554
+ 7. ``docker/cloudbuild.yaml`` (``_VERSION_TAG: "v<version>"`` when present)
1555
+ 8. All 6 generated agent surface files via `write_agent_surfaces` when ``mkdocs.yml`` exists.
1556
+
1557
+ Returns the list of modified file paths.
1558
+ """
1559
+ ver = new_version.strip().lstrip("v")
1560
+ if not _SEMVER_RE.match(ver):
1561
+ raise ValueError(f"Invalid semantic version {new_version!r}; expected 'MAJOR.MINOR.PATCH'.")
1562
+ root = repo_root or _default_repo_root()
1563
+ from datetime import UTC, datetime
1564
+
1565
+ rel_date = release_date or datetime.now(UTC).strftime("%Y-%m-%d")
1566
+ updated: list[Path] = []
1567
+
1568
+ pyproject_path = root / "pyproject.toml"
1569
+ if pyproject_path.is_file():
1570
+ text = pyproject_path.read_text(encoding="utf-8")
1571
+ new_text, n = re.subn(
1572
+ r'(?m)^version\s*=\s*"[^"]+"',
1573
+ f'version = "{ver}"',
1574
+ text,
1575
+ count=1,
1576
+ )
1577
+ if n:
1578
+ pyproject_path.write_text(new_text, encoding="utf-8")
1579
+ updated.append(pyproject_path)
1580
+
1581
+ init_path = root / "src" / "scale_forecasting" / "__init__.py"
1582
+ if init_path.is_file():
1583
+ text = init_path.read_text(encoding="utf-8")
1584
+ new_text, n = re.subn(
1585
+ r'(?m)^(\s*__version__\s*=\s*)"[^"]+"',
1586
+ rf'\1"{ver}"',
1587
+ text,
1588
+ count=1,
1589
+ )
1590
+ if n:
1591
+ init_path.write_text(new_text, encoding="utf-8")
1592
+ updated.append(init_path)
1593
+
1594
+ plugin_path = root / "plugin.json"
1595
+ if plugin_path.is_file():
1596
+ plugin_data = json.loads(plugin_path.read_text(encoding="utf-8"))
1597
+ plugin_data["version"] = ver
1598
+ plugin_path.write_text(json.dumps(plugin_data, indent=2) + "\n", encoding="utf-8")
1599
+ updated.append(plugin_path)
1600
+
1601
+ skill_path = root / "skills" / "scale-forecasting" / "SKILL.md"
1602
+ if skill_path.is_file():
1603
+ text = skill_path.read_text(encoding="utf-8")
1604
+ new_text, n = re.subn(
1605
+ r"(?m)^(\s*version:\s*)\S+",
1606
+ rf"\g<1>{ver}",
1607
+ text,
1608
+ count=1,
1609
+ )
1610
+ if n:
1611
+ skill_path.write_text(new_text, encoding="utf-8")
1612
+ updated.append(skill_path)
1613
+
1614
+ citation_path = root / "CITATION.cff"
1615
+ if citation_path.is_file():
1616
+ text = citation_path.read_text(encoding="utf-8")
1617
+ new_text = re.sub(
1618
+ r'(?m)^version:\s*"[^"]+"',
1619
+ f'version: "{ver}"',
1620
+ text,
1621
+ count=1,
1622
+ )
1623
+ new_text = re.sub(
1624
+ r'(?m)^date-released:\s*"[^"]+"',
1625
+ f'date-released: "{rel_date}"',
1626
+ new_text,
1627
+ count=1,
1628
+ )
1629
+ citation_path.write_text(new_text, encoding="utf-8")
1630
+ updated.append(citation_path)
1631
+
1632
+ changelog_path = root / "CHANGELOG.md"
1633
+ if changelog_path.is_file():
1634
+ text = changelog_path.read_text(encoding="utf-8")
1635
+ if f"## [{ver}]" not in text:
1636
+ stub = (
1637
+ f"## [Unreleased]\n\n---\n\n"
1638
+ f"## [{ver}] - {rel_date}\n\n"
1639
+ f"### Changed\n\n"
1640
+ f"- Release `{ver}`.\n\n---"
1641
+ )
1642
+ if "## [Unreleased]\n\n---" in text:
1643
+ new_text = text.replace("## [Unreleased]\n\n---", stub, 1)
1644
+ else:
1645
+ new_text = text + f"\n## [{ver}] - {rel_date}\n"
1646
+ changelog_path.write_text(new_text, encoding="utf-8")
1647
+ updated.append(changelog_path)
1648
+
1649
+ cloudbuild_path = root / "docker" / "cloudbuild.yaml"
1650
+ if cloudbuild_path.is_file():
1651
+ text = cloudbuild_path.read_text(encoding="utf-8")
1652
+ new_text, n = re.subn(
1653
+ r'(?m)^(\s*_VERSION_TAG:\s*)"[^"]+"',
1654
+ rf'\1"v{ver}"',
1655
+ text,
1656
+ count=1,
1657
+ )
1658
+ if n:
1659
+ cloudbuild_path.write_text(new_text, encoding="utf-8")
1660
+ updated.append(cloudbuild_path)
1661
+
1662
+ if (root / "mkdocs.yml").is_file():
1663
+ updated.extend(write_agent_surfaces(root))
1664
+
1665
+ return updated
1666
+
1667
+
1533
1668
  def main(argv: list[str] | None = None) -> int:
1534
1669
  """CLI entry point for generating, checking, or probing agent surfaces."""
1535
1670
  configure_cli_logging()
@@ -1553,12 +1688,23 @@ def main(argv: list[str] | None = None) -> int:
1553
1688
  action="store_true",
1554
1689
  help="Print a JSON report of installed extras, available models, and SF_* cloud readiness.",
1555
1690
  )
1691
+ group.add_argument(
1692
+ "--bump-version",
1693
+ metavar="X.Y.Z",
1694
+ help="Atomically bump package version across all version-locked files and agent surfaces.",
1695
+ )
1556
1696
  args = parser.parse_args(argv)
1557
1697
 
1558
1698
  if args.probe_env:
1559
1699
  print(json.dumps(probe_environment(), indent=2, sort_keys=True))
1560
1700
  return 0
1561
1701
 
1702
+ if args.bump_version:
1703
+ updated = bump_version(args.bump_version)
1704
+ for path in updated:
1705
+ print(f"updated {path}")
1706
+ return 0
1707
+
1562
1708
  if args.write:
1563
1709
  written = write_agent_surfaces()
1564
1710
  for path in written:
@@ -239,7 +239,10 @@ def submit_automl(
239
239
  },
240
240
  )
241
241
 
242
+ import time
243
+
242
244
  _ = wait
245
+ t_start = time.monotonic()
243
246
  outcome = automl_engine.run(
244
247
  cfg,
245
248
  models=automl_models,
@@ -247,6 +250,7 @@ def submit_automl(
247
250
  settings=settings,
248
251
  job_id=plan.display_name,
249
252
  )
253
+ wall_s = round(max(0.0, time.monotonic() - t_start), 1)
250
254
 
251
255
  # If the pipeline produced a concrete resource_name or pipeline_job_id, update the handle.
252
256
  models_telem = outcome.get("models") or {}
@@ -263,6 +267,12 @@ def submit_automl(
263
267
  region=settings.region,
264
268
  resource_name=str(resource_name) if resource_name else None,
265
269
  )
270
+ prov_s = first_telem.get("provision_s")
271
+ exec_s = first_telem.get("execute_s")
272
+ if exec_s is None:
273
+ exec_s = wall_s
274
+ if prov_s is None:
275
+ prov_s = round(max(0.0, wall_s - float(exec_s)), 1)
266
276
  if job_id is not None:
267
277
  with contextlib.suppress(Exception):
268
278
  update_job(
@@ -270,6 +280,11 @@ def submit_automl(
270
280
  settings=settings,
271
281
  merge_telemetry={
272
282
  "probe_handle": final_handle.to_blob(),
283
+ "platform": {
284
+ "provision_s": prov_s,
285
+ "execute_s": exec_s,
286
+ "total_wall_s": wall_s,
287
+ },
273
288
  "vertex_automl": {
274
289
  **plan.to_dict(),
275
290
  "models_telemetry": models_telem,
@@ -37,19 +37,50 @@ def _batch_client(region: str) -> BatchControllerClient:
37
37
  )
38
38
 
39
39
 
40
+ def _to_epoch_seconds(val: object) -> float | None:
41
+ """Coerce a protobuf ``Timestamp``, ``datetime``, epoch int/float, or ISO-8601 string."""
42
+ if val is None or isinstance(val, bool):
43
+ return None
44
+ ts_fn = getattr(val, "timestamp", None)
45
+ if callable(ts_fn):
46
+ try:
47
+ res = ts_fn()
48
+ return float(res) if isinstance(res, int | float) else None
49
+ except Exception: # noqa: BLE001 - telemetry is best-effort, never fatal
50
+ return None
51
+ if isinstance(val, int | float):
52
+ fval = float(val)
53
+ # Ray JobInfo timestamps are epoch milliseconds (> 1e11 for modern dates).
54
+ return fval / 1000.0 if fval > 1e11 else fval
55
+ if isinstance(val, str) and val.strip():
56
+ from datetime import datetime
57
+
58
+ raw = val.strip()
59
+ try:
60
+ return (
61
+ float(raw)
62
+ if raw.replace(".", "", 1).isdigit()
63
+ else datetime.fromisoformat(raw.replace("Z", "+00:00")).timestamp()
64
+ )
65
+ except Exception: # noqa: BLE001 - malformed timestamp string degrades cleanly
66
+ return None
67
+ return None
68
+
69
+
40
70
  def _rfc3339_seconds(a: object, b: object) -> float | None:
41
- """Whole seconds between two Dataproc timestamp fields (``b - a``), or None.
71
+ """Seconds between two timestamps (``b - a``), or None.
42
72
 
43
- Dataproc stamps ``create_time``/``state_time`` as ``google.protobuf.Timestamp``; both expose
44
- ``.timestamp()`` (via the proto's datetime helper). Returns None if either is missing so a
45
- partial batch object degrades cleanly rather than raising.
73
+ Accepts ``google.protobuf.Timestamp`` / ``datetime`` (``.timestamp()``), epoch seconds or
74
+ milliseconds (``int`` / ``float``), and ISO-8601 / RFC-3339 strings (such as Kubernetes
75
+ ``creationTimestamp`` / ``startTime`` / ``completionTime``). Returns None if either is missing
76
+ so a partial job object degrades cleanly rather than raising.
46
77
  """
47
- ts_a = getattr(a, "timestamp", None)
48
- ts_b = getattr(b, "timestamp", None)
49
- if not callable(ts_a) or not callable(ts_b):
78
+ ea = _to_epoch_seconds(a)
79
+ eb = _to_epoch_seconds(b)
80
+ if ea is None or eb is None:
50
81
  return None
51
82
  try:
52
- return round(ts_b() - ts_a(), 1)
83
+ return round(max(0.0, eb - ea), 1)
53
84
  except Exception: # noqa: BLE001 - telemetry is best-effort, never fatal
54
85
  return None
55
86
 
@@ -291,6 +291,9 @@ def submit_cluster_job(
291
291
  workers,
292
292
  )
293
293
 
294
+ import time
295
+
296
+ t_submit_start = time.perf_counter()
294
297
  if reuse:
295
298
  # A named cluster (standing, or the run's shared ephemeral): submit to its region — a shared
296
299
  # cluster may have hopped on a capacity failover, so trust the region the caller threads.
@@ -322,6 +325,8 @@ def submit_cluster_job(
322
325
 
323
326
  cluster_client = _cluster_client(region)
324
327
  job_client = _job_client(region)
328
+ t_job_start = time.perf_counter()
329
+ provision_s = round(max(0.0, t_job_start - t_submit_start), 1)
325
330
 
326
331
  # Before submit, so nothing this job writes can predate the liveness window the watchdog reads.
327
332
  since = launch_window_start()
@@ -355,6 +360,24 @@ def submit_cluster_job(
355
360
  # The fallback is kept for the case where we did name it and the response still came back
356
361
  # without one: the id is ours and we already know it, so there is no reason to return "".
357
362
  final_id = submitted_id or job_id or ""
363
+ if wait and job_id is not None:
364
+ import contextlib
365
+
366
+ from .registry.jobs import update_job
367
+
368
+ exec_s = round(max(0.0, time.perf_counter() - t_job_start), 1)
369
+ with contextlib.suppress(Exception):
370
+ update_job(
371
+ job_id,
372
+ settings=settings,
373
+ merge_telemetry={
374
+ "platform": {
375
+ "provision_s": provision_s,
376
+ "execute_s": exec_s,
377
+ "total_wall_s": round(max(0.0, provision_s + exec_s), 1),
378
+ }
379
+ },
380
+ )
358
381
  if wait and state_name != "DONE":
359
382
  raise EngineError(
360
383
  f"cluster job {final_id} terminal state {state_name}: {detail or '(no detail)'}"
@@ -194,3 +194,45 @@ def write_package_zip(dest_dir: str | Path | None = None) -> Path:
194
194
  out = base / f"scale_forecasting-{code_hash}.zip"
195
195
  out.write_bytes(data)
196
196
  return out
197
+
198
+
199
+ def verify_version_parity(
200
+ launcher_version: str | None = None,
201
+ worker_version: str | None = None,
202
+ *,
203
+ strict: bool = False,
204
+ ) -> dict[str, Any]:
205
+ """Compare launcher and worker ``scale_forecasting.__version__`` strings.
206
+
207
+ Returns a structured telemetry dict ``{"launcher": str, "worker": str, "match": bool}``.
208
+ When ``launcher_version`` or ``worker_version`` is omitted, resolves
209
+ ``scale_forecasting.__version__`` from the active process (or ``SF_LAUNCHER_VERSION`` in
210
+ ``os.environ`` for the launcher side on a remote worker). Logs a warning when the versions
211
+ differ and raises `ConfigError` when ``strict=True``.
212
+ """
213
+ import os
214
+
215
+ import scale_forecasting
216
+
217
+ from .errors import ConfigError, get_logger
218
+
219
+ current_ver = scale_forecasting.__version__
220
+ resolved_worker = (worker_version or current_ver).strip()
221
+ resolved_launcher = (
222
+ launcher_version or os.environ.get("SF_LAUNCHER_VERSION") or current_ver
223
+ ).strip()
224
+ match = resolved_launcher == resolved_worker
225
+ if not match:
226
+ msg = (
227
+ f"scale-forecasting version mismatch between launcher ({resolved_launcher!r}) and "
228
+ f"worker ({resolved_worker!r}); rebuild runtime image with `make image` or sync "
229
+ "package versions."
230
+ )
231
+ get_logger(__name__).warning(msg)
232
+ if strict:
233
+ raise ConfigError(msg)
234
+ return {
235
+ "launcher": resolved_launcher,
236
+ "worker": resolved_worker,
237
+ "match": match,
238
+ }
@@ -787,10 +787,12 @@ def _stamp_executed_sizing(
787
787
  inside a live run on the cluster head. A header that will not take an overlay costs a record,
788
788
  never a run.
789
789
  """
790
+ from ..code_delivery import verify_version_parity
790
791
  from ..registry.header import merge_header_telemetry
791
792
 
792
793
  try:
793
- merge_header_telemetry(run_id, patch, settings=settings)
794
+ parity = verify_version_parity()
795
+ merge_header_telemetry(run_id, {**patch, "version": parity}, settings=settings)
794
796
  except Exception as exc: # noqa: BLE001 - telemetry is an overlay, never fatal
795
797
  _log.warning("executed sizing capture failed (non-fatal): %r", exc)
796
798
 
@@ -100,11 +100,17 @@ def _stamp_executed_fanout(
100
100
  Best-effort in the same sense as every other telemetry write: this runs inside a live run, and
101
101
  a header that will not take an overlay is a lost record, never a lost run.
102
102
  """
103
+ from ..code_delivery import verify_version_parity
103
104
  from ..registry.header import executed_sizing_path, merge_header_telemetry
104
105
 
105
106
  try:
106
107
  path = executed_sizing_path(_fanout_family(executed))
107
- merge_header_telemetry(run_id, {path: {"fanout": fanout}}, settings=settings)
108
+ parity = verify_version_parity()
109
+ merge_header_telemetry(
110
+ run_id,
111
+ {path: {"fanout": fanout}, "version": parity},
112
+ settings=settings,
113
+ )
108
114
  except Exception as exc: # noqa: BLE001 - telemetry is an overlay, never fatal
109
115
  _log.warning("executed fan-out capture failed (non-fatal): %r", exc)
110
116
 
@@ -325,6 +325,8 @@ def build_gce_startup_script(
325
325
  f"STATUS_OBJECT_ENC={shlex.quote(encoded_obj)}\n"
326
326
  "LOG_FILE=/tmp/sf_container.log\n"
327
327
  ': > "$LOG_FILE"\n'
328
+ "BOOT_EPOCH=$(date +%s)\n"
329
+ "EXEC_START_EPOCH=$BOOT_EPOCH\n"
328
330
  "\n"
329
331
  "get_token() {\n"
330
332
  " curl -sf -H 'Metadata-Flavor: Google' \\\n"
@@ -337,12 +339,17 @@ def build_gce_startup_script(
337
339
  ' local st="$1"\n'
338
340
  ' local rc="$2"\n'
339
341
  ' local detail="$3"\n'
342
+ " local end_epoch\n"
343
+ " end_epoch=$(date +%s)\n"
344
+ " local exec_s=$((end_epoch - EXEC_START_EPOCH))\n"
340
345
  ' if [ -n "$STATUS_BUCKET" ]; then\n'
341
346
  " local tok\n"
342
347
  " tok=$(get_token || true)\n"
343
348
  ' printf \'{"status":"%s","exit_code":%s,"instance":"%s",'
344
- '"zone":"%s","detail":"%s"}\\n\' \\\n'
349
+ '"zone":"%s","detail":"%s","boot_epoch":%s,"exec_start_epoch":%s,'
350
+ '"end_epoch":%s,"execute_s":%s}\\n\' \\\n'
345
351
  ' "$st" "$rc" "$INSTANCE_NAME" "$ZONE" "$detail" \\\n'
352
+ ' "$BOOT_EPOCH" "$EXEC_START_EPOCH" "$end_epoch" "$exec_s" \\\n'
346
353
  ' | curl -sf -X POST -H "Authorization: Bearer ${tok}" \\\n'
347
354
  " -H 'Content-Type: application/json' --data-binary @- \\\n"
348
355
  ' "https://storage.googleapis.com/upload/storage/v1/b/'
@@ -378,6 +385,7 @@ def build_gce_startup_script(
378
385
  'echo "$TOKEN" | docker login -u oauth2accesstoken '
379
386
  f'--password-stdin {registry_url_q} >>"$LOG_FILE" 2>&1\n'
380
387
  f'docker pull {shlex.quote(plan.image_uri)} >>"$LOG_FILE" 2>&1\n'
388
+ "EXEC_START_EPOCH=$(date +%s)\n"
381
389
  f'{docker_cmd} >>"$LOG_FILE" 2>&1\n'
382
390
  )
383
391
 
@@ -399,12 +407,17 @@ def build_gce_instance_spec_dict(
399
407
  3. ``disks[0].autoDelete = True`` so the boot persistent disk never outlives the VM.
400
408
  """
401
409
  startup_script = build_gce_startup_script(plan, driver_args, project_id=project_id, zone=zone)
410
+ on_host_maintenance = (
411
+ "MIGRATE"
412
+ if plan.accelerator_count == 0 and str(plan.machine_type).startswith("e2-")
413
+ else "TERMINATE"
414
+ )
402
415
  spec: dict[str, Any] = {
403
416
  "name": plan.instance_name,
404
417
  "machineType": f"zones/{zone}/machineTypes/{plan.machine_type}",
405
418
  "scheduling": {
406
419
  "automaticRestart": False,
407
- "onHostMaintenance": "TERMINATE",
420
+ "onHostMaintenance": on_host_maintenance,
408
421
  "maxRunDuration": {"seconds": str(plan.max_run_duration_seconds)},
409
422
  "instanceTerminationAction": "DELETE",
410
423
  },
@@ -909,6 +922,7 @@ def submit_gce(
909
922
 
910
923
  if wait:
911
924
  assert chosen_zone is not None
925
+ t_poll_start = time.monotonic()
912
926
  try:
913
927
  marker = _poll_gce_instance(
914
928
  session,
@@ -920,6 +934,34 @@ def submit_gce(
920
934
  grace_s=int(infra.stall_grace_seconds),
921
935
  since=since,
922
936
  )
937
+ if effective_job_id is not None:
938
+ from .batch_telemetry import platform_phases
939
+
940
+ phases = platform_phases(
941
+ marker.get("create_epoch") or since or marker.get("boot_epoch"),
942
+ marker.get("exec_start_epoch") or marker.get("boot_epoch"),
943
+ marker.get("end_epoch"),
944
+ )
945
+ if phases.get("execute_s") is None and isinstance(
946
+ marker.get("execute_s"), int | float
947
+ ):
948
+ phases["execute_s"] = round(float(marker["execute_s"]), 1)
949
+ if phases.get("total_wall_s") is None:
950
+ phases["total_wall_s"] = round(max(0.0, time.monotonic() - t_poll_start), 1)
951
+ exec_val = phases.get("execute_s")
952
+ wall_val = phases.get("total_wall_s")
953
+ if (
954
+ phases.get("provision_s") is None
955
+ and exec_val is not None
956
+ and wall_val is not None
957
+ ):
958
+ phases["provision_s"] = round(max(0.0, wall_val - exec_val), 1)
959
+ with contextlib.suppress(Exception):
960
+ update_job(
961
+ effective_job_id,
962
+ settings=settings,
963
+ merge_telemetry={"platform": phases},
964
+ )
923
965
  if marker.get("status") != "SUCCEEDED":
924
966
  detail = marker.get("detail") or f"exit_code={marker.get('exit_code')}"
925
967
  raise EngineError(
@@ -1292,6 +1292,8 @@ def submit_gke(
1292
1292
  created_ephemeral_cluster = False
1293
1293
  landed_location = cluster_location or settings.region
1294
1294
  cluster_obj: dict[str, Any] | None = None
1295
+ t_submit_start = time.monotonic()
1296
+ t_job_start: float | None = None
1295
1297
 
1296
1298
  if not plan.ephemeral_cluster:
1297
1299
  cluster_obj = get_gke_cluster(
@@ -1429,6 +1431,7 @@ def submit_gke(
1429
1431
  if not wait:
1430
1432
  return run_id, plan.job_name, handle
1431
1433
 
1434
+ t_job_start = time.monotonic()
1432
1435
  timeout_s = (
1433
1436
  wait_timeout_seconds
1434
1437
  if wait_timeout_seconds is not None
@@ -1444,6 +1447,29 @@ def submit_gke(
1444
1447
  while time.monotonic() < deadline:
1445
1448
  cur_job = k8s.get_job(plan.namespace, plan.job_name)
1446
1449
  state, detail = k8s_job_state(cur_job)
1450
+ if state in {"SUCCEEDED", "FAILED"} and job_id:
1451
+ from .batch_telemetry import platform_phases
1452
+
1453
+ t_end = time.monotonic()
1454
+ meta = (cur_job or {}).get("metadata") or {}
1455
+ st = (cur_job or {}).get("status") or {}
1456
+ phases = platform_phases(
1457
+ (cluster_obj or {}).get("createTime") or meta.get("creationTimestamp"),
1458
+ st.get("startTime") or meta.get("creationTimestamp"),
1459
+ st.get("completionTime"),
1460
+ )
1461
+ if phases.get("provision_s") is None and t_job_start is not None:
1462
+ phases["provision_s"] = round(max(0.0, t_job_start - t_submit_start), 1)
1463
+ if phases.get("execute_s") is None and t_job_start is not None:
1464
+ phases["execute_s"] = round(max(0.0, t_end - t_job_start), 1)
1465
+ if phases.get("total_wall_s") is None:
1466
+ phases["total_wall_s"] = round(max(0.0, t_end - t_submit_start), 1)
1467
+ with contextlib.suppress(Exception):
1468
+ update_job(
1469
+ job_id,
1470
+ settings=settings,
1471
+ merge_telemetry={"platform": phases},
1472
+ )
1447
1473
  if state == "SUCCEEDED":
1448
1474
  _log.info(
1449
1475
  "gke job %s SUCCEEDED on cluster %s (%s)",