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/detection.py CHANGED
@@ -16,20 +16,26 @@ class DetectionResult:
16
16
  tier: str, # "backend", "frontend", "agent"
17
17
  framework: str,
18
18
  entrypoint: Optional[str] = None,
19
+ command: Optional[str] = None,
19
20
  port: int = 8000,
20
21
  package_manager: str = "pip",
21
22
  dependency_file: str = "requirements.txt",
22
- missing_deps: List[str] = None
23
+ missing_deps: List[str] = None,
24
+ required_secrets: List[str] = None,
25
+ is_infrastructure: bool = False
23
26
  ):
24
27
  self.name = name
25
28
  self.path = path
26
29
  self.tier = tier
27
30
  self.framework = framework
28
31
  self.entrypoint = entrypoint
32
+ self.command = command
29
33
  self.port = port
30
34
  self.package_manager = package_manager
31
35
  self.dependency_file = dependency_file
32
36
  self.missing_deps = missing_deps or []
37
+ self.required_secrets = required_secrets or []
38
+ self.is_infrastructure = is_infrastructure
33
39
 
34
40
  def to_dict(self) -> dict:
35
41
  return {
@@ -38,240 +44,296 @@ class DetectionResult:
38
44
  "tier": self.tier,
39
45
  "framework": self.framework,
40
46
  "entrypoint": self.entrypoint,
47
+ "command": self.command,
41
48
  "port": self.port,
42
49
  "package_manager": self.package_manager,
43
50
  "dependency_file": self.dependency_file,
44
51
  "missing_deps": self.missing_deps,
52
+ "required_secrets": self.required_secrets,
53
+ "is_infrastructure": self.is_infrastructure,
45
54
  }
46
55
 
47
56
 
48
- class BaseProbe(ABC):
49
- """Base class for language-specific detection probes."""
57
+ class DetectionRegistry:
58
+ """Registry of prioritized probes for service detection."""
59
+ _service_probes: List[tuple] = []
60
+
61
+ # Infrastructure indicators (Ported from discovery.py)
62
+ INFRA_INDICATORS = {
63
+ "kafka": [r"kafka-python", r"confluent-kafka", r"aiokafka"],
64
+ "redis": [r"redis", r"aioredis"],
65
+ "postgres": [r"psycopg2", r"asyncpg", r"sqlalchemy", r"sqlmodel"],
66
+ "mongodb": [r"pymongo", r"motor"],
67
+ }
50
68
 
51
- @abstractmethod
69
+ @classmethod
70
+ def infer_infrastructure_from_services(cls, services: List[DetectionResult]) -> List[DetectionResult]:
71
+ """Scans service dependencies to suggest infrastructure components."""
72
+ found_infra_types = set()
73
+ for svc in services:
74
+ svc_path = Path(svc.path)
75
+ for dep_file in ["requirements.txt", "package.json", "go.mod", "Cargo.toml", "pyproject.toml"]:
76
+ fpath = svc_path / dep_file
77
+ if not fpath.exists(): continue
78
+ content = fpath.read_text(errors="ignore")
79
+ for infra_type, patterns in cls.INFRA_INDICATORS.items():
80
+ for pattern in patterns:
81
+ if re.search(pattern, content, re.IGNORECASE):
82
+ found_infra_types.add(infra_type)
83
+
84
+ infra_results = []
85
+ for itype in found_infra_types:
86
+ infra_results.append(DetectionResult(
87
+ name=f"{itype}-cluster",
88
+ path="",
89
+ tier="infrastructure",
90
+ framework=itype,
91
+ is_infrastructure=True
92
+ ))
93
+ return infra_results
94
+
95
+ @classmethod
96
+ def register_service_probe(cls, priority: int):
97
+ """Decorator to register a detail probe (e.g. Python, Railpack)."""
98
+ def decorator(probe_cls):
99
+ cls._service_probes.append((priority, probe_cls()))
100
+ cls._service_probes.sort(key=lambda x: x[0])
101
+ return probe_cls
102
+ return decorator
103
+
104
+ @classmethod
105
+ def detect_service_details(cls, service_dir: Path, name: str) -> Optional[DetectionResult]:
106
+ """Run all registered service detail probes in priority order."""
107
+ for priority, probe in cls._service_probes:
108
+ result = probe.detect(service_dir, name)
109
+ if result:
110
+ return result
111
+ return None
112
+
113
+
114
+ @DetectionRegistry.register_service_probe(priority=0)
115
+ class RailpackProbe:
116
+ """Uses Railway's Railpack for high-fidelity service detection."""
52
117
  def detect(self, service_dir: Path, name: str) -> Optional[DetectionResult]:
