xenfra-sdk 0.2.5__py3-none-any.whl → 0.2.6__py3-none-any.whl
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.
- xenfra_sdk/__init__.py +46 -2
- xenfra_sdk/blueprints/base.py +150 -0
- xenfra_sdk/blueprints/factory.py +99 -0
- xenfra_sdk/blueprints/node.py +219 -0
- xenfra_sdk/blueprints/python.py +57 -0
- xenfra_sdk/blueprints/railpack.py +99 -0
- xenfra_sdk/blueprints/schema.py +70 -0
- xenfra_sdk/cli/main.py +175 -49
- xenfra_sdk/client.py +6 -2
- xenfra_sdk/constants.py +26 -0
- xenfra_sdk/db/session.py +8 -3
- xenfra_sdk/detection.py +262 -191
- xenfra_sdk/dockerizer.py +76 -120
- xenfra_sdk/engine.py +758 -172
- xenfra_sdk/events.py +254 -0
- xenfra_sdk/exceptions.py +9 -0
- xenfra_sdk/governance.py +150 -0
- xenfra_sdk/manifest.py +93 -138
- xenfra_sdk/mcp_client.py +7 -5
- xenfra_sdk/{models.py → models/__init__.py} +17 -1
- xenfra_sdk/models/context.py +61 -0
- xenfra_sdk/orchestrator.py +223 -99
- xenfra_sdk/privacy.py +11 -0
- xenfra_sdk/protocol.py +38 -0
- xenfra_sdk/railpack_adapter.py +357 -0
- xenfra_sdk/railpack_detector.py +587 -0
- xenfra_sdk/railpack_manager.py +312 -0
- xenfra_sdk/recipes.py +152 -19
- xenfra_sdk/resources/activity.py +45 -0
- xenfra_sdk/resources/build.py +157 -0
- xenfra_sdk/resources/deployments.py +22 -2
- xenfra_sdk/resources/intelligence.py +25 -0
- xenfra_sdk-0.2.6.dist-info/METADATA +118 -0
- xenfra_sdk-0.2.6.dist-info/RECORD +49 -0
- {xenfra_sdk-0.2.5.dist-info → xenfra_sdk-0.2.6.dist-info}/WHEEL +1 -1
- xenfra_sdk/templates/Caddyfile.j2 +0 -14
- xenfra_sdk/templates/Dockerfile.j2 +0 -41
- xenfra_sdk/templates/cloud-init.sh.j2 +0 -90
- xenfra_sdk/templates/docker-compose-multi.yml.j2 +0 -29
- xenfra_sdk/templates/docker-compose.yml.j2 +0 -30
- xenfra_sdk-0.2.5.dist-info/METADATA +0 -116
- xenfra_sdk-0.2.5.dist-info/RECORD +0 -38
xenfra_sdk/manifest.py
CHANGED
|
@@ -1,42 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Literal, Optional, Dict, Any
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import yaml
|
|
5
|
+
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
framework: fastapi
|
|
9
|
-
services: # <-- This makes it a microservices project
|
|
10
|
-
- name: users
|
|
11
|
-
port: 8001
|
|
12
|
-
- name: orders
|
|
13
|
-
port: 8002
|
|
14
|
-
"""
|
|
7
|
+
from xenfra_sdk.constants import DEFAULT_REGION, DEFAULT_SIZE, DEFAULT_OS
|
|
15
8
|
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
from typing import List, Literal, Optional
|
|
18
9
|
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
class ProviderConfig(BaseModel):
|
|
11
|
+
"""Configuration for cloud providers (currently DigitalOcean)."""
|
|
12
|
+
region: str = Field(default=DEFAULT_REGION)
|
|
13
|
+
size: str = Field(default=DEFAULT_SIZE)
|
|
14
|
+
image: str = Field(default=DEFAULT_OS)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InfrastructureService(BaseModel):
|
|
18
|
+
"""Managed infrastructure dependency (e.g., PostgreSQL, Kafka)."""
|
|
19
|
+
type: Literal["postgres", "kafka", "redis", "mongodb"]
|
|
20
|
+
name: str
|
|
21
|
+
version: Optional[str] = None
|
|
22
|
+
env_vars: Dict[str, str] = Field(default_factory=dict)
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
class ServiceDefinition(BaseModel):
|
|
24
26
|
"""
|
|
25
27
|
Single service definition in a microservices project.
|
|
26
|
-
|
|
27
|
-
Example in xenfra.yaml:
|
|
28
|
-
services:
|
|
29
|
-
- name: users
|
|
30
|
-
path: ./services/users
|
|
31
|
-
port: 8001
|
|
32
|
-
framework: fastapi
|
|
33
|
-
entrypoint: users_api.main:app
|
|
34
28
|
"""
|
|
29
|
+
model_config = ConfigDict(extra="ignore")
|
|
35
30
|
|
|
36
31
|
name: str = Field(..., min_length=1, max_length=50, description="Service name (unique)")
|
|
37
32
|
path: str = Field(default=".", description="Relative path to service directory")
|
|
38
33
|
port: int = Field(..., ge=1, le=65535, description="Service port")
|
|
39
|
-
framework: Literal[
|
|
34
|
+
framework: Literal[
|
|
35
|
+
# Python
|
|
36
|
+
"fastapi", "flask", "django", "python",
|
|
37
|
+
# Node.js
|
|
38
|
+
"nodejs", "nextjs", "react", "express", "nestjs",
|
|
39
|
+
# Go
|
|
40
|
+
"go", "gin", "echo",
|
|
41
|
+
# AI Agents
|
|
42
|
+
"langgraph", "crewai", "mcp",
|
|
43
|
+
# Other languages
|
|
44
|
+
"rust", "ruby", "rails", "php", "java",
|
|
45
|
+
# Alternative runtimes
|
|
46
|
+
"bun", "deno",
|
|
47
|
+
# Custom
|
|
48
|
+
"static", "docker", "other"
|
|
49
|
+
] = Field(
|
|
40
50
|
default="fastapi", description="Web framework"
|
|
41
51
|
)
|
|
42
52
|
entrypoint: Optional[str] = Field(
|
|
@@ -45,17 +55,14 @@ class ServiceDefinition(BaseModel):
|
|
|
45
55
|
command: Optional[str] = Field(
|
|
46
56
|
default=None, description="Custom start command"
|
|
47
57
|
)
|
|
48
|
-
env:
|
|
58
|
+
env: Dict[str, str] = Field(
|
|
49
59
|
default_factory=dict, description="Environment variables"
|
|
50
60
|
)
|
|
51
61
|
package_manager: Optional[str] = Field(
|
|
52
|
-
default="pip", description="Package manager (pip, uv)"
|
|
62
|
+
default="pip", description="Package manager (pip, uv, poetry, pipenv, npm, yarn, pnpm, bun, cargo, go, bundler, composer, maven, gradle)"
|
|
53
63
|
)
|
|
54
64
|
dependency_file: Optional[str] = Field(
|
|
55
|
-
default=
|
|
56
|
-
)
|
|
57
|
-
missing_deps: List[str] = Field(
|
|
58
|
-
default_factory=list, description="Proactively detected missing dependencies"
|
|
65
|
+
default=None, description="Dependency file"
|
|
59
66
|
)
|
|
60
67
|
|
|
61
68
|
@field_validator("name")
|
|
@@ -71,97 +78,69 @@ class ServiceDefinition(BaseModel):
|
|
|
71
78
|
return v.lower()
|
|
72
79
|
|
|
73
80
|
|
|
74
|
-
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
if len(names) != len(set(names)):
|
|
78
|
-
duplicates = [n for n in names if names.count(n) > 1]
|
|
79
|
-
raise ValueError(f"Duplicate service names: {set(duplicates)}")
|
|
80
|
-
return services
|
|
81
|
+
class XenfraConfig(BaseModel):
|
|
82
|
+
"""The root configuration model for xenfra.yaml."""
|
|
83
|
+
model_config = ConfigDict(extra="ignore")
|
|
81
84
|
|
|
85
|
+
name: str = Field(..., min_length=1, description="Project identity")
|
|
86
|
+
digitalocean: ProviderConfig = Field(default_factory=ProviderConfig)
|
|
87
|
+
infrastructure: List[InfrastructureService] = Field(default_factory=list)
|
|
88
|
+
services: List[ServiceDefinition] = Field(default_factory=list)
|
|
89
|
+
mode: Literal["single-droplet", "multi-droplet"] = Field(default="single-droplet")
|
|
82
90
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
@field_validator("services")
|
|
92
|
+
@classmethod
|
|
93
|
+
def validate_unique_services(cls, services: List[ServiceDefinition]) -> List[ServiceDefinition]:
|
|
94
|
+
"""Ensure all service names and ports are unique."""
|
|
95
|
+
names = [s.name for s in services]
|
|
96
|
+
if len(names) != len(set(names)):
|
|
97
|
+
raise ValueError(f"Duplicate service names detected: {set([n for n in names if names.count(n) > 1])}")
|
|
98
|
+
|
|
99
|
+
ports = [s.port for s in services]
|
|
100
|
+
if len(ports) != len(set(ports)):
|
|
101
|
+
raise ValueError(f"Duplicate service ports detected: {set([p for p in ports if ports.count(p) > 1])}")
|
|
102
|
+
|
|
103
|
+
return services
|
|
90
104
|
|
|
105
|
+
def to_yaml(self, path: Path):
|
|
106
|
+
"""Write the config to a YAML file."""
|
|
107
|
+
data = self.model_dump(exclude_none=True)
|
|
108
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
109
|
+
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
91
110
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
Load
|
|
95
|
-
|
|
96
|
-
Args:
|
|
97
|
-
project_path: Path to the project directory (default: current directory)
|
|
98
|
-
|
|
99
|
-
Returns:
|
|
100
|
-
List of ServiceDefinition if 'services' key found, None otherwise
|
|
101
|
-
|
|
102
|
-
Raises:
|
|
103
|
-
ValueError: If services array is invalid
|
|
104
|
-
"""
|
|
111
|
+
|
|
112
|
+
def load_xenfra_config(project_path: str = ".") -> Optional[XenfraConfig]:
|
|
113
|
+
"""Load and validate the entire xenfra.yaml config."""
|
|
105
114
|
yaml_path = Path(project_path) / "xenfra.yaml"
|
|
106
|
-
|
|
107
115
|
if not yaml_path.exists():
|
|
108
116
|
return None
|
|
109
117
|
|
|
110
118
|
with open(yaml_path, "r", encoding="utf-8") as f:
|
|
111
119
|
try:
|
|
112
120
|
data = yaml.safe_load(f)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
services = [ServiceDefinition(**svc) for svc in services_data]
|
|
125
|
-
validate_unique_names(services)
|
|
126
|
-
validate_unique_ports(services)
|
|
127
|
-
return services
|
|
128
|
-
except Exception as e:
|
|
129
|
-
raise ValueError(f"Invalid services configuration in xenfra.yaml: {e}")
|
|
121
|
+
if not data:
|
|
122
|
+
return None
|
|
123
|
+
return XenfraConfig(**data)
|
|
124
|
+
except (yaml.YAMLError, Exception) as e:
|
|
125
|
+
raise ValueError(f"Invalid xenfra.yaml: {e}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# Legacy helper for backwards compatibility
|
|
129
|
+
def load_services_from_xenfra_yaml(project_path: str = ".") -> Optional[List[ServiceDefinition]]:
|
|
130
|
+
config = load_xenfra_config(project_path)
|
|
131
|
+
return config.services if config else None
|
|
130
132
|
|
|
131
133
|
|
|
132
134
|
def is_microservices_project(project_path: str = ".") -> bool:
|
|
133
|
-
"""
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
Returns:
|
|
137
|
-
True if xenfra.yaml has 'services' array with 2+ services
|
|
138
|
-
"""
|
|
139
|
-
try:
|
|
140
|
-
services = load_services_from_xenfra_yaml(project_path)
|
|
141
|
-
return services is not None and len(services) > 1
|
|
142
|
-
except ValueError:
|
|
143
|
-
return False
|
|
135
|
+
"""Check if project has multiple services defined in xenfra.yaml."""
|
|
136
|
+
config = load_xenfra_config(project_path)
|
|
137
|
+
return config is not None and len(config.services) > 1
|
|
144
138
|
|
|
145
139
|
|
|
146
140
|
def get_deployment_mode(project_path: str = ".") -> Optional[str]:
|
|
147
|
-
"""
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
Returns:
|
|
151
|
-
"single-droplet", "multi-droplet", or None if not specified
|
|
152
|
-
"""
|
|
153
|
-
yaml_path = Path(project_path) / "xenfra.yaml"
|
|
154
|
-
|
|
155
|
-
if not yaml_path.exists():
|
|
156
|
-
return None
|
|
157
|
-
|
|
158
|
-
with open(yaml_path, "r", encoding="utf-8") as f:
|
|
159
|
-
try:
|
|
160
|
-
data = yaml.safe_load(f)
|
|
161
|
-
except yaml.YAMLError:
|
|
162
|
-
return None
|
|
163
|
-
|
|
164
|
-
return data.get("mode") if data else None
|
|
141
|
+
"""Get deployment mode from xenfra.yaml."""
|
|
142
|
+
config = load_xenfra_config(project_path)
|
|
143
|
+
return config.mode if config else None
|
|
165
144
|
|
|
166
145
|
|
|
167
146
|
def add_services_to_xenfra_yaml(
|
|
@@ -169,44 +148,20 @@ def add_services_to_xenfra_yaml(
|
|
|
169
148
|
services: List[dict],
|
|
170
149
|
mode: str = "single-droplet"
|
|
171
150
|
) -> Path:
|
|
172
|
-
"""
|
|
173
|
-
|
|
151
|
+
"""Add or update services array in existing xenfra.yaml."""
|
|
152
|
+
config = load_xenfra_config(project_path)
|
|
153
|
+
if not config:
|
|
154
|
+
# Create new config if missing
|
|
155
|
+
config = XenfraConfig(name=Path(project_path).name, mode=mode)
|
|
174
156
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
services: List of service dictionaries from auto-detection
|
|
178
|
-
mode: Deployment mode
|
|
157
|
+
config.services = [ServiceDefinition(**svc) for svc in services]
|
|
158
|
+
config.mode = mode
|
|
179
159
|
|
|
180
|
-
Returns:
|
|
181
|
-
Path to the updated xenfra.yaml
|
|
182
|
-
"""
|
|
183
160
|
yaml_path = Path(project_path) / "xenfra.yaml"
|
|
184
|
-
|
|
185
|
-
# Load existing xenfra.yaml if present
|
|
186
|
-
existing_data = {}
|
|
187
|
-
if yaml_path.exists():
|
|
188
|
-
with open(yaml_path, "r", encoding="utf-8") as f:
|
|
189
|
-
existing_data = yaml.safe_load(f) or {}
|
|
190
|
-
|
|
191
|
-
# Add services array
|
|
192
|
-
existing_data["services"] = services
|
|
193
|
-
existing_data["mode"] = mode
|
|
194
|
-
|
|
195
|
-
# Write back
|
|
196
|
-
with open(yaml_path, "w", encoding="utf-8") as f:
|
|
197
|
-
yaml.dump(existing_data, f, default_flow_style=False, sort_keys=False)
|
|
198
|
-
|
|
161
|
+
config.to_yaml(yaml_path)
|
|
199
162
|
return yaml_path
|
|
200
163
|
|
|
201
164
|
|
|
202
165
|
def create_services_from_detected(services: List[dict]) -> List[ServiceDefinition]:
|
|
203
|
-
"""
|
|
204
|
-
Create ServiceDefinition list from detected services.
|
|
205
|
-
|
|
206
|
-
Args:
|
|
207
|
-
services: List of service dictionaries from auto-detection
|
|
208
|
-
|
|
209
|
-
Returns:
|
|
210
|
-
List of ServiceDefinition instances
|
|
211
|
-
"""
|
|
166
|
+
"""Create ServiceDefinition list from detected services."""
|
|
212
167
|
return [ServiceDefinition(**svc) for svc in services]
|
xenfra_sdk/mcp_client.py
CHANGED
|
@@ -7,6 +7,8 @@ import subprocess
|
|
|
7
7
|
import tempfile
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
from xenfra_sdk.privacy import scrubbed_print
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
class MCPClient:
|
|
12
14
|
"""
|
|
@@ -96,7 +98,7 @@ class MCPClient:
|
|
|
96
98
|
"Invalid repository URL format. Expected format: https://github.com/owner/repo"
|
|
97
99
|
)
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
scrubbed_print(f" [MCP] Fetching file tree for {owner}/{repo_name} at {commit_sha}...")
|
|
100
102
|
tree_result = self._send_request(
|
|
101
103
|
method="git.get_repository_tree",
|
|
102
104
|
params={"owner": owner, "repo": repo_name, "tree_sha": commit_sha, "recursive": True},
|
|
@@ -107,7 +109,7 @@ class MCPClient:
|
|
|
107
109
|
raise RuntimeError("Could not retrieve repository file tree.")
|
|
108
110
|
|
|
109
111
|
temp_dir = tempfile.mkdtemp(prefix=f"xenfra_{repo_name}_")
|
|
110
|
-
|
|
112
|
+
scrubbed_print(f" [MCP] Downloading to temporary directory: {temp_dir}")
|
|
111
113
|
|
|
112
114
|
for item in tree:
|
|
113
115
|
item_path = item.get("path")
|
|
@@ -125,14 +127,14 @@ class MCPClient:
|
|
|
125
127
|
|
|
126
128
|
content_b64 = content_result.get("content")
|
|
127
129
|
if content_b64 is None:
|
|
128
|
-
|
|
130
|
+
scrubbed_print(f" [MCP] [Warning] Could not get content for {item_path}")
|
|
129
131
|
continue
|
|
130
132
|
|
|
131
133
|
try:
|
|
132
134
|
# Content is base64 encoded, with newlines.
|
|
133
135
|
decoded_content = base64.b64decode(content_b64.replace("\n", ""))
|
|
134
136
|
except (base64.binascii.Error, TypeError):
|
|
135
|
-
|
|
137
|
+
scrubbed_print(f" [MCP] [Warning] Could not decode content for {item_path}")
|
|
136
138
|
continue
|
|
137
139
|
|
|
138
140
|
# Create file and parent directories in the temp location
|
|
@@ -143,7 +145,7 @@ class MCPClient:
|
|
|
143
145
|
with open(local_file_path, "wb") as f:
|
|
144
146
|
f.write(decoded_content)
|
|
145
147
|
|
|
146
|
-
|
|
148
|
+
scrubbed_print(" [MCP] ✅ Repository download complete.")
|
|
147
149
|
return temp_dir
|
|
148
150
|
|
|
149
151
|
def __enter__(self):
|
|
@@ -4,7 +4,7 @@ These models are used for data validation, serialization, and providing clear sc
|
|
|
4
4
|
for external tools like OpenAI function calling.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from datetime import datetime
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
8
|
from enum import Enum
|
|
9
9
|
|
|
10
10
|
from pydantic import BaseModel, Field
|
|
@@ -39,6 +39,9 @@ class Deployment(BaseModel):
|
|
|
39
39
|
projectId: str = Field(..., description="Identifier of the project being deployed")
|
|
40
40
|
status: DeploymentStatus = Field(..., description="Current status of the deployment")
|
|
41
41
|
source: str = Field(..., description="Source of the deployment (e.g., 'cli', 'api')")
|
|
42
|
+
mode: str = Field("monolithic", description="Deployment mode: monolithic or microservices")
|
|
43
|
+
services_json: str | None = Field(None, description="JSON string describing microservices (if mode is microservices)")
|
|
44
|
+
is_dockerized: bool = Field(True, description="Whether the deployment is dockerized")
|
|
42
45
|
created_at: datetime = Field(..., description="Timestamp when the deployment was created")
|
|
43
46
|
finished_at: datetime | None = Field(None, description="Timestamp when the deployment finished")
|
|
44
47
|
|
|
@@ -115,6 +118,19 @@ class ProjectRead(BaseModel):
|
|
|
115
118
|
None, description="The estimated monthly cost of the project's infrastructure in USD."
|
|
116
119
|
)
|
|
117
120
|
created_at: datetime = Field(..., description="The timestamp when the project was created.")
|
|
121
|
+
user_id: int = Field(..., description="The ID of the user who owns the project.")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ActivityLog(BaseModel):
|
|
125
|
+
"""
|
|
126
|
+
Represents a user activity log entry.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
id: int | None = Field(None, description="Unique identifier for the log entry")
|
|
130
|
+
user_id: int = Field(..., description="The ID of the user associated with the activity")
|
|
131
|
+
action: str = Field(..., description="The action performed (e.g., 'Deployment Successful')")
|
|
132
|
+
details: str = Field(..., description="Detailed description of the action")
|
|
133
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), description="When the activity occurred")
|
|
118
134
|
|
|
119
135
|
|
|
120
136
|
# Intelligence Service Models
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deployment Context Models - Type-safe configuration for the Xenfra Engine.
|
|
3
|
+
Follows XENFRA_PROTOCOL.md by ensuring all data structures are Pydantic models.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, List, Optional, Any
|
|
7
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
8
|
+
from xenfra_sdk.constants import DEFAULT_PORT_RANGE_START, DEFAULT_REGION, DEFAULT_SIZE, DEFAULT_OS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DeploymentContext(BaseModel):
|
|
12
|
+
"""
|
|
13
|
+
Unified context for a single project or service deployment.
|
|
14
|
+
This model serves as the source of truth for all template rendering and
|
|
15
|
+
infrastructure provisioning.
|
|
16
|
+
"""
|
|
17
|
+
model_config = ConfigDict(extra="ignore")
|
|
18
|
+
|
|
19
|
+
# Project Identity
|
|
20
|
+
project_name: str = Field(..., description="Name of the project")
|
|
21
|
+
email: str = Field(..., description="User email for SSL/Caddy")
|
|
22
|
+
|
|
23
|
+
# Infrastructure (Single Droplet Defaults)
|
|
24
|
+
region: str = Field(default=DEFAULT_REGION)
|
|
25
|
+
size: str = Field(default=DEFAULT_SIZE)
|
|
26
|
+
image: str = Field(default=DEFAULT_OS)
|
|
27
|
+
|
|
28
|
+
# Application Specification
|
|
29
|
+
framework: str = Field(..., description="Web framework (fastapi, flask, django, etc.)")
|
|
30
|
+
port: int = Field(default=DEFAULT_PORT_RANGE_START)
|
|
31
|
+
entrypoint: Optional[str] = Field(None, description="App entrypoint (e.g. main:app)")
|
|
32
|
+
python_version: str = Field(default="3.11-slim")
|
|
33
|
+
|
|
34
|
+
# Deployment Strategy
|
|
35
|
+
is_dockerized: bool = Field(default=True)
|
|
36
|
+
branch: str = Field(default="main")
|
|
37
|
+
source_type: str = Field(default="local") # local or git
|
|
38
|
+
|
|
39
|
+
# Environment & Secrets (Scrubbed by default)
|
|
40
|
+
env_vars: Dict[str, str] = Field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
# Infrastructure Flags
|
|
43
|
+
include_postgres: bool = Field(default=False)
|
|
44
|
+
include_redis: bool = Field(default=False)
|
|
45
|
+
include_kafka: bool = Field(default=False)
|
|
46
|
+
|
|
47
|
+
# Resource Governance
|
|
48
|
+
tier: str = Field(default="FREE")
|
|
49
|
+
cpu_limit: Optional[float] = None
|
|
50
|
+
memory_limit: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
# Internal Metadata
|
|
53
|
+
phase_logs: List[str] = Field(default_factory=list)
|
|
54
|
+
file_manifest: List[Dict[str, Any]] = Field(default_factory=list)
|
|
55
|
+
source_path: Optional[str] = None
|
|
56
|
+
repo_path: Optional[str] = None
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_dict(cls, data: Dict[str, Any]) -> "DeploymentContext":
|
|
60
|
+
"""Backwards compatibility helper to convert legacy dicts."""
|
|
61
|
+
return cls(**data)
|