deployml-core 0.0.57__tar.gz → 0.0.58__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 (103) hide show
  1. {deployml_core-0.0.57 → deployml_core-0.0.58}/PKG-INFO +1 -1
  2. {deployml_core-0.0.57 → deployml_core-0.0.58}/pyproject.toml +1 -1
  3. deployml_core-0.0.58/src/deployml/docker/fastapi/.dockerignore +11 -0
  4. deployml_core-0.0.58/src/deployml/docker/fastapi/Dockerfile +37 -0
  5. deployml_core-0.0.58/src/deployml/docker/fastapi/main.py +127 -0
  6. deployml_core-0.0.58/src/deployml/docker/fastapi/requirements.txt +7 -0
  7. deployml_core-0.0.58/src/deployml/docker/grafana-container/Dockerfile +23 -0
  8. deployml_core-0.0.58/src/deployml/docker/grafana-container/README.md +3 -0
  9. deployml_core-0.0.58/src/deployml/docker/grafana-container/entrypoint.sh +10 -0
  10. deployml_core-0.0.58/src/deployml/docker/mlflow/Dockerfile +39 -0
  11. deployml_core-0.0.58/src/deployml/docker/mlflow/requirements.txt +4 -0
  12. deployml_core-0.0.58/src/deployml/notebook/docker.py +224 -0
  13. deployml_core-0.0.58/src/deployml/terraform/modules/bigquery/cloud/gcp/main.tf +38 -0
  14. deployml_core-0.0.58/src/deployml/terraform/modules/bigquery/cloud/gcp/outputs.tf +3 -0
  15. deployml_core-0.0.58/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/drift_metrics.json +9 -0
  16. deployml_core-0.0.58/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/ground_truth.json +5 -0
  17. deployml_core-0.0.58/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/offline_features.json +7 -0
  18. deployml_core-0.0.58/src/deployml/terraform/modules/bigquery/cloud/gcp/schemas/predictions.json +8 -0
  19. deployml_core-0.0.58/src/deployml/terraform/modules/bigquery/cloud/gcp/variables.tf +11 -0
  20. {deployml_core-0.0.57 → deployml_core-0.0.58}/LICENSE +0 -0
  21. {deployml_core-0.0.57 → deployml_core-0.0.58}/README.md +0 -0
  22. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/__init__.py +0 -0
  23. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/api.py +0 -0
  24. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/cli/__init__.py +0 -0
  25. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/cli/cli.py +0 -0
  26. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/diagnostics/__init__.py +0 -0
  27. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/diagnostics/doctor.py +0 -0
  28. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/enum/__init__.py +0 -0
  29. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/enum/cloud_provider.py +0 -0
  30. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/enum/deployment_type.py +0 -0
  31. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/notebook/__init__.py +0 -0
  32. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/notebook/deployment.py +0 -0
  33. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/notebook/display.py +0 -0
  34. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/notebook/stack.py +0 -0
  35. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/notebook/urls.py +0 -0
  36. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/notebook.py +0 -0
  37. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/templates/gcp/cloud_run/base_main.tf.j2 +0 -0
  38. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/templates/gcp/cloud_run/main.tf.j2 +0 -0
  39. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/templates/gcp/cloud_run/mlflow_main.tf.j2 +0 -0
  40. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/templates/gcp/cloud_run/terraform.tfvars.j2 +0 -0
  41. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/templates/gcp/cloud_run/variables.tf.j2 +0 -0
  42. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/templates/gcp/cloud_run/wandb_main.tf.j2 +0 -0
  43. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/templates/gcp/cloud_vm/main.tf.j2 +0 -0
  44. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/templates/gcp/cloud_vm/terraform.tfvars.j2 +0 -0
  45. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/templates/gcp/cloud_vm/variables.tf.j2 +0 -0
  46. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/templates/kubernetes_local/deployment.yaml.j2 +0 -0
  47. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/templates/kubernetes_local/mlflow-deployment.yaml.j2 +0 -0
  48. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/templates/kubernetes_local/mlflow-service.yaml.j2 +0 -0
  49. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/templates/kubernetes_local/service.yaml.j2 +0 -0
  50. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/cloud_sql_postgres/main.tf +0 -0
  51. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/cloud_sql_postgres/outputs.tf +0 -0
  52. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/cloud_sql_postgres/variables.tf +0 -0
  53. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/main.tf +0 -0
  54. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/outputs.tf +0 -0
  55. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/cron/cloud/gcp/cloud_run/variables.tf +0 -0
  56. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/main.tf +0 -0
  57. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/outputs.tf +0 -0
  58. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/explainability_monitoring/cloud/gcp/cloud_run/variables.tf +0 -0
  59. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/main.tf +0 -0
  60. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/outputs.tf +0 -0
  61. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/fairness_monitoring/cloud/gcp/cloud_run/variables.tf +0 -0
  62. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/main.tf +0 -0
  63. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/outputs.tf +0 -0
  64. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/fastapi/cloud/gcp/cloud_run/variables.tf +0 -0
  65. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/main.tf +0 -0
  66. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/outputs.tf +0 -0
  67. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_run/variables.tf +0 -0
  68. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/feast_env.tpl +0 -0
  69. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/main.tf +0 -0
  70. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/outputs.tf +0 -0
  71. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/feast/cloud/gcp/cloud_vm/variables.tf +0 -0
  72. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/main.tf +0 -0
  73. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/outputs.tf +0 -0
  74. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/grafana/cloud/gcp/cloud_run/variables.tf +0 -0
  75. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/main.tf +0 -0
  76. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/outputs.tf +0 -0
  77. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_run/variables.tf +0 -0
  78. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_vm/main.tf +0 -0
  79. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/mlflow/cloud/gcp/cloud_vm/variables.tf +0 -0
  80. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/main.tf +0 -0
  81. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/outputs.tf +0 -0
  82. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/offline_scoring/cloud/gcp/cloud_run/variables.tf +0 -0
  83. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/teardown/cloud/gcp/cloud_function/main.py +0 -0
  84. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/teardown/cloud/gcp/cloud_function/requirements.txt +0 -0
  85. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/teardown/cloud/gcp/cloudbuild.yaml +0 -0
  86. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/teardown/cloud/gcp/main.tf +0 -0
  87. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/teardown/cloud/gcp/outputs.tf +0 -0
  88. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/teardown/cloud/gcp/teardown_script.sh +0 -0
  89. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/teardown/cloud/gcp/variables.tf +0 -0
  90. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/main.tf +0 -0
  91. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/outputs.tf +0 -0
  92. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_run/variables.tf +0 -0
  93. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_vm/main.tf +0 -0
  94. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/terraform/modules/wandb/cloud/gcp/cloud_vm/variables.tf +0 -0
  95. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/utils/__init__.py +0 -0
  96. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/utils/banner.py +0 -0
  97. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/utils/constants.py +0 -0
  98. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/utils/helpers.py +0 -0
  99. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/utils/infracost.py +0 -0
  100. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/utils/kubernetes_gke.py +0 -0
  101. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/utils/kubernetes_local.py +0 -0
  102. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/utils/menu.py +0 -0
  103. {deployml_core-0.0.57 → deployml_core-0.0.58}/src/deployml/utils/teardown.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deployml-core