53
- pass
118
+ try:
119
+ from xenfra_sdk.railpack_detector import get_railpack_detector
120
+ detector = get_railpack_detector()
121
+ # Railpack's fallback logic is already decent, but we use it here
122
+ rp_result = detector.detect_from_path(str(service_dir))
123
+
124
+ if not rp_result or rp_result.framework == "unknown":
125
+ return None
126
+
127
+ return DetectionResult(
128
+ name=name,
129
+ path=str(service_dir),
130
+ tier="frontend" if rp_result.runtime_name == "node" and rp_result.framework in ["nextjs", "react"] else "backend",
131
+ framework=rp_result.framework,
132
+ command=rp_result.start_command,
133
+ port=rp_result.detected_port or 8000,
134
+ package_manager=rp_result.package_manager or "pip",
135
+ dependency_file=rp_result.detected_from,
136
+ missing_deps=_scan_proactive_dependencies(service_dir)
137
+ )
138
+ except Exception:
139
+ return None
54
140
 
55
141
 
56
- class PythonProbe(BaseProbe):
57
- """Probe for Python-based backends and agents."""
58
-
142
+ @DetectionRegistry.register_service_probe(priority=1)
143
+ class FrameworkProbe:
144
+ """Static file-based detection for common frameworks."""
59
145
  def detect(self, service_dir: Path, name: str) -> Optional[DetectionResult]:
146
+ # Python Check
60
147
  has_pyproject = (service_dir / "pyproject.toml").exists()
61
148
  has_requirements = (service_dir / "requirements.txt").exists()
62
-
63
- if not has_pyproject and not has_requirements:
64
- return None
65
-
149
+ if has_pyproject or has_requirements:
150
+ return self._detect_python(service_dir, name, has_pyproject)
151
+
152
+ # Node Check
153
+ if (service_dir / "package.json").exists():
154
+ return self._detect_node(service_dir, name)
155
+
156
+ # Go Check
157
+ if (service_dir / "go.mod").exists():
158
+ return self._detect_go(service_dir, name)
159
+
160
+ return None
161
+
162
+ def _detect_python(self, service_dir, name, has_pyproject):
66
163
  content = ""
67
164
  if has_pyproject:
68
165
  content = (service_dir / "pyproject.toml").read_text(errors="ignore").lower()
69
- elif has_requirements:
166
+ else:
70
167
  content = (service_dir / "requirements.txt").read_text(errors="ignore").lower()
71
168
 
72
- # Determine Tier & Framework
73
169
  tier = "backend"
74
- framework = "fastapi" # Default
75
-
76
- # Check for Agents
170
+ framework = "fastapi"
77
171
  if any(lib in content for lib in ["mcp", "langgraph", "crewai", "langchain"]):
78
172
  tier = "agent"
79
173
  framework = "mcp" if "mcp" in content else "langgraph"
80
- elif "django" in content or "djangorestframework" in content:
81
- framework = "django"
82
- elif "flask" in content:
83
- framework = "flask"
84
-
85
- entrypoint = _detect_python_entrypoint(service_dir, name)
86
-
87
- # Proactive Dependency Scanning (Zen Mode Auto-Healing)
88
- missing_deps = _scan_proactive_dependencies(service_dir)
174
+ elif "django" in content: framework = "django"
175
+ elif "flask" in content: framework = "flask"
89
176
 
90
177
  return DetectionResult(
91
178
  name=name,
92
179
  path=str(service_dir),
93
180
  tier=tier,
94
181
  framework=framework,
95
- entrypoint=entrypoint,
182
+ entrypoint=_detect_python_entrypoint(service_dir, name),
96
183
  package_manager="uv" if has_pyproject else "pip",
97
184
  dependency_file="pyproject.toml" if has_pyproject else "requirements.txt",
98
- missing_deps=missing_deps
185
+ missing_deps=_scan_proactive_dependencies(service_dir)
99
186
  )
100
187
 
101
-
102
- class NodeProbe(BaseProbe):
103
- """Probe for Node.js based frontends and backends."""
104
-
105
- def detect(self, service_dir: Path, name: str) -> Optional[DetectionResult]:
106
- package_json = service_dir / "package.json"
107
- if not package_json.exists():
108
- return None
109
-
188
+ def _detect_node(self, service_dir, name):
110
189
  try:
