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.
Files changed (42) hide show
  1. xenfra_sdk/__init__.py +46 -2
  2. xenfra_sdk/blueprints/base.py +150 -0
  3. xenfra_sdk/blueprints/factory.py +99 -0
  4. xenfra_sdk/blueprints/node.py +219 -0
  5. xenfra_sdk/blueprints/python.py +57 -0
  6. xenfra_sdk/blueprints/railpack.py +99 -0
  7. xenfra_sdk/blueprints/schema.py +70 -0
  8. xenfra_sdk/cli/main.py +175 -49
  9. xenfra_sdk/client.py +6 -2
  10. xenfra_sdk/constants.py +26 -0
  11. xenfra_sdk/db/session.py +8 -3
  12. xenfra_sdk/detection.py +262 -191
  13. xenfra_sdk/dockerizer.py +76 -120
  14. xenfra_sdk/engine.py +758 -172
  15. xenfra_sdk/events.py +254 -0
  16. xenfra_sdk/exceptions.py +9 -0
  17. xenfra_sdk/governance.py +150 -0
  18. xenfra_sdk/manifest.py +93 -138
  19. xenfra_sdk/mcp_client.py +7 -5
  20. xenfra_sdk/{models.py → models/__init__.py} +17 -1
  21. xenfra_sdk/models/context.py +61 -0
  22. xenfra_sdk/orchestrator.py +223 -99
  23. xenfra_sdk/privacy.py +11 -0
  24. xenfra_sdk/protocol.py +38 -0
  25. xenfra_sdk/railpack_adapter.py +357 -0
  26. xenfra_sdk/railpack_detector.py +587 -0
  27. xenfra_sdk/railpack_manager.py +312 -0
  28. xenfra_sdk/recipes.py +152 -19
  29. xenfra_sdk/resources/activity.py +45 -0
  30. xenfra_sdk/resources/build.py +157 -0
  31. xenfra_sdk/resources/deployments.py +22 -2
  32. xenfra_sdk/resources/intelligence.py +25 -0
  33. xenfra_sdk-0.2.6.dist-info/METADATA +118 -0
  34. xenfra_sdk-0.2.6.dist-info/RECORD +49 -0
  35. {xenfra_sdk-0.2.5.dist-info → xenfra_sdk-0.2.6.dist-info}/WHEEL +1 -1
  36. xenfra_sdk/templates/Caddyfile.j2 +0 -14
  37. xenfra_sdk/templates/Dockerfile.j2 +0 -41
  38. xenfra_sdk/templates/cloud-init.sh.j2 +0 -90
  39. xenfra_sdk/templates/docker-compose-multi.yml.j2 +0 -29
  40. xenfra_sdk/templates/docker-compose.yml.j2 +0 -30
  41. xenfra_sdk-0.2.5.dist-info/METADATA +0 -116
  42. xenfra_sdk-0.2.5.dist-info/RECORD +0 -38
xenfra_sdk/manifest.py CHANGED
@@ -1,42 +1,52 @@
1
- """
2
- Xenfra Microservices Manifest - Schema and Parser for xenfra.yaml services array.
1
+ from pathlib import Path
2
+ from typing import List, Literal, Optional, Dict, Any
3
3
 
4
- This module defines the Pydantic models for microservices deployment configuration.
5
- For microservices projects, xenfra.yaml includes a 'services' array:
4
+ import yaml
5
+ from pydantic import BaseModel, Field, field_validator, ConfigDict
6
6
 
7
- project_name: my-app
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
- import yaml
20
- from pydantic import BaseModel, Field, field_validator
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["fastapi", "flask", "django", "other"] = Field(
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: Optional[dict] = Field(
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="requirements.txt", description="Dependency file"
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
- def validate_unique_names(services: List[ServiceDefinition]) -> List[ServiceDefinition]:
75
- """Ensure all service names are unique."""
76
- names = [s.name for s in services]
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
- def validate_unique_ports(services: List[ServiceDefinition]) -> List[ServiceDefinition]:
84
- """Ensure all service ports are unique."""
85
- ports = [s.port for s in services]
86
- if len(ports) != len(set(ports)):
87
- duplicates = [p for p in ports if ports.count(p) > 1]
88
- raise ValueError(f"Duplicate service ports: {set(duplicates)}")
89
- return services
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
- def load_services_from_xenfra_yaml(project_path: str = ".") -> Optional[List[ServiceDefinition]]:
93
- """
94
- Load services array from xenfra.yaml if present.
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
- except yaml.YAMLError:
114
- return None
115
-
116
- if not data or "services" not in data:
117
- return None
118
-
119
- services_data = data.get("services", [])
120
- if not services_data or not isinstance(services_data, list):
121
- return None
122
-
123
- try:
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
- Check if project has multiple services defined in xenfra.yaml.
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
- Get deployment mode from xenfra.yaml if specified.
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
- Add or update services array in existing xenfra.yaml.
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
- Args:
176
- project_path: Path to the project directory
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
- print(f" [MCP] Fetching file tree for {owner}/{repo_name} at {commit_sha}...")
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
- print(f" [MCP] Downloading to temporary directory: {temp_dir}")
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
- print(f" [MCP] [Warning] Could not get content for {item_path}")
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
- print(f" [MCP] [Warning] Could not decode content for {item_path}")
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
- print(" [MCP] ✅ Repository download complete.")
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)