xenfra-sdk 0.2.4__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 (43) 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/models.py +2 -0
  12. xenfra_sdk/db/session.py +8 -3
  13. xenfra_sdk/detection.py +262 -191
  14. xenfra_sdk/dockerizer.py +76 -120
  15. xenfra_sdk/engine.py +762 -160
  16. xenfra_sdk/events.py +254 -0
  17. xenfra_sdk/exceptions.py +9 -0
  18. xenfra_sdk/governance.py +150 -0
  19. xenfra_sdk/manifest.py +93 -138
  20. xenfra_sdk/mcp_client.py +7 -5
  21. xenfra_sdk/{models.py → models/__init__.py} +17 -1
  22. xenfra_sdk/models/context.py +61 -0
  23. xenfra_sdk/orchestrator.py +223 -99
  24. xenfra_sdk/privacy.py +11 -0
  25. xenfra_sdk/protocol.py +38 -0
  26. xenfra_sdk/railpack_adapter.py +357 -0
  27. xenfra_sdk/railpack_detector.py +587 -0
  28. xenfra_sdk/railpack_manager.py +312 -0
  29. xenfra_sdk/recipes.py +152 -19
  30. xenfra_sdk/resources/activity.py +45 -0
  31. xenfra_sdk/resources/build.py +157 -0
  32. xenfra_sdk/resources/deployments.py +22 -2
  33. xenfra_sdk/resources/intelligence.py +25 -0
  34. xenfra_sdk-0.2.6.dist-info/METADATA +118 -0
  35. xenfra_sdk-0.2.6.dist-info/RECORD +49 -0
  36. {xenfra_sdk-0.2.4.dist-info → xenfra_sdk-0.2.6.dist-info}/WHEEL +1 -1
  37. xenfra_sdk/templates/Caddyfile.j2 +0 -14
  38. xenfra_sdk/templates/Dockerfile.j2 +0 -41
  39. xenfra_sdk/templates/cloud-init.sh.j2 +0 -90
  40. xenfra_sdk/templates/docker-compose-multi.yml.j2 +0 -29
  41. xenfra_sdk/templates/docker-compose.yml.j2 +0 -30
  42. xenfra_sdk-0.2.4.dist-info/METADATA +0 -116
  43. xenfra_sdk-0.2.4.dist-info/RECORD +0 -38
xenfra_sdk/dockerizer.py CHANGED
@@ -9,8 +9,6 @@ from pathlib import Path
9
9
  from typing import Dict, Optional
10
10
  import re
11
11
 
12
- from jinja2 import Environment, FileSystemLoader
13
-
14
12
 
15
13
  def detect_python_version(file_manifest: list = None) -> str:
16
14
  """
@@ -65,131 +63,89 @@ def detect_python_version(file_manifest: list = None) -> str:
65
63
  return default_version
66
64
 
67
65
 
68
- def render_deployment_assets(context: dict) -> Dict[str, str]:
66
+ def detect_entrypoint(file_manifest: list = None, framework: str = "fastapi") -> str:
69
67
  """
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).
68
+ Detect the correct uvicorn/gunicorn entrypoint from project structure.
69
+
70
+ Checks common patterns:
71
+ - main.py in root main:app
72
+ - app.py in root → app:app
73
+ - src/main.py → src.main:app
74
+ - app/main.py → app.main:app
74
75
 
75
76
  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)
77
+ file_manifest: List of file info dicts with 'path' key
78
+ framework: The framework being used (fastapi, flask, django)
86
79
 
87
80
  Returns:
88
- Dict with keys "Dockerfile" and "docker-compose.yml", values are rendered content strings.
89
- Returns empty dict if no framework is provided.
81
+ Entrypoint string (e.g., "main:app", "src.main:app")
90
82
  """
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,
83
+ if not file_manifest:
84
+ return "main:app" # Default fallback
85
+
86
+ # Build set of paths for fast lookup
87
+ paths = {f.get('path', '') for f in file_manifest}
88
+
89
+ # Check common entrypoint patterns in priority order
90
+ entrypoint_patterns = [
91
+ ("main.py", "main:app"),
92
+ ("app.py", "app:app"),
93
+ ("src/main.py", "src.main:app"),
94
+ ("src/app.py", "src.app:app"),
95
+ ("app/main.py", "app.main:app"),
96
+ ("api/main.py", "api.main:app"),
97
+ ]
98
+
99
+ for file_path, entrypoint in entrypoint_patterns:
100
+ if file_path in paths:
101
+ return entrypoint
102
+
103
+ # === PROTOCOL COMPLIANT: Framework Recipe Pattern ===
104
+ # Instead of if-else chains, use a dictionary of patterns
105
+ FRAMEWORK_PATTERNS = {
106
+ "django": {
107
+ "file_suffix": "wsgi.py",
108
+ "instance": "application",
109
+ "class_pattern": None, # Django uses wsgi discovery
110
+ },
111
+ "fastapi": {
112
+ "file_suffix": None,
113
+ "instance": "app",
114
+ "class_pattern": r'([a-zA-Z0-9_]+)\s*=\s*FastAPI\(',
115
+ },
116
+ "flask": {
117
+ "file_suffix": None,
118
+ "instance": "app",
119
+ "class_pattern": r'([a-zA-Z0-9_]+)\s*=\s*Flask\(',
120
+ },
128
121
  }
129
122
 
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()
123
+ # Django special case: Look for wsgi.py
124
+ recipe = FRAMEWORK_PATTERNS.get(framework, {})
125
+ if recipe.get("file_suffix"):
126
+ for path in paths:
127
+ if path.endswith(recipe["file_suffix"]):
128
+ module = path.replace("/", ".").replace(".py", "")
129
+ return f"{module}:{recipe['instance']}"
130
+
131
+ # === SEARCH & RESCUE: Deep Content Scan ===
132
+ # If filename scan fails, look inside .py files for instantiation
133
+ class_pattern = recipe.get("class_pattern")
134
+ if class_pattern:
135
+ for file_info in file_manifest:
136
+ path = file_info.get('path', '')
137
+ if not path.endswith(".py") or path.startswith("tests/"):
138
+ continue
139
+
140
+ content = file_info.get('content', '')
141
+ if not content:
142
+ continue
143
+
144
+ match = re.search(class_pattern, content)
145
+ if match:
146
+ instance = match.group(1)
147
+ module = path.replace("/", ".").replace(".py", "").replace("src.", "").replace("app.", "")
148
+ return f"{module}:{instance}"
154
149
 
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"
150
+ return "main:app" # Default fallback
159
151
 
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