111
- with open(package_json, "r") as f:
112
- config = json.load(f)
113
- except Exception:
114
- return None
115
-
116
- deps = {**config.get("dependencies", {}), **config.get("devDependencies", {})}
117
-
118
- # Determine Tier
119
- tier = "backend"
120
- framework = "node"
121
-
122
- if any(lib in deps for lib in ["next", "react", "vue", "svelte", "nuxt", "angular"]):
123
- tier = "frontend"
124
- framework = "nextjs" if "next" in deps else ("react" if "react" in deps else "frontend")
125
- elif any(lib in deps for lib in ["express", "fastify", "nest", "hono"]):
126
- tier = "backend"
127
- framework = "express" if "express" in deps else "node-backend"
128
- elif "@modelcontextprotocol/sdk" in deps:
129
- tier = "agent"
130
- framework = "mcp"
131
-
190
+ config = json.loads((service_dir / "package.json").read_text())
191
+ deps = {**config.get("dependencies", {}), **config.get("devDependencies", {})}
192
+ tier = "frontend" if any(l in deps for l in ["next", "react", "vue"]) else "backend"
193
+ framework = "nextjs" if "next" in deps else ("express" if "express" in deps else "node")
194
+ return DetectionResult(
195
+ name=name, path=str(service_dir), tier=tier, framework=framework,
196
+ package_manager="npm", dependency_file="package.json",
197
+ port=3000 if tier == "frontend" else 8000
198
+ )
199
+ except Exception: return None
200
+
201
+ def _detect_go(self, service_dir, name):
202
+ content = (service_dir / "go.mod").read_text(errors="ignore").lower()
203
+ framework = "gin" if "gin-gonic" in content else ("echo" if "echo" in content else "go-std")
132
204
  return DetectionResult(
133
- name=name,
134
- path=str(service_dir),
135
- tier=tier,
136
- framework=framework,
137
- package_manager="npm",
138
- dependency_file="package.json",
139
- port=3000 if tier == "frontend" else 8000
205
+ name=name, path=str(service_dir), tier="backend", framework=framework,
206
+ package_manager="go", dependency_file="go.mod"
140
207
  )
141
208
 
142
209
 
143
- class GoProbe(BaseProbe):
144
- """Probe for Go backends."""
210
+ class TopLevelRegistry:
211
+ """Registry of high-level probes (e.g. Docker Compose, Recursive Search)."""
212
+ _top_probes: List[tuple] = []
145
213
 
146
- def detect(self, service_dir: Path, name: str) -> Optional[DetectionResult]:
147
- go_mod = service_dir / "go.mod"
148
- if not go_mod.exists():
149
- return None
214
+ @classmethod
215
+ def register(cls, priority: int):
216
+ def decorator(probe_cls):
217
+ cls._top_probes.append((priority, probe_cls()))
218
+ cls._top_probes.sort(key=lambda x: x[0])
219
+ return probe_cls
220
+ return decorator
221
+
222
+ @classmethod
223
+ def auto_detect(cls, project_path: Path) -> List[DetectionResult]:
224
+ """Execute top-level probes in order and return results from the first one that succeeds."""
225
+ for priority, probe in cls._top_probes:
226
+ results = probe.scan(project_path)
227
+ if results:
228
+ return results
229
+ return []
230
+
231
+
232
+ @TopLevelRegistry.register(priority=0)
233
+ class DockerComposeProbe:
234
+ """Detects services from docker-compose.yml files."""
235
+ def scan(self, project_path: Path) -> List[DetectionResult]:
236
+ candidates = ["docker-compose.prod.yml", "docker-compose.prod.yaml", "docker-compose.yml", "docker-compose.yaml"]
237
+ compose_path = next((project_path / c for c in candidates if (project_path / c).exists()), None)
238
+ if not compose_path: return []
239
+
240
+ try:
241
+ with open(compose_path, "r", encoding="utf-8") as f:
242
+ compose = yaml.safe_load(f)
243
+ if not compose or "services" not in compose: return []
244
+ except Exception: return []
245
+
246
+ results = []
247
+ for name, config in compose.get("services", {}).items():
248
+ if _is_infrastructure_service(name, config): continue
150
249
 
151
- content = go_mod.read_text(errors="ignore").lower()
152
- framework = "go-std"
153
- if "gin-gonic" in content:
154
- framework = "gin"
155
- elif "labstack/echo" in content:
156
- framework = "echo"
250
+ build_config = config.get("build", ".")
251
+ build_path = build_config.get("context", ".") if isinstance(build_config, dict) else str(build_config)
252
+ src_path = (project_path / build_path).resolve()
157
253
 
