deployml-core 0.0.58__tar.gz → 0.0.60__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 (107) hide show
  1. deployml_core-0.0.60/PKG-INFO +117 -0
  2. deployml_core-0.0.60/README.md +92 -0
  3. {deployml_core-0.0.58 → deployml_core-0.0.60}/pyproject.toml +1 -1
  4. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/cli/cli.py +187 -18
  5. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/fastapi/Dockerfile +3 -2
  6. deployml_core-0.0.60/src/deployml/docker/fastapi/main.py +167 -0
  7. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/fastapi/requirements.txt +3 -1
  8. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/mlflow/Dockerfile +3 -2
  9. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_run/mlflow_main.tf.j2 +32 -38
  10. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/bigquery/cloud/gcp/main.tf +4 -0
  11. deployml_core-0.0.60/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/offline_features.json +12 -0
  12. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/cloud_sql_postgres/main.tf +3 -4
  13. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/main.tf +5 -1
  14. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/variables.tf +7 -1
  15. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/main.tf +12 -1
  16. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/helpers.py +53 -97
  17. deployml_core-0.0.58/PKG-INFO +0 -111
  18. deployml_core-0.0.58/README.md +0 -87
  19. deployml_core-0.0.58/src/deployml/docker/fastapi/main.py +0 -127
  20. deployml_core-0.0.58/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/offline_features.json +0 -7
  21. {deployml_core-0.0.58 → deployml_core-0.0.60}/LICENSE +0 -0
  22. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/__init__.py +0 -0
  23. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/api.py +0 -0
  24. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/cli/__init__.py +0 -0
  25. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/diagnostics/__init__.py +0 -0
  26. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/diagnostics/doctor.py +0 -0
  27. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/fastapi/.dockerignore +0 -0
  28. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/grafana-container/Dockerfile +0 -0
  29. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/grafana-container/README.md +0 -0
  30. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/grafana-container/entrypoint.sh +0 -0
  31. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/mlflow/requirements.txt +0 -0
  32. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/enum/__init__.py +0 -0
  33. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/enum/cloud_provider.py +0 -0
  34. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/enum/deployment_type.py +0 -0
  35. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/notebook/__init__.py +0 -0
  36. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/notebook/deployment.py +0 -0
  37. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/notebook/display.py +0 -0
  38. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/notebook/docker.py +0 -0
  39. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/notebook/stack.py +0 -0
  40. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/notebook/urls.py +0 -0
  41. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/notebook.py +0 -0
  42. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_run/base_main.tf.j2 +0 -0
  43. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_run/main.tf.j2 +0 -0
  44. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_run/terraform.tfvars.j2 +0 -0
  45. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_run/variables.tf.j2 +0 -0
  46. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_run/wandb_main.tf.j2 +0 -0
  47. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_vm/main.tf.j2 +0 -0
  48. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_vm/terraform.tfvars.j2 +0 -0
  49. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_vm/variables.tf.j2 +0 -0
  50. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/kubernetes_local/deployment.yaml.j2 +0 -0
  51. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/kubernetes_local/mlflow-deployment.yaml.j2 +0 -0
  52. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/kubernetes_local/mlflow-service.yaml.j2 +0 -0
  53. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/kubernetes_local/service.yaml.j2 +0 -0
  54. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/bigquery/cloud/gcp/outputs.tf +0 -0
  55. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/drift_metrics.json +0 -0
  56. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/ground_truth.json +0 -0
  57. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/predictions.json +0 -0
  58. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/bigquery/cloud/gcp/variables.tf +0 -0
  59. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/cloud_sql_postgres/outputs.tf +0 -0
  60. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/cloud_sql_postgres/variables.tf +0 -0
  61. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/main.tf +0 -0
  62. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/outputs.tf +0 -0
  63. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/variables.tf +0 -0
  64. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/main.tf +0 -0
  65. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/outputs.tf +0 -0
  66. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/variables.tf +0 -0
  67. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/main.tf +0 -0
  68. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/outputs.tf +0 -0
  69. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/variables.tf +0 -0
  70. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/outputs.tf +0 -0
  71. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/main.tf +0 -0
  72. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/outputs.tf +0 -0
  73. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/variables.tf +0 -0
  74. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/feast_env.tpl +0 -0
  75. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/main.tf +0 -0
  76. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/outputs.tf +0 -0
  77. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/variables.tf +0 -0
  78. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/main.tf +0 -0
  79. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/outputs.tf +0 -0
  80. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/variables.tf +0 -0
  81. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/outputs.tf +0 -0
  82. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/variables.tf +0 -0
  83. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_vm/main.tf +0 -0
  84. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_vm/variables.tf +0 -0
  85. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/main.tf +0 -0
  86. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/outputs.tf +0 -0
  87. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/variables.tf +0 -0
  88. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/teardown/cloud/gcp/cloud_function/main.py +0 -0
  89. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/teardown/cloud/gcp/cloud_function/requirements.txt +0 -0
  90. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/teardown/cloud/gcp/cloudbuild.yaml +0 -0
  91. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/teardown/cloud/gcp/main.tf +0 -0
  92. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/teardown/cloud/gcp/outputs.tf +0 -0
  93. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/teardown/cloud/gcp/teardown_script.sh +0 -0
  94. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/teardown/cloud/gcp/variables.tf +0 -0
  95. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/main.tf +0 -0
  96. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/outputs.tf +0 -0
  97. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/variables.tf +0 -0
  98. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_vm/main.tf +0 -0
  99. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_vm/variables.tf +0 -0
  100. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/__init__.py +0 -0
  101. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/banner.py +0 -0
  102. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/constants.py +0 -0
  103. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/infracost.py +0 -0
  104. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/kubernetes_gke.py +0 -0
  105. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/kubernetes_local.py +0 -0
  106. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/menu.py +0 -0
  107. {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/teardown.py +0 -0
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: deployml-core
3
+ Version: 0.0.60
4
+ Summary: Infra for academia
5
+ License-File: LICENSE
6
+ Author: Drew Hoang
7
+ Author-email: codentell@gmail.com
8
+ Requires-Python: >=3.11
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Requires-Dist: google-cloud-storage (>=3.1.1,<4.0.0)
15
+ Requires-Dist: ipython (>=9.0.0)
16
+ Requires-Dist: jinja2
17
+ Requires-Dist: jupyter (>=1.0.0)
18
+ Requires-Dist: pandas (>=2.2.1)
19
+ Requires-Dist: pyyaml
20
+ Requires-Dist: requests (>=2.28.0)
21
+ Requires-Dist: rich
22
+ Requires-Dist: typer
23
+ Description-Content-Type: text/markdown
24
+
25
+ # deployml
26
+
27
+ A CLI tool that deploys a production MLOps stack on GCP with a single command. Built for academic ML courses so students can focus on building models, not infrastructure.
28
+
29
+ ## What you get
30
+
31
+ - **MLflow** — experiment tracking, artifact storage, and model registry (backed by Cloud SQL Postgres + GCS)
32
+ - **FastAPI** — model serving endpoint that loads the latest registered model from MLflow automatically
33
+ - **Grafana** — monitoring dashboard connected to your metrics database
34
+ - **BigQuery** — `mlops` dataset with tables for features, predictions, ground truth, and drift metrics
35
+
36
+ All running on GCP Cloud Run — no servers to manage, scales to zero when idle.
37
+
38
+ ## Quick Start
39
+
40
+ **1. Install**
41
+
42
+ ```bash
43
+ pip install deployml-core
44
+ ```
45
+
46
+ **2. Initialize your GCP project** (enables APIs, creates Artifact Registry)
47
+
48
+ ```bash
49
+ deployml init --provider gcp --project-id YOUR_PROJECT_ID
50
+ ```
51
+
52
+ **3. Configure**
53
+
54
+ ```bash
55
+ cp config.example.yaml config.yaml
56
+ # Edit config.yaml and set your project_id
57
+ ```
58
+
59
+ **4. Build images**
60
+
61
+ ```bash
62
+ deployml build-images
63
+ ```
64
+
65
+ **5. Deploy**
66
+
67
+ ```bash
68
+ deployml deploy --verbose
69
+ ```
70
+
71
+ First deploy takes ~20 minutes (Cloud SQL provisioning). Subsequent deploys are 1–2 minutes.
72
+
73
+ **6. Get your URLs**
74
+
75
+ ```bash
76
+ deployml get-urls
77
+ ```
78
+
79
+ Prints service URLs and writes a `.env` file with `MLFLOW_URL`, `FASTAPI_URL`, `GRAFANA_URL`, `BIGQUERY_PROJECT`, and `BIGQUERY_DATASET`.
80
+
81
+ ## End-to-End Example
82
+
83
+ Once deployed, the `example/` directory walks through a complete MLOps workflow using a synthetic housing price dataset:
84
+
85
+ ```bash
86
+ pip install mlflow scikit-learn pandas numpy google-cloud-bigquery db-dtypes python-dotenv requests
87
+
88
+ python example/scripts/01_load_training_data.py # load 500 rows into BigQuery
89
+ python example/scripts/02_train_model.py # train RandomForest, log to MLflow
90
+ python example/scripts/03_register_model.py # register model as Production
91
+ python example/scripts/04_make_predictions.py # serve 50 predictions via FastAPI
92
+ python example/scripts/05_generate_ground_truth.py # simulate actual outcomes
93
+ python example/scripts/06_compute_drift_metrics.py # compute feature drift + MAE
94
+ python example/scripts/07_setup_grafana.py # provision monitoring dashboard
95
+ ```
96
+
97
+ See [example/README.md](example/README.md) for details.
98
+
99
+ ## Teardown
100
+
101
+ ```bash
102
+ deployml destroy
103
+ ```
104
+
105
+ Deletes all Cloud Run services, Cloud SQL, GCS bucket, and BigQuery dataset. Does not delete Artifact Registry images or the GCP project.
106
+
107
+ ## Full Tutorial
108
+
109
+ See [docs/tutorials/gcp-cloud-run.md](docs/tutorials/gcp-cloud-run.md) for a step-by-step walkthrough.
110
+
111
+ ## Requirements
112
+
113
+ - Python 3.10+
114
+ - `gcloud` CLI (authenticated)
115
+ - Docker (running)
116
+ - Terraform
117
+
@@ -0,0 +1,92 @@
1
+ # deployml
2
+
3
+ A CLI tool that deploys a production MLOps stack on GCP with a single command. Built for academic ML courses so students can focus on building models, not infrastructure.
4
+
5
+ ## What you get
6
+
7
+ - **MLflow** — experiment tracking, artifact storage, and model registry (backed by Cloud SQL Postgres + GCS)
8
+ - **FastAPI** — model serving endpoint that loads the latest registered model from MLflow automatically
9
+ - **Grafana** — monitoring dashboard connected to your metrics database
10
+ - **BigQuery** — `mlops` dataset with tables for features, predictions, ground truth, and drift metrics
11
+
12
+ All running on GCP Cloud Run — no servers to manage, scales to zero when idle.
13
+
14
+ ## Quick Start
15
+
16
+ **1. Install**
17
+
18
+ ```bash
19
+ pip install deployml-core
20
+ ```
21
+
22
+ **2. Initialize your GCP project** (enables APIs, creates Artifact Registry)
23
+
24
+ ```bash
25
+ deployml init --provider gcp --project-id YOUR_PROJECT_ID
26
+ ```
27
+
28
+ **3. Configure**
29
+
30
+ ```bash
31
+ cp config.example.yaml config.yaml
32
+ # Edit config.yaml and set your project_id
33
+ ```
34
+
35
+ **4. Build images**
36
+
37
+ ```bash
38
+ deployml build-images
39
+ ```
40
+
41
+ **5. Deploy**
42
+
43
+ ```bash
44
+ deployml deploy --verbose
45
+ ```
46
+
47
+ First deploy takes ~20 minutes (Cloud SQL provisioning). Subsequent deploys are 1–2 minutes.
48
+
49
+ **6. Get your URLs**
50
+
51
+ ```bash
52
+ deployml get-urls
53
+ ```
54
+
55
+ Prints service URLs and writes a `.env` file with `MLFLOW_URL`, `FASTAPI_URL`, `GRAFANA_URL`, `BIGQUERY_PROJECT`, and `BIGQUERY_DATASET`.
56
+
57
+ ## End-to-End Example
58
+
59
+ Once deployed, the `example/` directory walks through a complete MLOps workflow using a synthetic housing price dataset:
60
+
61
+ ```bash
62
+ pip install mlflow scikit-learn pandas numpy google-cloud-bigquery db-dtypes python-dotenv requests
63
+
64
+ python example/scripts/01_load_training_data.py # load 500 rows into BigQuery
65
+ python example/scripts/02_train_model.py # train RandomForest, log to MLflow
66
+ python example/scripts/03_register_model.py # register model as Production
67
+ python example/scripts/04_make_predictions.py # serve 50 predictions via FastAPI
68
+ python example/scripts/05_generate_ground_truth.py # simulate actual outcomes
69
+ python example/scripts/06_compute_drift_metrics.py # compute feature drift + MAE
70
+ python example/scripts/07_setup_grafana.py # provision monitoring dashboard
71
+ ```
72
+
73
+ See [example/README.md](example/README.md) for details.
74
+
75
+ ## Teardown
76
+
77
+ ```bash
78
+ deployml destroy
79
+ ```
80
+
81
+ Deletes all Cloud Run services, Cloud SQL, GCS bucket, and BigQuery dataset. Does not delete Artifact Registry images or the GCP project.
82
+
83
+ ## Full Tutorial
84
+
85
+ See [docs/tutorials/gcp-cloud-run.md](docs/tutorials/gcp-cloud-run.md) for a step-by-step walkthrough.
86
+
87
+ ## Requirements
88
+
89
+ - Python 3.10+
90
+ - `gcloud` CLI (authenticated)
91
+ - Docker (running)
92
+ - Terraform
@@ -1,7 +1,7 @@
1
1
 
2
2
  [project]
3
3
  name = "deployml-core"
4
- version = "0.0.58"
4
+ version = "0.0.60"
5
5
  description = "Infra for academia"
6
6
  authors = [
7
7
  {name = "Drew Hoang", email = "codentell@gmail.com"},
@@ -4,6 +4,7 @@ import typer
4
4
  import shutil
5
5
  import subprocess
6
6
  import re
7
+ import importlib.resources as pkg_resources
7
8
  from deployml.utils.banner import display_banner
8
9
  from deployml.utils.menu import prompt, show_menu
9
10
  from deployml.utils.constants import (
@@ -780,7 +781,7 @@ def terraform(
780
781
  @cli.command()
781
782
  def deploy(
782
783
  config_path: Path = typer.Option(
783
- ..., "--config-path", "-c", help="Path to YAML config file"
784
+ Path("config.yaml"), "--config-path", "-c", help="Path to YAML config file"
784
785
  ),
785
786
  yes: bool = typer.Option(
786
787
  False, "--yes", "-y", help="Skip confirmation prompts and deploy"
@@ -788,6 +789,9 @@ def deploy(
788
789
  generate_only: bool = typer.Option(
789
790
  False, "--generate-only", "-g", help="Only generate manifests, do not apply (for GKE deployments)"
790
791
  ),
792
+ verbose: bool = typer.Option(
793
+ False, "--verbose", "-v", help="Stream Terraform logs to stdout instead of showing progress bar"
794
+ ),
791
795
  ):
792
796
  """
793
797
  Deploy infrastructure based on a YAML configuration file.
@@ -1027,6 +1031,34 @@ def deploy(
1027
1031
 
1028
1032
  typer.echo(f" Unified bucket creation: {create_artifact_bucket}")
1029
1033
 
1034
+ # Auto-resolve image URIs: if a service image is not specified in the config (or still
1035
+ # points to a legacy gcr.io path), derive it from the Artifact Registry path that
1036
+ # `deployml build-images` produces: {region}-docker.pkg.dev/{project}/mlops-images/{name}
1037
+ # This keeps the YAML config free of hardcoded image URIs — project/region are enough.
1038
+ _TOOL_IMAGE_NAMES = {
1039
+ "mlflow": "mlflow",
1040
+ "feast": "feast",
1041
+ "fastapi": "fastapi",
1042
+ "grafana": "grafana-container",
1043
+ "wandb": "wandb",
1044
+ }
1045
+ _ar_base = f"{region}-docker.pkg.dev/{project_id}/mlops-images"
1046
+ for stage in stack:
1047
+ for stage_name, tool in stage.items():
1048
+ tool_name = tool.get("name", "")
1049
+ params = tool.setdefault("params", {})
1050
+ existing_image = params.get("image", "")
1051
+ if not existing_image or existing_image.startswith("gcr.io/"):
1052
+ image_name = _TOOL_IMAGE_NAMES.get(tool_name)
1053
+ if image_name:
1054
+ params["image"] = f"{_ar_base}/{image_name}:latest"
1055
+ # Cron job images are per-job and must be set explicitly — skip here
1056
+ if stage_name == "workflow_orchestration" and tool_name == "cron":
1057
+ for job in params.get("jobs", []):
1058
+ if not job.get("image") or job.get("image", "").startswith("gcr.io/"):
1059
+ job_name = job.get("service_name", "")
1060
+ job["image"] = f"{_ar_base}/{job_name}:latest"
1061
+
1030
1062
  env = Environment(loader=FileSystemLoader(TEMPLATE_DIR))
1031
1063
  # PATCH: Use wandb_main.tf.j2 or mlflow_main.tf.j2 for cloud_run if present
1032
1064
  if deployment_type == "cloud_run":
@@ -1253,6 +1285,7 @@ def deploy(
1253
1285
  ["terraform", "apply", "-auto-approve"],
1254
1286
  DEPLOYML_TERRAFORM_DIR,
1255
1287
  minutes,
1288
+ verbose=verbose,
1256
1289
  )
1257
1290
  if result_code == 0:
1258
1291
  typer.echo(" Deployment complete!")
@@ -1421,10 +1454,93 @@ def deploy(
1421
1454
  typer.echo(" Deployment cancelled")
1422
1455
 
1423
1456
 
1457
+ @cli.command()
1458
+ def get_urls(
1459
+ config_path: Path = typer.Option(
1460
+ Path("config.yaml"), "--config-path", "-c", help="Path to YAML config file"
1461
+ ),
1462
+ env_path: Path = typer.Option(
1463
+ Path(".env"), "--env-path", help="Path to write .env file"
1464
+ ),
1465
+ ):
1466
+ """
1467
+ Print service URLs from the last deployment and write them to a .env file.
1468
+ """
1469
+ if not config_path.exists():
1470
+ typer.echo(f" Config file not found: {config_path}")
1471
+ raise typer.Exit(code=1)
1472
+
1473
+ config = yaml.safe_load(config_path.read_text())
1474
+ workspace_name = config.get("name") or "development"
1475
+ project_id = config.get("provider", {}).get("project_id", "")
1476
+ terraform_dir = Path.cwd() / ".deployml" / workspace_name / "terraform"
1477
+
1478
+ if not terraform_dir.exists():
1479
+ typer.echo(f" No deployment found at {terraform_dir}. Run 'deployml deploy' first.")
1480
+ raise typer.Exit(code=1)
1481
+
1482
+ output_proc = subprocess.run(
1483
+ ["terraform", "output", "-json"],
1484
+ cwd=terraform_dir,
1485
+ capture_output=True,
1486
+ text=True,
1487
+ )
1488
+ if output_proc.returncode != 0:
1489
+ typer.echo(" Could not retrieve Terraform outputs. Is the stack deployed?")
1490
+ raise typer.Exit(code=1)
1491
+
1492
+ try:
1493
+ outputs = json.loads(output_proc.stdout)
1494
+ except Exception:
1495
+ typer.echo(" Failed to parse Terraform outputs.")
1496
+ raise typer.Exit(code=1)
1497
+
1498
+ if not outputs:
1499
+ typer.echo("No outputs found. Is the stack deployed?")
1500
+ raise typer.Exit(code=1)
1501
+
1502
+ typer.echo("\n DeployML Outputs:")
1503
+ env_lines = []
1504
+ for key, value in outputs.items():
1505
+ is_sensitive = value.get("sensitive", False)
1506
+ output_val = value.get("value")
1507
+ env_key = key.upper()
1508
+ if is_sensitive:
1509
+ typer.secho(f" {key}: [SENSITIVE] (value hidden)", fg=typer.colors.YELLOW)
1510
+ elif isinstance(output_val, str):
1511
+ if output_val.startswith("http://") or output_val.startswith("https://"):
1512
+ typer.secho(f" {key}: {output_val}", fg=typer.colors.BRIGHT_BLUE, bold=True)
1513
+ env_lines.append(f"{env_key}={output_val}")
1514
+ elif output_val == "":
1515
+ typer.secho(f" {key}: [No value]", fg=typer.colors.YELLOW)
1516
+ else:
1517
+ typer.echo(f" {key}: {output_val}")
1518
+ env_lines.append(f"{env_key}={output_val}")
1519
+ elif isinstance(output_val, dict):
1520
+ typer.echo(f" {key}:")
1521
+ for subkey, subval in output_val.items():
1522
+ sub_env_key = f"{env_key}_{subkey.upper()}"
1523
+ if isinstance(subval, str) and (subval.startswith("http://") or subval.startswith("https://")):
1524
+ typer.secho(f" {subkey}: {subval}", fg=typer.colors.BRIGHT_BLUE, bold=True)
1525
+ env_lines.append(f"{sub_env_key}={subval}")
1526
+ elif isinstance(subval, str) and subval:
1527
+ typer.echo(f" {subkey}: {subval}")
1528
+ env_lines.append(f"{sub_env_key}={subval}")
1529
+ else:
1530
+ typer.echo(f" {key}: {output_val}")
1531
+
1532
+ if project_id:
1533
+ env_lines.append(f"BIGQUERY_PROJECT={project_id}")
1534
+ typer.echo(f" bigquery_project: {project_id}")
1535
+
1536
+ env_path.write_text("\n".join(env_lines) + "\n")
1537
+ typer.echo(f"\n .env written to {env_path.resolve()}")
1538
+
1539
+
1424
1540
  @cli.command()
1425
1541
  def destroy(
1426
1542
  config_path: Path = typer.Option(
1427
- ..., "--config-path", "-c", help="Path to YAML config file"
1543
+ Path("config.yaml"), "--config-path", "-c", help="Path to YAML config file"
1428
1544
  ),
1429
1545
  workspace: Optional[str] = typer.Option(
1430
1546
  None, "--workspace", help="Override workspace name from config"
@@ -1487,16 +1603,53 @@ def destroy(
1487
1603
  cwd=DEPLOYML_TERRAFORM_DIR,
1488
1604
  )
1489
1605
 
1490
- # Check if we have Cloud SQL resources and clean them up first
1491
- plan_result = subprocess.run(
1492
- ["terraform", "plan", "-destroy"],
1606
+ # Shut down Cloud Run services first to close any open DB connections
1607
+ # before attempting to destroy Cloud SQL — otherwise active connections
1608
+ # prevent database/user deletion and the destroy fails.
1609
+ region = config.get("provider", {}).get("region", "us-central1")
1610
+ cr_result = subprocess.run(
1611
+ ["gcloud", "run", "services", "list",
1612
+ "--project", project_id,
1613
+ "--region", region,
1614
+ "--format", "value(metadata.name)"],
1615
+ capture_output=True, text=True
1616
+ )
1617
+ if cr_result.returncode == 0:
1618
+ services = [s.strip() for s in cr_result.stdout.splitlines() if s.strip()]
1619
+ for service in services:
1620
+ typer.echo(f" Deleting Cloud Run service: {service}")
1621
+ subprocess.run(
1622
+ ["gcloud", "run", "services", "delete", service,
1623
+ "--project", project_id,
1624
+ "--region", region,
1625
+ "--quiet"],
1626
+ capture_output=True,
1627
+ )
1628
+
1629
+ # Remove Cloud SQL databases and user from Terraform state so Terraform
1630
+ # doesn't try to delete them individually — the instance deletion handles
1631
+ # that automatically, avoiding active-connection errors on destroy.
1632
+ state_result = subprocess.run(
1633
+ ["terraform", "state", "list"],
1493
1634
  cwd=DEPLOYML_TERRAFORM_DIR,
1494
1635
  capture_output=True,
1495
1636
  text=True,
1496
1637
  )
1497
-
1498
- if "google_sql_database_instance" in plan_result.stdout:
1499
- cleanup_cloud_sql_resources(DEPLOYML_TERRAFORM_DIR, project_id)
1638
+ if state_result.returncode == 0:
1639
+ resources_to_remove = [
1640
+ r.strip() for r in state_result.stdout.splitlines()
1641
+ if any(x in r for x in [
1642
+ "google_sql_database.",
1643
+ "google_sql_user.",
1644
+ ])
1645
+ ]
1646
+ for resource in resources_to_remove:
1647
+ typer.echo(f" Removing from state: {resource}")
1648
+ subprocess.run(
1649
+ ["terraform", "state", "rm", resource],
1650
+ cwd=DEPLOYML_TERRAFORM_DIR,
1651
+ capture_output=True,
1652
+ )
1500
1653
 
1501
1654
  # Build destroy command
1502
1655
  cmd = ["terraform", "destroy", "--auto-approve"]
@@ -2287,15 +2440,17 @@ def gke_apply(
2287
2440
 
2288
2441
  @cli.command("build-images")
2289
2442
  def build_images_command(
2290
- docker_root: Path = typer.Option(
2291
- ...,
2443
+ config_path: Path = typer.Option(
2444
+ Path("config.yaml"),
2445
+ "--config-path",
2446
+ "-c",
2447
+ help="Path to YAML config file. Used to infer project and region automatically.",
2448
+ ),
2449
+ docker_root: Optional[Path] = typer.Option(
2450
+ None,
2292
2451
  "--docker-root",
2293
2452
  "-d",
2294
- help="Path to folder containing subfolders with Dockerfiles.",
2295
- exists=True,
2296
- file_okay=False,
2297
- dir_okay=True,
2298
- resolve_path=True,
2453
+ help="Path to folder containing subfolders with Dockerfiles. Defaults to the built-in deployml docker directory.",
2299
2454
  ),
2300
2455
  gcp_project: Optional[str] = typer.Option(
2301
2456
  None,
@@ -2303,10 +2458,10 @@ def build_images_command(
2303
2458
  "-p",
2304
2459
  help="If provided, images will be built using Cloud Build in this GCP project.",
2305
2460
  ),
2306
- region: str = typer.Option(
2307
- "us-central1",
2461
+ region: Optional[str] = typer.Option(
2462
+ None,
2308
2463
  "--region",
2309
- help="GCP region for Artifact Registry.",
2464
+ help="GCP region for Artifact Registry. Inferred from config if not set.",
2310
2465
  ),
2311
2466
  repository: str = typer.Option(
2312
2467
  "mlops-images",
@@ -2342,6 +2497,16 @@ def build_images_command(
2342
2497
  --dry-run prints commands without executing them.
2343
2498
  """
2344
2499
 
2500
+ if config_path and config_path.exists():
2501
+ config = yaml.safe_load(config_path.read_text())
2502
+ if not gcp_project:
2503
+ gcp_project = config.get("provider", {}).get("project_id")
2504
+ if not region:
2505
+ region = config.get("provider", {}).get("region", "us-central1")
2506
+
2507
+ if not region:
2508
+ region = "us-central1"
2509
+
2345
2510
  if create_repo and not gcp_project:
2346
2511
  typer.secho(
2347
2512
  "--create-repo can only be used with --gcp-project.",
@@ -2349,6 +2514,10 @@ def build_images_command(
2349
2514
  )
2350
2515
  raise typer.Exit(code=1)
2351
2516
 
2517
+ if docker_root is None:
2518
+ docker_root = Path(str(pkg_resources.files("deployml") / "docker"))
2519
+ typer.echo(f"Using built-in docker directory: {docker_root}")
2520
+
2352
2521
  try:
2353
2522
  build_images(
2354
2523
  docker_root=docker_root,
@@ -1,5 +1,5 @@
1
1
  # Build for Linux/amd64 platform (not Apple Silicon)
2
- FROM --platform=linux/amd64 python:3.9-slim
2
+ FROM python:3.9-slim
3
3
 
4
4
  # Set working directory
5
5
  WORKDIR /app
@@ -34,4 +34,5 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
34
34
  CMD curl -f http://localhost:8000/health || exit 1
35
35
 
36
36
  # Run the application
37
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
37
+ # Uses PORT env var so it works on Cloud Run (PORT=8080) and locally (PORT=8000)
38
+ CMD uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}
@@ -0,0 +1,167 @@
1
+ from fastapi import FastAPI
2
+ from fastapi.responses import JSONResponse
3
+ from pydantic import BaseModel
4
+ from typing import Dict, Optional
5
+ import os
6
+ import uuid
7
+ from datetime import datetime, timezone
8
+ import pandas as pd
9
+ import mlflow
10
+ import mlflow.pyfunc
11
+
12
+ app = FastAPI(
13
+ title="FastAPI Demo",
14
+ description="Simple FastAPI application for Kubernetes deployment demo",
15
+ version="1.0.0"
16
+ )
17
+
18
+ MLFLOW_TRACKING_URI = os.getenv("MLFLOW_TRACKING_URI", "http://mlflow-service:5000")
19
+ MODEL_NAME = os.getenv("MODEL_NAME", "HousingPriceModel")
20
+ BIGQUERY_PROJECT = os.getenv("GOOGLE_CLOUD_PROJECT", "")
21
+ BIGQUERY_DATASET = os.getenv("BIGQUERY_DATASET", "mlops")
22
+
23
+ model = None
24
+ bq_client = None
25
+
26
+ FEATURE_ORDER = ['bedrooms', 'bathrooms', 'area_sqft', 'lot_size', 'year_built', 'city', 'state']
27
+
28
+
29
+ class PredictionRequest(BaseModel):
30
+ entity_id: Optional[str] = None
31
+ features: Dict[str, float]
32
+
33
+
34
+ class PredictionResponse(BaseModel):
35
+ prediction: float
36
+ timestamp: str
37
+ model_used: Optional[str] = None
38
+ entity_id: Optional[str] = None
39
+
40
+
41
+ class HealthResponse(BaseModel):
42
+ status: str
43
+ timestamp: str
44
+ port: int
45
+ mlflow_connected: bool
46
+ model_loaded: bool
47
+
48
+
49
+ def load_model_from_mlflow():
50
+ global model
51
+ try:
52
+ mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)
53
+ model = mlflow.pyfunc.load_model(f"models:/{MODEL_NAME}/Production")
54
+ print(f"✓ Loaded model '{MODEL_NAME}' from Production stage")
55
+ return True
56
+ except Exception as e:
57
+ print(f"⚠️ Could not load model from MLflow: {e}")
58
+ model = None
59
+ return False
60
+
61
+
62
+ def init_bigquery():
63
+ global bq_client
64
+ if not BIGQUERY_PROJECT:
65
+ return
66
+ try:
67
+ from google.cloud import bigquery
68
+ bq_client = bigquery.Client(project=BIGQUERY_PROJECT)
69
+ print(f"✓ BigQuery client initialized for project {BIGQUERY_PROJECT}")
70
+ except Exception as e:
71
+ print(f"⚠️ Could not initialize BigQuery client: {e}")
72
+ bq_client = None
73
+
74
+
75
+ def log_prediction_to_bigquery(entity_id: str, predicted_value: float, model_version: str):
76
+ if bq_client is None:
77
+ return
78
+ try:
79
+ table_id = f"{BIGQUERY_PROJECT}.{BIGQUERY_DATASET}.predictions"
80
+ row = {
81
+ "prediction_id": str(uuid.uuid4()),
82
+ "entity_id": entity_id,
83
+ "prediction_timestamp": datetime.now(timezone.utc).isoformat(),
84
+ "predicted_value": predicted_value,
85
+ "model_version": model_version,
86
+ "feature_snapshot_timestamp": datetime.now(timezone.utc).isoformat(),
87
+ }
88
+ bq_client.insert_rows_json(table_id, [row])
89
+ except Exception as e:
90
+ print(f"⚠️ Could not log prediction to BigQuery: {e}")
91
+
92
+
93
+ @app.on_event("startup")
94
+ async def startup_event():
95
+ load_model_from_mlflow()
96
+ init_bigquery()
97
+
98
+
99
+ @app.get("/")
100
+ async def root():
101
+ return {
102
+ "service": "FastAPI Demo",
103
+ "version": "1.0.0",
104
+ "model_loaded": model is not None,
105
+ "model_name": MODEL_NAME,
106
+ "mlflow_uri": MLFLOW_TRACKING_URI,
107
+ "endpoints": {"health": "/health", "predict": "/predict", "docs": "/docs"}
108
+ }
109
+
110
+
111
+ @app.get("/health", response_model=HealthResponse)
112
+ async def health():
113
+ port = int(os.getenv("PORT", "8000"))
114
+ return HealthResponse(
115
+ status="healthy",
116
+ timestamp=datetime.now().isoformat(),
117
+ port=port,
118
+ mlflow_connected=True,
119
+ model_loaded=model is not None
120
+ )
121
+
122
+
123
+ @app.post("/predict", response_model=PredictionResponse)
124
+ async def predict(request: PredictionRequest):
125
+ global model
126
+
127
+ entity_id = request.entity_id or str(uuid.uuid4())
128
+
129
+ if model is None:
130
+ load_model_from_mlflow()
131
+
132
+ if model is None:
133
+ return PredictionResponse(
134
+ prediction=-1.0,
135
+ timestamp=datetime.now().isoformat(),
136
+ model_used=None,
137
+ entity_id=entity_id
138
+ )
139
+
140
+ try:
141
+ feature_values = [request.features.get(f, 0.0) for f in FEATURE_ORDER]
142
+ X = pd.DataFrame([feature_values], columns=FEATURE_ORDER)
143
+
144
+ prediction = float(model.predict(X)[0])
145
+ model_version = f"MLflow: {MODEL_NAME}"
146
+
147
+ log_prediction_to_bigquery(entity_id, prediction, model_version)
148
+
149
+ return PredictionResponse(
150
+ prediction=prediction,
151
+ timestamp=datetime.now().isoformat(),
152
+ model_used=model_version,
153
+ entity_id=entity_id
154
+ )
155
+ except Exception as e:
156
+ return PredictionResponse(
157
+ prediction=-1.0,
158
+ timestamp=datetime.now().isoformat(),
159
+ model_used=None,
160
+ entity_id=entity_id
161
+ )
162
+
163
+
164
+ if __name__ == "__main__":
165
+ import uvicorn
166
+ port = int(os.getenv("PORT", "8000"))
167
+ uvicorn.run(app, host="0.0.0.0", port=port)