3
- Version: 0.0.57
3
+ Version: 0.0.58
4
4
  Summary: Infra for academia
5
5
  License-File: LICENSE
6
6
  Author: Drew Hoang
@@ -1,7 +1,7 @@
1
1
 
2
2
  [project]
3
3
  name = "deployml-core"
4
- version = "0.0.57"
4
+ version = "0.0.58"
5
5
  description = "Infra for academia"
6
6
  authors = [
7
7
  {name = "Drew Hoang", email = "codentell@gmail.com"},
@@ -0,0 +1,11 @@
1
+ __pycache__
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .Python
6
+ env/
7
+ venv/
8
+ .venv
9
+ .git
10
+ .gitignore
11
+ README.md
@@ -0,0 +1,37 @@
1
+ # Build for Linux/amd64 platform (not Apple Silicon)
2
+ FROM --platform=linux/amd64 python:3.9-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ curl \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Install Python dependencies
13
+ RUN pip install --no-cache-dir --upgrade pip setuptools wheel
14
+
15
+ # Copy requirements and install
16
+ COPY requirements.txt .
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy application code
20
+ COPY main.py .
21
+
22
+ # Create non-root user
23
+ RUN useradd -m -s /bin/bash fastapi && \
24
+ chown -R fastapi:fastapi /app
25
+
26
+ # Switch to non-root user
27
+ USER fastapi
28
+
29
+ # Expose port
30
+ EXPOSE 8000
31
+
32
+ # Health check
33
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
34
+ CMD curl -f http://localhost:8000/health || exit 1
35
+
36
+ # Run the application
37
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
@@ -0,0 +1,127 @@
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.responses import JSONResponse
3
+ from pydantic import BaseModel
4
+ from typing import List, Dict, Any, Optional
5
+ import os
6
+ from datetime import datetime
7
+ import pandas as pd
8
+ import mlflow
9
+ import mlflow.pyfunc
10
+
11
+ app = FastAPI(
12
+ title="FastAPI Demo",
13
+ description="Simple FastAPI application for Kubernetes deployment demo",
14
+ version="1.0.0"
15
+ )
16
+
17
+ # Hardcoded MLflow configuration
18
+ MLFLOW_TRACKING_URI = "http://mlflow-service:5000"
19
+ MODEL_NAME = "HousingPriceModel"
20
+
21
+ # Global model variable
22
+ model = None
23
+
24
+ # Pydantic models
25
+ class PredictionRequest(BaseModel):
26
+ features: Dict[str, float]
27
+
28
+ class PredictionResponse(BaseModel):
29
+ prediction: float
30
+ timestamp: str
31
+ model_used: Optional[str] = None
32
+
33
+ class HealthResponse(BaseModel):
34
+ status: str
35
+ timestamp: str
36
+ port: int
37
+ mlflow_connected: bool
38
+ model_loaded: bool
39
+
40
+ def load_model_from_mlflow():
41
+ """Load model from MLflow Model Registry"""
42
+ global model
43
+
44
+ try:
45
+ mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)
46
+ model = mlflow.pyfunc.load_model(f"models:/{MODEL_NAME}/Production")
47
+ print(f"✓ Loaded model '{MODEL_NAME}' from Production stage")
48
+ return True
49
+ except Exception as e:
50
+ print(f"⚠️ Could not load model from MLflow: {e}")
51
+ model = None
52
+ return False
53
+
54
+ # Load model at startup
55
+ @app.on_event("startup")
56
+ async def startup_event():
57
+ """Load MLflow model on startup"""
58
+ load_model_from_mlflow()
59
+
60
+ @app.get("/")
61
+ async def root():
62
+ """Root endpoint"""
63
+ return {
64
+ "service": "FastAPI Demo",
65
+ "version": "1.0.0",
66
+ "model_loaded": model is not None,
67
+ "model_name": MODEL_NAME,
68
+ "mlflow_uri": MLFLOW_TRACKING_URI,
69
+ "endpoints": {
70
+ "health": "/health",
71
+ "predict": "/predict",
72
+ "docs": "/docs"
73
+ }
74
+ }
75
+
76
+ @app.get("/health", response_model=HealthResponse)
77
+ async def health():
78
+ """Health check endpoint"""
79
+ port = int(os.getenv("FASTAPI_PORT", "8000"))
80
+ return HealthResponse(
81
+ status="healthy",
82
+ timestamp=datetime.now().isoformat(),
83
+ port=port,
84
+ mlflow_connected=True,
85
+ model_loaded=model is not None
86
+ )
87
+
88
+ @app.post("/predict", response_model=PredictionResponse)
89
+ async def predict(request: PredictionRequest):
90
+ """Prediction endpoint using MLflow model"""
91
+ global model
92
+
93
+ # If model is not loaded, return -1
94
+ if model is None:
95
+ return PredictionResponse(
96
+ prediction=-1.0,
97
+ timestamp=datetime.now().isoformat(),
98
+ model_used=None
99
+ )
100
+
101
+ try:
102
+ # Convert features dict to DataFrame in correct order
103
+ # Features: bedrooms, bathrooms, area_sqft, lot_size, year_built, city, state
104
+ feature_order = ['bedrooms', 'bathrooms', 'area_sqft', 'lot_size', 'year_built', 'city', 'state']
105
+ feature_values = [request.features.get(f, 0.0) for f in feature_order]
106
+ X = pd.DataFrame([feature_values], columns=feature_order)
107
+
108
+ prediction = float(model.predict(X)[0])
109
+ model_used = f"MLflow: {MODEL_NAME}"
110
+
111
+ return PredictionResponse(
112
+ prediction=prediction,
113
+ timestamp=datetime.now().isoformat(),
114
+ model_used=model_used
115
+ )
116
+ except Exception as e:
117
+ # On any error, return -1
118
+ return PredictionResponse(
119
+ prediction=-1.0,
120
+ timestamp=datetime.now().isoformat(),
121
+ model_used=None
122
+ )
123
+
124
+ if __name__ == "__main__":
125
+ import uvicorn
126
+ port = int(os.getenv("FASTAPI_PORT", "8000"))
127
+ uvicorn.run(app, host="0.0.0.0", port=port)
@@ -0,0 +1,7 @@
1
+ fastapi==0.104.1
2
+ uvicorn==0.24.0
3
+ pydantic==2.5.2
4
+ mlflow>=2.8.0
5
+ scikit-learn>=1.0.0
6
+ pandas>=1.3.0
7
+ numpy>=1.21.0
@@ -0,0 +1,23 @@
1
+ FROM grafana/grafana:10.4.5
2
+
3
+ # Switch to root to copy files and change permissions
4
+ USER root
5
+
6
+ # Configure Grafana to listen on Cloud Run's expected port
7
+ ENV GF_SERVER_HTTP_PORT=8080 \
8
+ GF_SECURITY_ADMIN_USER=admin \
9
+ GF_SECURITY_ADMIN_PASSWORD=admin
10
+
11
+ # Copy entrypoint script and set permissions
12
+ COPY entrypoint.sh /usr/local/bin/entrypoint.sh
13
+ RUN chmod 755 /usr/local/bin/entrypoint.sh
14
+
15
+ # Create provisioning directories
16
+ RUN mkdir -p /etc/grafana/provisioning/datasources \
17
+ /etc/grafana/provisioning/dashboards
18
+
19
+ # Switch back to the default grafana user for runtime
20
+ USER grafana
21
+
22
+ # Set custom entrypoint
23
+ ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
@@ -0,0 +1,3 @@
1
+ docker build --platform=linux/amd64 -t gcr.io/mlops-intro-461805/grafana/grafana:latest .
2
+
3
+ docker push gcr.io/mlops-intro-461805/grafana/grafana:latest
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env sh
2
+ set -e
3
+
4
+ # Map Cloud Run's PORT → Grafana's expected var
5
+ export GF_SERVER_HTTP_PORT="${PORT:-${GF_SERVER_HTTP_PORT:-8080}}"
6
+
7
+ echo "Starting Grafana on port ${GF_SERVER_HTTP_PORT}..."
8
+ [ -n "${GF_DATABASE_URL:-}" ] && echo "Using database: ${GF_DATABASE_URL}"
9
+
10
+ exec /run.sh
@@ -0,0 +1,39 @@
1
+ # Build for Linux/amd64 platform (not Apple Silicon)
2
+ FROM --platform=linux/amd64 python:3.11-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ curl \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Install Python dependencies
13
+ RUN pip install --no-cache-dir --upgrade pip setuptools wheel
14
+
15
+ # Copy requirements and install
16
+ COPY requirements.txt .
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Create non-root user
20
+ RUN useradd -m -s /bin/bash mlflow && \
21
+ chown -R mlflow:mlflow /app
22
+
23
+ # Create directories for MLflow data
24
+ RUN mkdir -p /mlflow-artifacts && \
25
+ chown -R mlflow:mlflow /mlflow-artifacts
26
+
27
+ # Switch to non-root user
28
+ USER mlflow
29
+
30
+ # Expose port
31
+ EXPOSE 5000
32
+
33
+ # Health check
34
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
35
+ CMD curl -f http://localhost:5000/health || exit 1
36
+
37
+ # Default command - MLflow server
38
+ CMD ["mlflow", "server", "--host", "0.0.0.0", "--port", "5000"]
39
+
@@ -0,0 +1,4 @@
1
+ mlflow==3.10.0
2
+ psycopg2-binary
3
+ boto3
4
+ google-cloud-storage
@@ -0,0 +1,224 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ class ImageBuildError(Exception):
6
+ pass
7
+
8
+ def build_images(
9
+ docker_root: Path,
10
+ gcp_project_id: Optional[str] = None,
11
+ region: str = "us-west1",
12
+ repository: str = "mlops-images",
13
+ tag: str = "latest",
14
+ create_repo: bool = False,
15
+ dry_run: bool = False,
16
+ ) -> None:
17
+ """
18
+ Build all Docker images located in subdirectories of docker_root.
19
+
20
+ Each subdirectory containing a Dockerfile will be built as a separate image.
21
+ The image name will match the subdirectory name.
22
+
23
+ Local mode:
24
+ Builds images locally using Docker.
25
+
26
+ GCP mode:
27
+ Uses Cloud Build and pushes images to Artifact Registry.
28
+
29
+ Args:
30
+ docker_root: Root folder containing subfolders with Dockerfiles.
31
+ gcp_project_id: If provided, use Cloud Build in this GCP project.
32
+ region: GCP region for Artifact Registry.
33
+ repository: Artifact Registry repository name.
34
+ tag: Docker image tag.
35
+ create_repo: Whether to create Artifact Registry repository (GCP mode only).
36
+ dry_run: If True, print commands without executing them.
37
+ """
38
+
39
+ docker_root = Path(docker_root)
40
+
41
+ if not docker_root.exists():
42
+ raise ValueError(f"Docker root does not exist: {docker_root}")
43
+
44
+ # Discover services
45
+ services = [
46
+ d for d in docker_root.iterdir()
47
+ if d.is_dir() and (d / "Dockerfile").exists()
48
+ ]
49
+
50
+ if not services:
51
+ print("No Dockerfiles found.")
52
+ return
53
+
54
+ print(f"Found {len(services)} service(s):")
55
+ for s in services:
56
+ print(f" - {s.name}")
57
+
58
+ print()
59
+
60
+ # ----------------------------------------
61
+ # GCP MODE
62
+ # ----------------------------------------
63
+ if gcp_project_id:
64
+
65
+ image_base = f"{region}-docker.pkg.dev/{gcp_project_id}/{repository}"
66
+
67
+ # Create Artifact Registry repo if requested
68
+ if create_repo:
69
+ create_cmd = [
70
+ "gcloud", "artifacts", "repositories", "create", repository,
71
+ "--repository-format=docker",
72
+ "--location", region,
73
+ "--project", gcp_project_id,
74
+ ]
75
+
76
+ if dry_run:
77
+ print("Would create Artifact Registry repository:")
78
+ print(" " + " ".join(create_cmd))
79
+ print()
80
+ else:
81
+ print("Ensuring Artifact Registry repository exists...")
82
+ subprocess.run(create_cmd, check=False) # safe if already exists
83
+ print()
84
+
85
+ # Build each service
86
+ for service_dir in services:
87
+ service_name = service_dir.name
88
+ image_uri = f"{image_base}/{service_name}:{tag}"
89
+
90
+ build_cmd = [
91
+ "gcloud", "builds", "submit",
92
+ str(service_dir),
93
+ "--tag", image_uri,
94
+ "--project", gcp_project_id,
95
+ ]
96
+
97
+ if dry_run:
98
+ print("Would submit Cloud Build:")
99
+ print(" " + " ".join(build_cmd))
100
+ print()
101
+ else:
102
+ print(f"Building {service_name} via Cloud Build...")
103
+ subprocess.run(build_cmd, check=True)
104
+ print(f"Pushed: {image_uri}")
105
+ print()
106
+
107
+ # ----------------------------------------
108
+ # LOCAL MODE
109
+ # ----------------------------------------
110
+ else:
111
+ for service_dir in services:
112
+ service_name = service_dir.name
113
+ image_name = f"{service_name}:{tag}"
114
+
115
+ build_cmd = [
116
+ "docker", "build",
117
+ "-t", image_name,
118
+ str(service_dir),
119
+ ]
120
+
121
+ if dry_run:
122
+ print("Would build locally:")
123
+ print(" " + " ".join(build_cmd))
124
+ print()
125
+ else:
126
+ print(f"Building {service_name} locally...")
127
+ subprocess.run(build_cmd, check=True)
128
+ print(f"Built: {image_name}")
129
+ print()
130
+
131
+ if dry_run:
132
+ print("Dry run complete. No commands were executed.")
133
+
134
+ # -----------------------------
135
+ # Local Docker Build
136
+ # -----------------------------
137
+
138
+ def _validate_docker():
139
+ try:
140
+ subprocess.run(
141
+ ["docker", "--version"],
142
+ check=True,
143
+ stdout=subprocess.PIPE,
144
+ stderr=subprocess.PIPE,
145
+ )
146
+ except Exception:
147
+ raise ImageBuildError("Docker is not installed or not available in PATH.")
148
+
149
+
150
+ def _build_locally(service_dirs: list[Path], tag: str):
151
+ print("Building images locally with Docker...\n")
152
+
153
+ for service_dir in service_dirs:
154
+ service_name = service_dir.name
155
+ image_name = f"{service_name}:{tag}"
156
+
157
+ print(f"Building {image_name} ...")
158
+
159
+ subprocess.run(
160
+ [
161
+ "docker",
162
+ "build",
163
+ "-t",
164
+ image_name,
165
+ str(service_dir),
166
+ ],
167
+ check=True,
168
+ )
169
+
170
+ print(f"Successfully built {image_name}\n")
171
+
172
+
173
+ # -----------------------------
174
+ # GCP Cloud Build
175
+ # -----------------------------
176
+
177
+ def _validate_gcloud():
178
+ try:
179
+ subprocess.run(
180
+ ["gcloud", "--version"],
181
+ check=True,
182
+ stdout=subprocess.PIPE,
183
+ stderr=subprocess.PIPE,
184
+ )
185
+ except Exception:
186
+ raise ImageBuildError("gcloud CLI is not installed or not available in PATH.")
187
+
188
+
189
+ def _build_with_cloud_build(
190
+ service_dirs: list[Path],
191
+ gcp_project_id: str,
192
+ region: str,
193
+ repository: str,
194
+ tag: str,
195
+ ):
196
+ print(f"Building images using Cloud Build in project '{gcp_project_id}'...\n")
197
+
198
+ for service_dir in service_dirs:
199
+ service_name = service_dir.name
200
+
201
+ image_uri = (
202
+ f"{region}-docker.pkg.dev/"
203
+ f"{gcp_project_id}/"
204
+ f"{repository}/"
205
+ f"{service_name}:{tag}"
206
+ )
207
+
208
+ print(f"Submitting Cloud Build for {image_uri} ...")
209
+
210
+ subprocess.run(
211
+ [
212
+ "gcloud",
213
+ "builds",
214
+ "submit",
215
+ str(service_dir),
216
+ "--tag",
217
+ image_uri,
218
+ "--project",
219
+ gcp_project_id,
220
+ ],
221
+ check=True,
222
+ )
223
+
224
+ print(f"Successfully built and pushed {image_uri}\n")
@@ -0,0 +1,38 @@
1
+ resource "google_bigquery_dataset" "mlops" {
2
+ project = var.project_id
3
+ dataset_id = var.dataset_id
4
+ location = var.region
5
+ }
6
+
7
+ resource "google_bigquery_table" "drift_metrics" {
8
+ dataset_id = google_bigquery_dataset.mlops.dataset_id
9
+ table_id = "drift_metrics"
10
+ project = var.project_id
11
+
12
+ schema = file("${path.module}/schemas/drift_metrics.json")
13
+ }
14
+
15
+ resource "google_bigquery_table" "ground_truth" {
16
+ dataset_id = google_bigquery_dataset.mlops.dataset_id
17
+ table_id = "ground_truth"
18
+ project = var.project_id
19
+
20
+ schema = file("${path.module}/schemas/ground_truth.json")
21
+ }
22
+
23
+ resource "google_bigquery_table" "offline_features" {
24
+ dataset_id = google_bigquery_dataset.mlops.dataset_id
25
+ table_id = "offline_features"
26
+ project = var.project_id
27
+
28
+ schema = file("${path.module}/schemas/offline_features.json")
29
+ }
30
+
31
+ resource "google_bigquery_table" "predictions" {
32
+ dataset_id = google_bigquery_dataset.mlops.dataset_id
33
+ table_id = "predictions"
34
+ project = var.project_id
35
+
36
+ schema = file("${path.module}/schemas/predictions.json")
37
+ }
38
+
@@ -0,0 +1,3 @@
1
+ output "dataset_id" {
2
+ value = google_bigquery_dataset.mlops.dataset_id
3
+ }
@@ -0,0 +1,9 @@
1
+ [
2
+ {"name": "metric_timestamp", "type": "TIMESTAMP", "mode": "REQUIRED"},
3
+ {"name": "metric_type", "type": "STRING", "mode": "REQUIRED"},
4
+ {"name": "feature_name", "type": "STRING", "mode": "NULLABLE"},
5
+ {"name": "value", "type": "FLOAT", "mode": "NULLABLE"},
6
+ {"name": "window_start", "type": "TIMESTAMP", "mode": "NULLABLE"},
7
+ {"name": "window_end", "type": "TIMESTAMP", "mode": "NULLABLE"},
8
+ {"name": "model_version", "type": "STRING", "mode": "NULLABLE"}
9
+ ]
@@ -0,0 +1,5 @@
1
+ [
2
+ {"name": "entity_id", "type": "STRING", "mode": "REQUIRED"},
3
+ {"name": "event_timestamp", "type": "TIMESTAMP", "mode": "REQUIRED"},
4
+ {"name": "actual_value", "type": "FLOAT", "mode": "NULLABLE"}
5
+ ]
@@ -0,0 +1,7 @@
1
+ [
2
+ {"name": "entity_id", "type": "STRING", "mode": "REQUIRED"},
3
+ {"name": "event_timestamp", "type": "TIMESTAMP", "mode": "REQUIRED"},
4
+ {"name": "feature_1", "type": "FLOAT", "mode": "NULLABLE"},
5
+ {"name": "feature_2", "type": "FLOAT", "mode": "NULLABLE"},
6
+ {"name": "feature_3", "type": "FLOAT", "mode": "NULLABLE"}
7
+ ]
@@ -0,0 +1,8 @@
1
+ [
2
+ {"name": "prediction_id", "type": "STRING", "mode": "REQUIRED"},
3
+ {"name": "entity_id", "type": "STRING", "mode": "REQUIRED"},
4
+ {"name": "prediction_timestamp", "type": "TIMESTAMP", "mode": "REQUIRED"},
5
+ {"name": "predicted_value", "type": "FLOAT", "mode": "NULLABLE"},
6
+ {"name": "model_version", "type": "STRING", "mode": "NULLABLE"},
7
+ {"name": "feature_snapshot_timestamp", "type": "TIMESTAMP", "mode": "NULLABLE"}
8
+ ]
@@ -0,0 +1,11 @@
1
+ variable "project_id" {
2
+ type = string
3
+ }
4
+
5
+ variable "region" {
6
+ type = string
7
+ }
8
+
9
+ variable "dataset_id" {
10
+ type = string
11
+ }
File without changes
File without changes