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.
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/CHANGELOG.md +21 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/PKG-INFO +1 -1
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/pyproject.toml +1 -1
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/__init__.py +1 -1
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/agent_surfaces.py +146 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/automl_submit.py +15 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/batch_telemetry.py +39 -8
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/cluster_submit.py +23 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/code_delivery.py +42 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/ray_engine.py +3 -1
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/spark_explode.py +7 -1
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/gce_submit.py +44 -2
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/gke_submit.py +26 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/probes/runtimes.py +48 -8
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ray_submit.py +21 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/.gitignore +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/LICENSE +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/README.md +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/_entry.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/_infra_args.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/airflow_emit.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/airflow_tasks.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/backtest.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/batch_infra.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/calibration.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/capacity.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/cluster_deps.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/cluster_telemetry.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/commands.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/compute_fallback.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/config.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/dag.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/data_gen/__init__.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/data_gen/generator.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/data_gen/seed_spark.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/dataproc_cluster.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/device_audit.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/diagnostics.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/__init__.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/automl_engine.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/bigquery_engine.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/bigquery_names.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/bigquery_sql.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/ray_io.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/spark_io.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/vertex_engine.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ensemble_run.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ensembler.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/errors.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/features.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/hardware.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/hpo.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/identity.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/job_launch.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/job_outcome.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/job_wait.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/launch_plan.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/main.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/mcp.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/__init__.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/_intervals.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/base_metric.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/bias.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/coverage.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/cv.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/interval_score.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/interval_width.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/maape.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/mae.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/mape.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/mase.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/mase_seasonal.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/mse.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/msis.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/msse.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/ope.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/pinball.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/r2.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/rmse.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/rmsle.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/rmsse.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/smape.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/metrics/wape.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/__init__.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/_foundation_base.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/_lag_forecaster.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/_neuralforecast_base.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/_vertex_automl_base.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/auto_arima.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/auto_ces.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/auto_theta.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/autoets.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/base_model.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/bigquery_native.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/catboost_model.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/chronos2.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/chronos_bolt.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/croston.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/fft.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/holtwinters.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/kalman.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/lightgbm_model.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/naive_drift.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/naive_mean.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/naive_moving_average.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/naive_seasonal.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/neuralprophet_model.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/patchtst.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/prophet_model.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/random_forest.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/regression_lags.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/sarimax.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/stl_bagging.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/tbats_model.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/tft.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/theta.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/tide.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/timesfm_torch.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/tsmixer.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/ucm.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/vertex_l2l.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/vertex_seq2seq.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/vertex_tft.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/vertex_tide.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/models/xgboost_model.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/notebook_acceptance.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/playground.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/probes/__init__.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/probes/cancel.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/probes/reconcile.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/probes/settle.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/probes/vocabulary.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/__init__.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/baseline.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/cost.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/measure.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/numeric.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/sampling.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/signature.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/source.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/profiling/stats.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/py.typed +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/quota.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ray_cluster.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ray_entry.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ray_infra.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ray_jobs.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ray_reaper.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/ray_telemetry.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/reconciliation.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/__init__.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/artifacts.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/cells.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/ddl.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/harvest.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/header.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/ids.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/jobs.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/lifecycle.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/ops.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/params.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/reads.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/rows.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/tables.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/views.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/registry/write_api.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/report.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/resources/__init__.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/resources/audit.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/resources/catalog.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/resources/cluster.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/resources/fleet.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/resources/serverless.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/resources/slot.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/retry_policy.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/retry_run.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/review.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/router.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/scenarios.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/sdk.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/seasonality.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/settings.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/shared_clusters.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/smoke_run.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/spark_entry.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/staging.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/submit.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/submitters.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/validation.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/vertex_entry.py +0 -0
- {scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/vertex_submit.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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,
|
{scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/batch_telemetry.py
RENAMED
|
@@ -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
|
-
"""
|
|
71
|
+
"""Seconds between two timestamps (``b - a``), or None.
|
|
42
72
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
if
|
|
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(
|
|
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
|
+
}
|
{scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/ray_engine.py
RENAMED
|
@@ -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
|
-
|
|
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
|
|
{scale_forecasting-1.2.0 → scale_forecasting-1.2.1}/src/scale_forecasting/engines/spark_explode.py
RENAMED
|
@@ -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
|
-
|
|
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"
|
|
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":
|
|
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)",
|