158
- return DetectionResult(
159
- name=name,
160
- path=str(service_dir),
161
- tier="backend",
162
- framework=framework,
163
- package_manager="go",
164
- dependency_file="go.mod"
165
- )
166
-
167
-
168
- class DetectionRegistry:
169
- """Registry of probes for multi-language detection."""
170
-
171
- def __init__(self):
172
- self.probes: List[BaseProbe] = [
173
- PythonProbe(),
174
- NodeProbe(),
175
- GoProbe()
176
- ]
254
+ # Use detail probes to identify what's inside the build context
255
+ res = DetectionRegistry.detect_service_details(src_path, name)
256
+ if res:
257
+ res.path = build_path # Keep relative path from compose
258
+ res.port = _extract_port(config.get("ports", []), config.get("expose", []), res.port)
259
+ results.append(res)
260
+ else:
261
+ # Fallback for unrecognized services
262
+ results.append(DetectionResult(
263
+ name=name, path=build_path, tier="backend", framework="other",
264
+ port=_extract_port(config.get("ports", []), config.get("expose", []), 8000)
265
+ ))
266
+ return results
267
+
268
+
269
+ @TopLevelRegistry.register(priority=1)
270
+ class RecursiveScannerProbe:
271
+ """Consolidated recursive scanner (ports logic from discovery.py)."""
272
+ def scan(self, project_path: Path) -> List[DetectionResult]:
273
+ results = []
274
+ project_root = project_path.resolve()
177
275
 
178
- def detect_all(self, service_dir: Path, name: str) -> Optional[DetectionResult]:
179
- for probe in self.probes:
180
- result = probe.detect(service_dir, name)
181
- if result:
182
- return result
183
- return None
276
+ # Scan standard directories first
277
+ for dir_name in ["services", "apps"]:
278
+ s_dir = project_root / dir_name
279
+ if not s_dir.exists(): continue
280
+ for subdir in [d for d in s_dir.iterdir() if d.is_dir()]:
281
+ if subdir.name.startswith((".", "_")) or subdir.name in {"common", "lib", "assets"}:
282
+ continue
283
+ res = DetectionRegistry.detect_service_details(subdir, subdir.name)
284
+ if res:
285
+ res.path = f"./{dir_name}/{subdir.name}"
286
+ results.append(res)
287
+
288
+ # If still nothing, do a deeper recursive scan (Option: discovery.py logic)
289
+ if not results:
290
+ results = self._deep_scan(project_root, project_root)
291
+
292
+ return results
184
293
 
294
+ def _deep_scan(self, path: Path, root: Path) -> List[DetectionResult]:
295
+ # Implementation similar to discovery.py but using Detail Probes
296
+ if path.name.startswith(".") or path.name in ["venv", "node_modules", "target", "build", "dist"]:
297
+ return []
298
+
299
+ res = DetectionRegistry.detect_service_details(path, path.name)
300
+ if res and path != root:
301
+ res.path = str(path.relative_to(root))
302
+ return [res]
303
+
304
+ results = []
305
+ if path.is_dir():
306
+ for item in path.iterdir():
307
+ if item.is_dir():
308
+ results.extend(self._deep_scan(item, root))
309
+ return results
185
310
 
186
- # --- Singleton for discovery ---
187
- _registry = DetectionRegistry()
188
311
 
312
+ def auto_detect_services(project_path: str = ".") -> Optional[List[dict]]:
313
+ """Try all detection methods in priority order using the Unified Brain."""
314
+ path = Path(project_path)
315
+ results = TopLevelRegistry.auto_detect(path)
316
+ if results:
317
+ infra = DetectionRegistry.infer_infrastructure_from_services(results)
318
+ results.extend(infra)
319
+ return [r.to_dict() for r in results]
320
+ return None
189
321
 
