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.
- deployml_core-0.0.60/PKG-INFO +117 -0
- deployml_core-0.0.60/README.md +92 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/pyproject.toml +1 -1
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/cli/cli.py +187 -18
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/fastapi/Dockerfile +3 -2
- deployml_core-0.0.60/src/deployml/docker/fastapi/main.py +167 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/fastapi/requirements.txt +3 -1
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/mlflow/Dockerfile +3 -2
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_run/mlflow_main.tf.j2 +32 -38
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/bigquery/cloud/gcp/main.tf +4 -0
- deployml_core-0.0.60/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/offline_features.json +12 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/cloud_sql_postgres/main.tf +3 -4
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/main.tf +5 -1
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/variables.tf +7 -1
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/main.tf +12 -1
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/helpers.py +53 -97
- deployml_core-0.0.58/PKG-INFO +0 -111
- deployml_core-0.0.58/README.md +0 -87
- deployml_core-0.0.58/src/deployml/docker/fastapi/main.py +0 -127
- deployml_core-0.0.58/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/offline_features.json +0 -7
- {deployml_core-0.0.58 → deployml_core-0.0.60}/LICENSE +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/__init__.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/api.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/cli/__init__.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/diagnostics/__init__.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/diagnostics/doctor.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/fastapi/.dockerignore +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/grafana-container/Dockerfile +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/grafana-container/README.md +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/grafana-container/entrypoint.sh +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/docker/mlflow/requirements.txt +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/enum/__init__.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/enum/cloud_provider.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/enum/deployment_type.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/notebook/__init__.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/notebook/deployment.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/notebook/display.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/notebook/docker.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/notebook/stack.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/notebook/urls.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/notebook.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_run/base_main.tf.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_run/main.tf.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_run/terraform.tfvars.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_run/variables.tf.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_run/wandb_main.tf.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_vm/main.tf.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_vm/terraform.tfvars.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/gcp/cloud_vm/variables.tf.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/kubernetes_local/deployment.yaml.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/kubernetes_local/mlflow-deployment.yaml.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/kubernetes_local/mlflow-service.yaml.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/templates/kubernetes_local/service.yaml.j2 +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/bigquery/cloud/gcp/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/drift_metrics.json +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/ground_truth.json +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/predictions.json +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/bigquery/cloud/gcp/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/cloud_sql_postgres/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/cloud_sql_postgres/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/feast_env.tpl +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_vm/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_vm/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/teardown/cloud/gcp/cloud_function/main.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/teardown/cloud/gcp/cloud_function/requirements.txt +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/teardown/cloud/gcp/cloudbuild.yaml +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/teardown/cloud/gcp/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/teardown/cloud/gcp/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/teardown/cloud/gcp/teardown_script.sh +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/teardown/cloud/gcp/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/outputs.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_vm/main.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_vm/variables.tf +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/__init__.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/banner.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/constants.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/infracost.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/kubernetes_gke.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/kubernetes_local.py +0 -0
- {deployml_core-0.0.58 → deployml_core-0.0.60}/src/deployml/utils/menu.py +0 -0
- {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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
-
|
|
1499
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|