xenfra-sdk 0.2.2__py3-none-any.whl → 0.2.3__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 +61 -21
- xenfra_sdk/cli/main.py +226 -226
- xenfra_sdk/client.py +90 -90
- xenfra_sdk/config.py +26 -26
- xenfra_sdk/db/models.py +24 -24
- xenfra_sdk/db/session.py +30 -30
- xenfra_sdk/dependencies.py +39 -39
- xenfra_sdk/detection.py +396 -0
- xenfra_sdk/dockerizer.py +195 -194
- xenfra_sdk/engine.py +741 -619
- xenfra_sdk/exceptions.py +19 -19
- xenfra_sdk/manifest.py +212 -0
- xenfra_sdk/mcp_client.py +154 -154
- xenfra_sdk/models.py +184 -184
- xenfra_sdk/orchestrator.py +666 -0
- xenfra_sdk/patterns.json +13 -13
- xenfra_sdk/privacy.py +153 -153
- xenfra_sdk/recipes.py +26 -26
- xenfra_sdk/resources/base.py +3 -3
- xenfra_sdk/resources/deployments.py +278 -248
- xenfra_sdk/resources/files.py +101 -101
- xenfra_sdk/resources/intelligence.py +102 -95
- xenfra_sdk/security.py +41 -41
- xenfra_sdk/security_scanner.py +431 -0
- xenfra_sdk/templates/Caddyfile.j2 +14 -0
- xenfra_sdk/templates/Dockerfile.j2 +41 -38
- xenfra_sdk/templates/cloud-init.sh.j2 +90 -90
- xenfra_sdk/templates/docker-compose-multi.yml.j2 +29 -0
- xenfra_sdk/templates/docker-compose.yml.j2 +30 -30
- xenfra_sdk-0.2.3.dist-info/METADATA +116 -0
- xenfra_sdk-0.2.3.dist-info/RECORD +38 -0
- xenfra_sdk-0.2.2.dist-info/METADATA +0 -118
- xenfra_sdk-0.2.2.dist-info/RECORD +0 -32
- {xenfra_sdk-0.2.2.dist-info → xenfra_sdk-0.2.3.dist-info}/WHEEL +0 -0
xenfra_sdk/dockerizer.py
CHANGED
|
@@ -1,194 +1,195 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Xenfra Dockerizer - Generates deployment assets as strings.
|
|
3
|
-
|
|
4
|
-
This module renders Dockerfile and docker-compose.yml templates
|
|
5
|
-
to strings (in-memory) so they can be written to the target droplet via SSH.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Dict, Optional
|
|
10
|
-
import re
|
|
11
|
-
|
|
12
|
-
from jinja2 import Environment, FileSystemLoader
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def detect_python_version(file_manifest: list = None) -> str:
|
|
16
|
-
"""
|
|
17
|
-
Detect Python version from project files.
|
|
18
|
-
|
|
19
|
-
Checks in order:
|
|
20
|
-
1. .python-version file (e.g., "3.13")
|
|
21
|
-
2. pyproject.toml requires-python field (e.g., ">=3.13")
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
file_manifest: List of file info dicts with 'path' and optionally 'content'
|
|
25
|
-
If None, uses 3.11 as default.
|
|
26
|
-
|
|
27
|
-
Returns:
|
|
28
|
-
Docker image version string (e.g., "python:3.13-slim")
|
|
29
|
-
"""
|
|
30
|
-
default_version = "python:3.11-slim"
|
|
31
|
-
|
|
32
|
-
if not file_manifest:
|
|
33
|
-
return default_version
|
|
34
|
-
|
|
35
|
-
# Build a lookup dict for quick access
|
|
36
|
-
file_lookup = {f.get('path', ''): f for f in file_manifest}
|
|
37
|
-
|
|
38
|
-
# Option 1: Check .python-version file
|
|
39
|
-
if '.python-version' in file_lookup:
|
|
40
|
-
file_info = file_lookup['.python-version']
|
|
41
|
-
content = file_info.get('content', '')
|
|
42
|
-
if content:
|
|
43
|
-
# Parse version like "3.13" or "3.13.1"
|
|
44
|
-
version = content.strip().split('\n')[0].strip()
|
|
45
|
-
if version:
|
|
46
|
-
# Extract major.minor (e.g., "3.13" from "3.13.1")
|
|
47
|
-
match = re.match(r'(\d+\.\d+)', version)
|
|
48
|
-
if match:
|
|
49
|
-
return f"python:{match.group(1)}-slim"
|
|
50
|
-
|
|
51
|
-
# Option 2: Check pyproject.toml requires-python
|
|
52
|
-
if 'pyproject.toml' in file_lookup:
|
|
53
|
-
file_info = file_lookup['pyproject.toml']
|
|
54
|
-
content = file_info.get('content', '')
|
|
55
|
-
if content:
|
|
56
|
-
# Parse requires-python = ">=3.13" or "^3.13"
|
|
57
|
-
match = re.search(r'requires-python\s*=\s*["\']([^"\']+)["\']', content)
|
|
58
|
-
if match:
|
|
59
|
-
version_spec = match.group(1)
|
|
60
|
-
# Extract version number (e.g., "3.13" from ">=3.13")
|
|
61
|
-
version_match = re.search(r'(\d+\.\d+)', version_spec)
|
|
62
|
-
if version_match:
|
|
63
|
-
return f"python:{version_match.group(1)}-slim"
|
|
64
|
-
|
|
65
|
-
return default_version
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def render_deployment_assets(context: dict) -> Dict[str, str]:
|
|
69
|
-
"""
|
|
70
|
-
Renders deployment assets (Dockerfile, docker-compose.yml) using Jinja2 templates.
|
|
71
|
-
|
|
72
|
-
IMPORTANT: This function returns strings, NOT files. The caller is responsible
|
|
73
|
-
for writing these to the correct location (e.g., via SSH to a remote droplet).
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
context: A dictionary containing information for rendering templates.
|
|
77
|
-
Required keys:
|
|
78
|
-
- framework: str (fastapi, flask, django)
|
|
79
|
-
- port: int (default 8000)
|
|
80
|
-
Optional keys:
|
|
81
|
-
- command: str (start command, auto-generated if not provided)
|
|
82
|
-
- database: str (postgres, mysql, etc.)
|
|
83
|
-
- package_manager: str (pip, uv)
|
|
84
|
-
- dependency_file: str (requirements.txt, pyproject.toml)
|
|
85
|
-
- python_version: str (default python:3.11-slim)
|
|
86
|
-
|
|
87
|
-
Returns:
|
|
88
|
-
Dict with keys "Dockerfile" and "docker-compose.yml", values are rendered content strings.
|
|
89
|
-
Returns empty dict if no framework is provided.
|
|
90
|
-
"""
|
|
91
|
-
# Path to the templates directory
|
|
92
|
-
template_dir = Path(__file__).parent / "templates"
|
|
93
|
-
env = Environment(loader=FileSystemLoader(template_dir))
|
|
94
|
-
|
|
95
|
-
# Get framework from context (MUST be provided by caller, no auto-detection)
|
|
96
|
-
framework = context.get("framework")
|
|
97
|
-
if not framework:
|
|
98
|
-
# Framework is required - caller should have validated this
|
|
99
|
-
return {}
|
|
100
|
-
|
|
101
|
-
# Get port with default
|
|
102
|
-
port = context.get("port") or 8000
|
|
103
|
-
|
|
104
|
-
# Generate default command based on framework if not provided
|
|
105
|
-
command = context.get("command")
|
|
106
|
-
if not command:
|
|
107
|
-
if framework == "fastapi":
|
|
108
|
-
command = f"uvicorn main:app --host 0.0.0.0 --port {port}"
|
|
109
|
-
elif framework == "flask":
|
|
110
|
-
command = f"gunicorn app:app -b 0.0.0.0:{port}"
|
|
111
|
-
elif framework == "django":
|
|
112
|
-
command = f"gunicorn app.wsgi:application --bind 0.0.0.0:{port}"
|
|
113
|
-
else:
|
|
114
|
-
command = f"uvicorn main:app --host 0.0.0.0 --port {port}"
|
|
115
|
-
|
|
116
|
-
# Build render context with all values
|
|
117
|
-
render_context = {
|
|
118
|
-
"framework": framework,
|
|
119
|
-
"port": port,
|
|
120
|
-
"command": command,
|
|
121
|
-
"database": context.get("database"),
|
|
122
|
-
"package_manager": context.get("package_manager", "pip"),
|
|
123
|
-
"dependency_file": context.get("dependency_file", "requirements.txt"),
|
|
124
|
-
"python_version": context.get("python_version", "python:3.11-slim"),
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
1
|
+
"""
|
|
2
|
+
Xenfra Dockerizer - Generates deployment assets as strings.
|
|
3
|
+
|
|
4
|
+
This module renders Dockerfile and docker-compose.yml templates
|
|
5
|
+
to strings (in-memory) so they can be written to the target droplet via SSH.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Optional
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from jinja2 import Environment, FileSystemLoader
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def detect_python_version(file_manifest: list = None) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Detect Python version from project files.
|
|
18
|
+
|
|
19
|
+
Checks in order:
|
|
20
|
+
1. .python-version file (e.g., "3.13")
|
|
21
|
+
2. pyproject.toml requires-python field (e.g., ">=3.13")
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
file_manifest: List of file info dicts with 'path' and optionally 'content'
|
|
25
|
+
If None, uses 3.11 as default.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Docker image version string (e.g., "python:3.13-slim")
|
|
29
|
+
"""
|
|
30
|
+
default_version = "python:3.11-slim"
|
|
31
|
+
|
|
32
|
+
if not file_manifest:
|
|
33
|
+
return default_version
|
|
34
|
+
|
|
35
|
+
# Build a lookup dict for quick access
|
|
36
|
+
file_lookup = {f.get('path', ''): f for f in file_manifest}
|
|
37
|
+
|
|
38
|
+
# Option 1: Check .python-version file
|
|
39
|
+
if '.python-version' in file_lookup:
|
|
40
|
+
file_info = file_lookup['.python-version']
|
|
41
|
+
content = file_info.get('content', '')
|
|
42
|
+
if content:
|
|
43
|
+
# Parse version like "3.13" or "3.13.1"
|
|
44
|
+
version = content.strip().split('\n')[0].strip()
|
|
45
|
+
if version:
|
|
46
|
+
# Extract major.minor (e.g., "3.13" from "3.13.1")
|
|
47
|
+
match = re.match(r'(\d+\.\d+)', version)
|
|
48
|
+
if match:
|
|
49
|
+
return f"python:{match.group(1)}-slim"
|
|
50
|
+
|
|
51
|
+
# Option 2: Check pyproject.toml requires-python
|
|
52
|
+
if 'pyproject.toml' in file_lookup:
|
|
53
|
+
file_info = file_lookup['pyproject.toml']
|
|
54
|
+
content = file_info.get('content', '')
|
|
55
|
+
if content:
|
|
56
|
+
# Parse requires-python = ">=3.13" or "^3.13"
|
|
57
|
+
match = re.search(r'requires-python\s*=\s*["\']([^"\']+)["\']', content)
|
|
58
|
+
if match:
|
|
59
|
+
version_spec = match.group(1)
|
|
60
|
+
# Extract version number (e.g., "3.13" from ">=3.13")
|
|
61
|
+
version_match = re.search(r'(\d+\.\d+)', version_spec)
|
|
62
|
+
if version_match:
|
|
63
|
+
return f"python:{version_match.group(1)}-slim"
|
|
64
|
+
|
|
65
|
+
return default_version
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def render_deployment_assets(context: dict) -> Dict[str, str]:
|
|
69
|
+
"""
|
|
70
|
+
Renders deployment assets (Dockerfile, docker-compose.yml) using Jinja2 templates.
|
|
71
|
+
|
|
72
|
+
IMPORTANT: This function returns strings, NOT files. The caller is responsible
|
|
73
|
+
for writing these to the correct location (e.g., via SSH to a remote droplet).
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
context: A dictionary containing information for rendering templates.
|
|
77
|
+
Required keys:
|
|
78
|
+
- framework: str (fastapi, flask, django)
|
|
79
|
+
- port: int (default 8000)
|
|
80
|
+
Optional keys:
|
|
81
|
+
- command: str (start command, auto-generated if not provided)
|
|
82
|
+
- database: str (postgres, mysql, etc.)
|
|
83
|
+
- package_manager: str (pip, uv)
|
|
84
|
+
- dependency_file: str (requirements.txt, pyproject.toml)
|
|
85
|
+
- python_version: str (default python:3.11-slim)
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Dict with keys "Dockerfile" and "docker-compose.yml", values are rendered content strings.
|
|
89
|
+
Returns empty dict if no framework is provided.
|
|
90
|
+
"""
|
|
91
|
+
# Path to the templates directory
|
|
92
|
+
template_dir = Path(__file__).parent / "templates"
|
|
93
|
+
env = Environment(loader=FileSystemLoader(template_dir))
|
|
94
|
+
|
|
95
|
+
# Get framework from context (MUST be provided by caller, no auto-detection)
|
|
96
|
+
framework = context.get("framework")
|
|
97
|
+
if not framework:
|
|
98
|
+
# Framework is required - caller should have validated this
|
|
99
|
+
return {}
|
|
100
|
+
|
|
101
|
+
# Get port with default
|
|
102
|
+
port = context.get("port") or 8000
|
|
103
|
+
|
|
104
|
+
# Generate default command based on framework if not provided
|
|
105
|
+
command = context.get("command")
|
|
106
|
+
if not command:
|
|
107
|
+
if framework == "fastapi":
|
|
108
|
+
command = f"uvicorn main:app --host 0.0.0.0 --port {port}"
|
|
109
|
+
elif framework == "flask":
|
|
110
|
+
command = f"gunicorn app:app -b 0.0.0.0:{port}"
|
|
111
|
+
elif framework == "django":
|
|
112
|
+
command = f"gunicorn app.wsgi:application --bind 0.0.0.0:{port}"
|
|
113
|
+
else:
|
|
114
|
+
command = f"uvicorn main:app --host 0.0.0.0 --port {port}"
|
|
115
|
+
|
|
116
|
+
# Build render context with all values
|
|
117
|
+
render_context = {
|
|
118
|
+
"framework": framework,
|
|
119
|
+
"port": port,
|
|
120
|
+
"command": command,
|
|
121
|
+
"database": context.get("database"),
|
|
122
|
+
"package_manager": context.get("package_manager", "pip"),
|
|
123
|
+
"dependency_file": context.get("dependency_file", "requirements.txt"),
|
|
124
|
+
"python_version": context.get("python_version", "python:3.11-slim"),
|
|
125
|
+
"missing_deps": context.get("missing_deps", []),
|
|
126
|
+
# Pass through any additional context
|
|
127
|
+
**context,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
result = {}
|
|
131
|
+
|
|
132
|
+
# --- 1. Render Dockerfile ---
|
|
133
|
+
dockerfile_template = env.get_template("Dockerfile.j2")
|
|
134
|
+
result["Dockerfile"] = dockerfile_template.render(render_context)
|
|
135
|
+
|
|
136
|
+
# --- 2. Render docker-compose.yml ---
|
|
137
|
+
compose_template = env.get_template("docker-compose.yml.j2")
|
|
138
|
+
result["docker-compose.yml"] = compose_template.render(render_context)
|
|
139
|
+
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Keep detect_framework for potential local CLI use (not used in remote deployment)
|
|
144
|
+
def detect_framework(path: str = ".") -> tuple:
|
|
145
|
+
"""
|
|
146
|
+
Scans common Python project structures to guess the framework and entrypoint.
|
|
147
|
+
|
|
148
|
+
NOTE: This is only useful when running LOCALLY on the user's machine.
|
|
149
|
+
It should NOT be called when the engine runs on a remote server.
|
|
150
|
+
|
|
151
|
+
Returns: (framework_name, default_port, start_command) or (None, None, None)
|
|
152
|
+
"""
|
|
153
|
+
project_root = Path(path).resolve()
|
|
154
|
+
|
|
155
|
+
# Check for Django first (common pattern: manage.py in root)
|
|
156
|
+
if (project_root / "manage.py").is_file():
|
|
157
|
+
project_name = project_root.name
|
|
158
|
+
return "django", 8000, f"gunicorn {project_name}.wsgi:application --bind 0.0.0.0:8000"
|
|
159
|
+
|
|
160
|
+
candidate_files = []
|
|
161
|
+
|
|
162
|
+
# Check directly in project root
|
|
163
|
+
for name in ["main.py", "app.py"]:
|
|
164
|
+
if (project_root / name).is_file():
|
|
165
|
+
candidate_files.append(project_root / name)
|
|
166
|
+
|
|
167
|
+
# Check in src/*/ (standard package layout)
|
|
168
|
+
for src_dir in project_root.glob("src/*"):
|
|
169
|
+
if src_dir.is_dir():
|
|
170
|
+
for name in ["main.py", "app.py"]:
|
|
171
|
+
if (src_dir / name).is_file():
|
|
172
|
+
candidate_files.append(src_dir / name)
|
|
173
|
+
|
|
174
|
+
import os
|
|
175
|
+
for file_path in candidate_files:
|
|
176
|
+
try:
|
|
177
|
+
with open(file_path, "r") as f:
|
|
178
|
+
content = f.read()
|
|
179
|
+
except Exception:
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
module_name = str(file_path.relative_to(project_root)).replace(os.sep, '.')[:-3]
|
|
184
|
+
if module_name.startswith("src."):
|
|
185
|
+
module_name = module_name[4:]
|
|
186
|
+
except ValueError:
|
|
187
|
+
module_name = file_path.stem
|
|
188
|
+
|
|
189
|
+
if "FastAPI" in content:
|
|
190
|
+
return "fastapi", 8000, f"uvicorn {module_name}:app --host 0.0.0.0 --port 8000"
|
|
191
|
+
|
|
192
|
+
if "Flask" in content:
|
|
193
|
+
return "flask", 5000, f"gunicorn {module_name}:app -b 0.0.0.0:5000"
|
|
194
|
+
|
|
195
|
+
return None, None, None
|