190
- def detect_docker_compose_services(project_path: str = ".") -> Optional[List[dict]]:
191
- """Parse docker-compose.yml to detect services."""
192
- compose_path = Path(project_path) / "docker-compose.yml"
193
- if not compose_path.exists():
194
- compose_path = Path(project_path) / "docker-compose.yaml"
195
-
196
- if not compose_path.exists():
197
- return None
198
-
199
- try:
200
- with open(compose_path, "r", encoding="utf-8") as f:
201
- compose = yaml.safe_load(f)
202
- except yaml.YAMLError:
203
- return None
204
-
205
- if not compose or "services" not in compose:
206
- return None
207
-
208
- services = []
209
- base_port = 8000
210
-
211
- for name, config in compose.get("services", {}).items():
212
- if _is_infrastructure_service(name, config):
213
- continue
214
-
215
- build_config = config.get("build", ".")
216
- build_path = build_config.get("context", ".") if isinstance(build_config, dict) else str(build_config)
217
-
218
- src_path = (Path(project_path) / build_path).resolve()
219
-
220
- # Use our new registry for deep detection
221
- result = _registry.detect_all(src_path, name)
222
-
223
- if result:
224
- svc_dict = result.to_dict()
225
- svc_dict["path"] = build_path # Use relative path from compose
226
- # Override port if explicitly set in compose
227
- svc_dict["port"] = _extract_port(config.get("ports", []), svc_dict["port"])
228
- services.append(svc_dict)
229
- else:
230
- # Fallback for unrecognized languages
231
- services.append({
232
- "name": name,
233
- "path": build_path,
234
- "port": _extract_port(config.get("ports", []), base_port + len(services)),
235
- "framework": "other",
236
- "tier": "backend"
237
- })
238
-
239
- return services if services else None
240
322
 
323
+ # --- Legacy Wrappers (Backward Compatibility) ---
241
324
 
242
- def detect_pyproject_services(project_path: str = ".") -> Optional[List[dict]]:
243
- """Scan for multiple services/* directories with language indicators."""
244
- services = []
245
- project_root = Path(project_path).resolve()
246
-
247
- services_dir = project_root / "services"
248
- if not services_dir.exists():
249
- return None
250
-
251
- for service_dir in sorted(services_dir.iterdir()):
252
- if not service_dir.is_dir():
253
- continue
254
-
255
- result = _registry.detect_all(service_dir, service_dir.name)
256
- if result:
257
- svc_dict = result.to_dict()
258
- svc_dict["path"] = f"./services/{service_dir.name}"
259
- services.append(svc_dict)
260
-
261
- return services if len(services) > 1 else None
325
+ def detect_docker_compose_services(project_path: str = ".") -> List[dict]:
326
+ """Wrapper for DockerComposeProbe."""
327
+ probe = DockerComposeProbe()
328
+ results = probe.scan(Path(project_path))
329
+ return [r.to_dict() for r in results]
262
330
 
263
331
 
264
- def auto_detect_services(project_path: str = ".") -> Optional[List[dict]]:
265
- """Try all detection methods in priority order."""
266
- services = detect_docker_compose_services(project_path)
267
- if services:
268
- return services
269
-
270
- services = detect_pyproject_services(project_path)
271
- if services:
272
- return services
273
-
274
- return None
332
+ def detect_pyproject_services(project_path: str = ".") -> List[dict]:
333
+ """Wrapper for RecursiveScannerProbe."""
334
+ probe = RecursiveScannerProbe()
335
+ results = probe.scan(Path(project_path))
336
+ return [r.to_dict() for r in results]
275
337
 
276
338
 
277
339
  # --- Helper Functions (Internal) ---
@@ -284,12 +346,21 @@ def _is_infrastructure_service(name: str, config: dict) -> bool:
284
346
  return any(infra in image.lower() for infra in infra_names)
285
347
 
286
348
 
287
- def _extract_port(ports: list, default: int) -> int:
288
- if not ports: return default
289
- port_str = str(ports[0])
290
- if isinstance(ports[0], dict): return int(ports[0].get("published", default))
291
- match = re.match(r"(\d+):", port_str)
292
- return int(match.group(1)) if match else (int(port_str) if port_str.isdigit() else default)
349
+ def _extract_port(ports: list, expose: list, default: int) -> int:
350
+ # 1. Check 'ports' (published ports)
351
+ if ports:
352
+ port_str = str(ports[0])
353
+ if isinstance(ports[0], dict): return int(ports[0].get("published", default))
354
+ match = re.match(r"(\d+):", port_str)
355
+ if match: return int(match.group(1))
356
+ if port_str.isdigit(): return int(port_str)
357
+
358
+ # 2. Check 'expose' (internal only)
359
+ if expose:
360
+ port_str = str(expose[0])
361
+ if port_str.isdigit(): return int(port_str)
362
+
363
+ return default
293
364
 
294
365
 
295
366
  def _detect_python_entrypoint(service_dir: Path, service_name: str = None) -> Optional[str]: