xenfra-sdk 0.2.5__tar.gz → 0.2.7__tar.gz
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-0.2.7/PKG-INFO +118 -0
- xenfra_sdk-0.2.7/README.md +91 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/pyproject.toml +2 -19
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/__init__.py +46 -2
- xenfra_sdk-0.2.7/src/xenfra_sdk/blueprints/base.py +150 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/blueprints/factory.py +99 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/blueprints/node.py +219 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/blueprints/python.py +57 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/blueprints/railpack.py +99 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/blueprints/schema.py +70 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/cli/main.py +352 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/client.py +6 -2
- xenfra_sdk-0.2.7/src/xenfra_sdk/constants.py +26 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/db/session.py +8 -3
- xenfra_sdk-0.2.7/src/xenfra_sdk/detection.py +467 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/dockerizer.py +151 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/engine.py +1352 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/events.py +254 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/exceptions.py +9 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/governance.py +150 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/manifest.py +167 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/mcp_client.py +7 -5
- xenfra_sdk-0.2.5/src/xenfra_sdk/models.py → xenfra_sdk-0.2.7/src/xenfra_sdk/models/__init__.py +17 -1
- xenfra_sdk-0.2.7/src/xenfra_sdk/models/context.py +61 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/orchestrator.py +223 -99
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/privacy.py +11 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/protocol.py +38 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/railpack_adapter.py +357 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/railpack_detector.py +587 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/railpack_manager.py +312 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/recipes.py +159 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/resources/activity.py +45 -0
- xenfra_sdk-0.2.7/src/xenfra_sdk/resources/build.py +157 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/resources/deployments.py +22 -2
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/resources/intelligence.py +25 -0
- xenfra_sdk-0.2.5/PKG-INFO +0 -116
- xenfra_sdk-0.2.5/README.md +0 -82
- xenfra_sdk-0.2.5/src/xenfra_sdk/cli/main.py +0 -226
- xenfra_sdk-0.2.5/src/xenfra_sdk/detection.py +0 -396
- xenfra_sdk-0.2.5/src/xenfra_sdk/dockerizer.py +0 -195
- xenfra_sdk-0.2.5/src/xenfra_sdk/engine.py +0 -757
- xenfra_sdk-0.2.5/src/xenfra_sdk/manifest.py +0 -212
- xenfra_sdk-0.2.5/src/xenfra_sdk/recipes.py +0 -26
- xenfra_sdk-0.2.5/src/xenfra_sdk/templates/Caddyfile.j2 +0 -14
- xenfra_sdk-0.2.5/src/xenfra_sdk/templates/Dockerfile.j2 +0 -41
- xenfra_sdk-0.2.5/src/xenfra_sdk/templates/cloud-init.sh.j2 +0 -90
- xenfra_sdk-0.2.5/src/xenfra_sdk/templates/docker-compose-multi.yml.j2 +0 -29
- xenfra_sdk-0.2.5/src/xenfra_sdk/templates/docker-compose.yml.j2 +0 -30
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/cli/__init__.py +0 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/client_with_hooks.py +0 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/config.py +0 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/db/__init__.py +0 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/db/models.py +0 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/dependencies.py +0 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/patterns.json +0 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/resources/__init__.py +0 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/resources/base.py +0 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/resources/files.py +0 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/resources/projects.py +0 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/security.py +0 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/security_scanner.py +0 -0
- {xenfra_sdk-0.2.5 → xenfra_sdk-0.2.7}/src/xenfra_sdk/utils.py +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: xenfra-sdk
|
|
3
|
+
Version: 0.2.7
|
|
4
|
+
Summary: Xenfra SDK: Core engine and utilities for the Xenfra platform.
|
|
5
|
+
Author: xenfra-cloud
|
|
6
|
+
Author-email: xenfra-cloud <xenfracloud@gmail.com>
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
13
|
+
Classifier: Topic :: System :: Systems Administration
|
|
14
|
+
Requires-Dist: fabric>=3.2.2
|
|
15
|
+
Requires-Dist: python-digitalocean>=1.17.0
|
|
16
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
17
|
+
Requires-Dist: rich>=14.2.0
|
|
18
|
+
Requires-Dist: sqlmodel>=0.0.16
|
|
19
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
20
|
+
Requires-Dist: httpx>=0.27.0
|
|
21
|
+
Requires-Dist: jinja2>=3.1.3
|
|
22
|
+
Requires-Dist: python-jose[cryptography]>=3.3.0
|
|
23
|
+
Requires-Dist: passlib>=1.7.4
|
|
24
|
+
Requires-Dist: cryptography>=41.0.0
|
|
25
|
+
Requires-Python: >=3.13
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Xenfra Python SDK
|
|
29
|
+
|
|
30
|
+
The official, open-source Python SDK for interacting with the Xenfra API.
|
|
31
|
+
|
|
32
|
+
This SDK provides a simple and Pythonic interface for developers and AI Agents to programmatically manage infrastructure, deployments, and other platform resources.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install xenfra-sdk
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Basic Usage
|
|
41
|
+
|
|
42
|
+
Initialize the client with your API token (or ensure the `XENFRA_TOKEN` environment variable is set).
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import os
|
|
46
|
+
from xenfra_sdk import XenfraClient
|
|
47
|
+
from xenfra_sdk.exceptions import XenfraAPIError
|
|
48
|
+
|
|
49
|
+
client = XenfraClient(token=os.getenv("XENFRA_TOKEN"))
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
projects = client.projects.list()
|
|
53
|
+
for p in projects:
|
|
54
|
+
print(f"Found project: {p.name} (Status: {p.status})")
|
|
55
|
+
except XenfraAPIError as e:
|
|
56
|
+
print(f"API Error: {e.detail}")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage for Agentic Workflows
|
|
60
|
+
|
|
61
|
+
The Xenfra SDK is designed to be used as a "tool" by AI Agents (e.g., OpenAI Assistants). The Pydantic models are compatible with function-calling schemas, allowing an agent to easily call these methods.
|
|
62
|
+
|
|
63
|
+
Here is a conceptual example of how an agent might use the SDK to fulfill a user's request.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
# This is a conceptual representation of an agent's internal logic.
|
|
67
|
+
# The agent would be configured with functions that call these SDK methods.
|
|
68
|
+
|
|
69
|
+
def list_all_projects():
|
|
70
|
+
"""Lists all available projects in the Xenfra account."""
|
|
71
|
+
return client.projects.list()
|
|
72
|
+
|
|
73
|
+
def create_new_deployment(project_name: str, git_repo: str, branch: str = "main"):
|
|
74
|
+
"""
|
|
75
|
+
Creates a new deployment for a project.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
project_name: The name for the new deployment.
|
|
79
|
+
git_repo: The URL of the git repository to deploy.
|
|
80
|
+
branch: The branch to deploy (defaults to 'main').
|
|
81
|
+
"""
|
|
82
|
+
return client.deployments.create(
|
|
83
|
+
project_name=project_name,
|
|
84
|
+
git_repo=git_repo,
|
|
85
|
+
branch=branch,
|
|
86
|
+
framework="fastapi" # Framework detection would be part of a more complex agent
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# --- Agent Execution Flow ---
|
|
90
|
+
|
|
91
|
+
# User prompt: "Deploy my new app from github.com/user/my-app"
|
|
92
|
+
|
|
93
|
+
# 1. Agent decides which tool to use: `create_new_deployment`
|
|
94
|
+
# 2. Agent extracts parameters:
|
|
95
|
+
# - project_name = "my-app" (inferred)
|
|
96
|
+
# - git_repo = "https://github.com/user/my-app"
|
|
97
|
+
# 3. Agent calls the tool:
|
|
98
|
+
# create_new_deployment(
|
|
99
|
+
# project_name="my-app",
|
|
100
|
+
# git_repo="https://github.com/user/my-app"
|
|
101
|
+
# )
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Error Handling
|
|
105
|
+
|
|
106
|
+
The SDK uses custom exceptions for clear error handling. All API-related errors will raise a `XenfraAPIError`, which contains the `status_code` and a `detail` message from the API response.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from xenfra_sdk.exceptions import XenfraAPIError, AuthenticationError
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
# Make an API call
|
|
113
|
+
...
|
|
114
|
+
except AuthenticationError as e:
|
|
115
|
+
print("Authentication failed. Please check your token.")
|
|
116
|
+
except XenfraAPIError as e:
|
|
117
|
+
print(f"An API error occurred with status {e.status_code}: {e.detail}")
|
|
118
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Xenfra Python SDK
|
|
2
|
+
|
|
3
|
+
The official, open-source Python SDK for interacting with the Xenfra API.
|
|
4
|
+
|
|
5
|
+
This SDK provides a simple and Pythonic interface for developers and AI Agents to programmatically manage infrastructure, deployments, and other platform resources.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install xenfra-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Basic Usage
|
|
14
|
+
|
|
15
|
+
Initialize the client with your API token (or ensure the `XENFRA_TOKEN` environment variable is set).
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import os
|
|
19
|
+
from xenfra_sdk import XenfraClient
|
|
20
|
+
from xenfra_sdk.exceptions import XenfraAPIError
|
|
21
|
+
|
|
22
|
+
client = XenfraClient(token=os.getenv("XENFRA_TOKEN"))
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
projects = client.projects.list()
|
|
26
|
+
for p in projects:
|
|
27
|
+
print(f"Found project: {p.name} (Status: {p.status})")
|
|
28
|
+
except XenfraAPIError as e:
|
|
29
|
+
print(f"API Error: {e.detail}")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage for Agentic Workflows
|
|
33
|
+
|
|
34
|
+
The Xenfra SDK is designed to be used as a "tool" by AI Agents (e.g., OpenAI Assistants). The Pydantic models are compatible with function-calling schemas, allowing an agent to easily call these methods.
|
|
35
|
+
|
|
36
|
+
Here is a conceptual example of how an agent might use the SDK to fulfill a user's request.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# This is a conceptual representation of an agent's internal logic.
|
|
40
|
+
# The agent would be configured with functions that call these SDK methods.
|
|
41
|
+
|
|
42
|
+
def list_all_projects():
|
|
43
|
+
"""Lists all available projects in the Xenfra account."""
|
|
44
|
+
return client.projects.list()
|
|
45
|
+
|
|
46
|
+
def create_new_deployment(project_name: str, git_repo: str, branch: str = "main"):
|
|
47
|
+
"""
|
|
48
|
+
Creates a new deployment for a project.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
project_name: The name for the new deployment.
|
|
52
|
+
git_repo: The URL of the git repository to deploy.
|
|
53
|
+
branch: The branch to deploy (defaults to 'main').
|
|
54
|
+
"""
|
|
55
|
+
return client.deployments.create(
|
|
56
|
+
project_name=project_name,
|
|
57
|
+
git_repo=git_repo,
|
|
58
|
+
branch=branch,
|
|
59
|
+
framework="fastapi" # Framework detection would be part of a more complex agent
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# --- Agent Execution Flow ---
|
|
63
|
+
|
|
64
|
+
# User prompt: "Deploy my new app from github.com/user/my-app"
|
|
65
|
+
|
|
66
|
+
# 1. Agent decides which tool to use: `create_new_deployment`
|
|
67
|
+
# 2. Agent extracts parameters:
|
|
68
|
+
# - project_name = "my-app" (inferred)
|
|
69
|
+
# - git_repo = "https://github.com/user/my-app"
|
|
70
|
+
# 3. Agent calls the tool:
|
|
71
|
+
# create_new_deployment(
|
|
72
|
+
# project_name="my-app",
|
|
73
|
+
# git_repo="https://github.com/user/my-app"
|
|
74
|
+
# )
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Error Handling
|
|
78
|
+
|
|
79
|
+
The SDK uses custom exceptions for clear error handling. All API-related errors will raise a `XenfraAPIError`, which contains the `status_code` and a `detail` message from the API response.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from xenfra_sdk.exceptions import XenfraAPIError, AuthenticationError
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# Make an API call
|
|
86
|
+
...
|
|
87
|
+
except AuthenticationError as e:
|
|
88
|
+
print("Authentication failed. Please check your token.")
|
|
89
|
+
except XenfraAPIError as e:
|
|
90
|
+
print(f"An API error occurred with status {e.status_code}: {e.detail}")
|
|
91
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "xenfra-sdk"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.7"
|
|
4
4
|
description = "Xenfra SDK: Core engine and utilities for the Xenfra platform."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -30,24 +30,7 @@ dependencies = [
|
|
|
30
30
|
"passlib>=1.7.4",
|
|
31
31
|
"cryptography>=41.0.0",
|
|
32
32
|
]
|
|
33
|
-
requires-python = ">=3.
|
|
34
|
-
|
|
35
|
-
[project.urls]
|
|
36
|
-
Homepage = "https://github.com/xenfra-cloud/xenfra-sdk"
|
|
37
|
-
Issues = "https://github.com/xenfra-cloud/xenfra-sdk/issues"
|
|
38
|
-
|
|
39
|
-
[project.optional-dependencies]
|
|
40
|
-
dev = [
|
|
41
|
-
"pytest>=8.0.0",
|
|
42
|
-
"pytest-mock>=3.12.0",
|
|
43
|
-
"pytest-cov>=4.0.0",
|
|
44
|
-
"pytest-asyncio>=0.21.0"
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
[tool.pytest.ini_options]
|
|
48
|
-
asyncio_mode = "auto"
|
|
49
|
-
testpaths = ["tests"]
|
|
50
|
-
python_files = ["test_*.py"]
|
|
33
|
+
requires-python = ">=3.13"
|
|
51
34
|
|
|
52
35
|
[build-system]
|
|
53
36
|
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
# This file makes src/xenfra_sdk a Python package.
|
|
2
2
|
|
|
3
3
|
from .client import XenfraClient
|
|
4
|
-
from .
|
|
4
|
+
from .engine import InfraEngine
|
|
5
|
+
from .events import BuildEvent, DeploymentPhase, EventEmitter, EventStatus
|
|
6
|
+
from .exceptions import AuthenticationError, DeploymentError, XenfraAPIError, XenfraError
|
|
7
|
+
from .protocol import ProtocolRegistry
|
|
5
8
|
from .models import (
|
|
9
|
+
BalanceRead,
|
|
6
10
|
CodebaseAnalysisResponse,
|
|
7
11
|
DiagnosisResponse,
|
|
12
|
+
DropletCostRead,
|
|
8
13
|
PatchObject,
|
|
9
14
|
ProjectRead,
|
|
10
15
|
)
|
|
11
16
|
|
|
12
|
-
# Microservices support
|
|
17
|
+
# Microservices & Config support
|
|
13
18
|
from .manifest import (
|
|
19
|
+
XenfraConfig,
|
|
20
|
+
load_xenfra_config,
|
|
14
21
|
ServiceDefinition,
|
|
15
22
|
load_services_from_xenfra_yaml,
|
|
16
23
|
is_microservices_project,
|
|
@@ -37,6 +44,22 @@ from .security_scanner import (
|
|
|
37
44
|
Severity,
|
|
38
45
|
)
|
|
39
46
|
|
|
47
|
+
# Railpack Integration
|
|
48
|
+
from .railpack_detector import (
|
|
49
|
+
RailpackDetector,
|
|
50
|
+
RailpackDetectionResult,
|
|
51
|
+
EnvVariable,
|
|
52
|
+
get_railpack_detector,
|
|
53
|
+
)
|
|
54
|
+
from .railpack_manager import (
|
|
55
|
+
RailpackManager,
|
|
56
|
+
get_railpack_manager,
|
|
57
|
+
)
|
|
58
|
+
from .railpack_adapter import (
|
|
59
|
+
RailpackAdapter,
|
|
60
|
+
RailpackPlan,
|
|
61
|
+
)
|
|
62
|
+
|
|
40
63
|
__all__ = [
|
|
41
64
|
"XenfraClient",
|
|
42
65
|
"XenfraError",
|
|
@@ -46,6 +69,11 @@ __all__ = [
|
|
|
46
69
|
"CodebaseAnalysisResponse",
|
|
47
70
|
"PatchObject",
|
|
48
71
|
"ProjectRead",
|
|
72
|
+
"BalanceRead",
|
|
73
|
+
"DropletCostRead",
|
|
74
|
+
# Config
|
|
75
|
+
"XenfraConfig",
|
|
76
|
+
"load_xenfra_config",
|
|
49
77
|
# Microservices
|
|
50
78
|
"ServiceDefinition",
|
|
51
79
|
"load_services_from_xenfra_yaml",
|
|
@@ -58,4 +86,20 @@ __all__ = [
|
|
|
58
86
|
"detect_pyproject_services",
|
|
59
87
|
"ServiceOrchestrator",
|
|
60
88
|
"get_orchestrator_for_project",
|
|
89
|
+
"InfraEngine",
|
|
90
|
+
"DeploymentError",
|
|
91
|
+
"ProtocolRegistry",
|
|
92
|
+
"EventEmitter",
|
|
93
|
+
"BuildEvent",
|
|
94
|
+
"DeploymentPhase",
|
|
95
|
+
"EventStatus",
|
|
96
|
+
# Railpack Integration
|
|
97
|
+
"RailpackDetector",
|
|
98
|
+
"RailpackDetectionResult",
|
|
99
|
+
"EnvVariable",
|
|
100
|
+
"get_railpack_detector",
|
|
101
|
+
"RailpackManager",
|
|
102
|
+
"get_railpack_manager",
|
|
103
|
+
"RailpackAdapter",
|
|
104
|
+
"RailpackPlan",
|
|
61
105
|
]
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from xenfra_sdk.blueprints.schema import (
|
|
7
|
+
DeploymentBlueprintManifest, DockerfileModel, ComposeModel, ServiceDetail,
|
|
8
|
+
DeployModel, DeployResourcesModel, ResourceLimitsModel, ResourceReservationsModel
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from xenfra_sdk.constants import DEFAULT_PORT_RANGE_START
|
|
12
|
+
|
|
13
|
+
class BaseBlueprint(ABC):
|
|
14
|
+
"""
|
|
15
|
+
Abstract base class for all Xenfra Blueprints (Build Packs).
|
|
16
|
+
|
|
17
|
+
A Blueprint is responsible for:
|
|
18
|
+
1. Analyzing code to detect requirements.
|
|
19
|
+
2. Building a valid Pydantic manifest.
|
|
20
|
+
3. Rendering that manifest to files (Dockerfile, compose, env).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, context: dict):
|
|
24
|
+
self.context = context
|
|
25
|
+
self.framework = context.get("framework")
|
|
26
|
+
self.port = context.get("port") or DEFAULT_PORT_RANGE_START
|
|
27
|
+
self.file_manifest = context.get("file_manifest", [])
|
|
28
|
+
self.resource_limits = context.get("resource_limits", {})
|
|
29
|
+
|
|
30
|
+
def _generate_deploy_model(self) -> DeployModel:
|
|
31
|
+
"""Centralized helper for resource governance."""
|
|
32
|
+
return DeployModel(
|
|
33
|
+
resources=DeployResourcesModel(
|
|
34
|
+
limits=ResourceLimitsModel(
|
|
35
|
+
memory=self.resource_limits.get("memory", "512m"),
|
|
36
|
+
cpus=self.resource_limits.get("cpus", "0.5"),
|
|
37
|
+
),
|
|
38
|
+
reservations=ResourceReservationsModel(
|
|
39
|
+
memory=self.resource_limits.get("memory_reserved", "128m"),
|
|
40
|
+
cpus=self.resource_limits.get("cpus_reserved", "0.25"),
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def generate_manifest(self) -> DeploymentBlueprintManifest:
|
|
47
|
+
"""Analyze context and files to build the Pydantic manifest."""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def render(self) -> Dict[str, str]:
|
|
51
|
+
"""Renders the generated manifest into final deployment strings."""
|
|
52
|
+
manifest = self.generate_manifest()
|
|
53
|
+
result = {}
|
|
54
|
+
|
|
55
|
+
# 1. Render Dockerfile
|
|
56
|
+
result["Dockerfile"] = self._render_dockerfile(manifest.dockerfile)
|
|
57
|
+
|
|
58
|
+
# 2. Render docker-compose.yml
|
|
59
|
+
result["docker-compose.yml"] = self._render_compose(manifest.compose)
|
|
60
|
+
|
|
61
|
+
# 3. Render .env
|
|
62
|
+
if manifest.env_file:
|
|
63
|
+
result[".env"] = "\n".join([f'{k}="{v}"' for k, v in manifest.env_file.items()])
|
|
64
|
+
|
|
65
|
+
# 4. Render Caddyfile (if provided)
|
|
66
|
+
if manifest.caddyfile:
|
|
67
|
+
result["Caddyfile"] = manifest.caddyfile
|
|
68
|
+
|
|
69
|
+
# 5. Render Railpack Plan (if provided)
|
|
70
|
+
if hasattr(manifest, "railpack_plan") and manifest.railpack_plan:
|
|
71
|
+
result["railpack-plan.json"] = manifest.railpack_plan
|
|
72
|
+
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
def _render_dockerfile(self, model: DockerfileModel) -> str:
|
|
76
|
+
"""Converts DockerfileModel to a string."""
|
|
77
|
+
lines = [f"FROM {model.base_image}"]
|
|
78
|
+
|
|
79
|
+
# Inject ARG instructions (Build-time variables)
|
|
80
|
+
# Must come after FROM (usually) or before depending on if they affect FROM
|
|
81
|
+
# Standard practice: FROM -> ARG -> WORKDIR -> COPY -> RUN
|
|
82
|
+
if model.args:
|
|
83
|
+
for arg in model.args:
|
|
84
|
+
lines.append(f"ARG {arg}")
|
|
85
|
+
|
|
86
|
+
# APT packages
|
|
87
|
+
if model.system_packages:
|
|
88
|
+
packages = " ".join(model.system_packages)
|
|
89
|
+
lines.append("RUN apt-get update && apt-get install -y " + packages + " && rm -rf /var/lib/apt/lists/*")
|
|
90
|
+
|
|
91
|
+
lines.append(f"WORKDIR {model.workdir}")
|
|
92
|
+
|
|
93
|
+
# Env vars (Non-secret defaults only)
|
|
94
|
+
# We rely on .env + docker-compose for actual deployment values.
|
|
95
|
+
# ARGs are already available during build.
|
|
96
|
+
arg_keys = set(model.args)
|
|
97
|
+
for k, v in model.env_vars.items():
|
|
98
|
+
if k in arg_keys:
|
|
99
|
+
# Skip ENV if it's already an ARG, to keep secrets out of Dockerfile and fix syntax errors
|
|
100
|
+
continue
|
|
101
|
+
# Quote values for safety
|
|
102
|
+
lines.append(f'ENV {k}="{v}"')
|
|
103
|
+
|
|
104
|
+
# Copy commands
|
|
105
|
+
if model.copy_dirs:
|
|
106
|
+
for d in model.copy_dirs:
|
|
107
|
+
lines.append(f"COPY {d} .")
|
|
108
|
+
else:
|
|
109
|
+
lines.append("COPY . .")
|
|
110
|
+
|
|
111
|
+
# Custom RUN commands
|
|
112
|
+
for cmd in model.run_commands:
|
|
113
|
+
lines.append(f"RUN {cmd}")
|
|
114
|
+
|
|
115
|
+
if model.expose_port:
|
|
116
|
+
lines.append(f"EXPOSE {model.expose_port}")
|
|
117
|
+
|
|
118
|
+
if model.entrypoint:
|
|
119
|
+
if isinstance(model.entrypoint, list):
|
|
120
|
+
ep = ", ".join([f'"{x}"' for x in model.entrypoint])
|
|
121
|
+
lines.append(f"ENTRYPOINT [{ep}]")
|
|
122
|
+
else:
|
|
123
|
+
lines.append(f"ENTRYPOINT {model.entrypoint}")
|
|
124
|
+
|
|
125
|
+
if model.command:
|
|
126
|
+
if isinstance(model.command, list):
|
|
127
|
+
cmd = ", ".join([f'"{x}"' for x in model.command])
|
|
128
|
+
lines.append(f"CMD [{cmd}]")
|
|
129
|
+
else:
|
|
130
|
+
lines.append(f"CMD {model.command}")
|
|
131
|
+
|
|
132
|
+
return "\n".join(lines)
|
|
133
|
+
|
|
134
|
+
def _render_compose(self, model: ComposeModel) -> str:
|
|
135
|
+
"""Converts ComposeModel to a YAML string."""
|
|
136
|
+
# We use a custom dumper to ensure services come first etc if needed
|
|
137
|
+
# But standard yaml works for start
|
|
138
|
+
data = model.model_dump(by_alias=True, exclude_none=True)
|
|
139
|
+
return yaml.dump(data, sort_keys=False, default_flow_style=False)
|
|
140
|
+
|
|
141
|
+
def has_file(self, filename: str) -> bool:
|
|
142
|
+
"""Helper to check if a file exists in the manifest."""
|
|
143
|
+
return any(f.get("path") == filename for f in self.file_manifest)
|
|
144
|
+
|
|
145
|
+
def get_file_content(self, filename: str) -> Optional[str]:
|
|
146
|
+
"""Helper to get file content from manifest."""
|
|
147
|
+
for f in self.file_manifest:
|
|
148
|
+
if f.get("path") == filename:
|
|
149
|
+
return f.get("content")
|
|
150
|
+
return None
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from typing import Type, Dict, List
|
|
2
|
+
from xenfra_sdk.blueprints.base import BaseBlueprint
|
|
3
|
+
from xenfra_sdk.blueprints.python import PythonBlueprint
|
|
4
|
+
from xenfra_sdk.blueprints.node import NodeBlueprint
|
|
5
|
+
from xenfra_sdk.blueprints.railpack import RailpackBlueprint
|
|
6
|
+
from xenfra_sdk.governance import get_resource_limits, ResourceLimits
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolve_blueprint_class(context: dict) -> Type[BaseBlueprint]:
|
|
10
|
+
"""
|
|
11
|
+
The 'Check-In Desk' for manifest generation.
|
|
12
|
+
Decides between Sovereign (Lean) and Specialist (Railpack) paths.
|
|
13
|
+
|
|
14
|
+
Priority:
|
|
15
|
+
1. Explicit framework selection (user-specified)
|
|
16
|
+
2. File-based detection fallback
|
|
17
|
+
"""
|
|
18
|
+
framework = str(context.get("framework", "")).lower().strip()
|
|
19
|
+
file_manifest = context.get("file_manifest", [])
|
|
20
|
+
file_names = {f.get("path") for f in file_manifest}
|
|
21
|
+
|
|
22
|
+
# 1. EXPLICIT FRAMEWORK SELECTION (takes priority)
|
|
23
|
+
# If user explicitly selected a framework, respect that choice
|
|
24
|
+
SOVEREIGN_PYTHON = ("python", "fastapi", "flask", "django")
|
|
25
|
+
|
|
26
|
+
# Modern Node.js frameworks that need railpack (auto-detect + build)
|
|
27
|
+
# These have complex build pipelines that railpack/nixpacks handles better
|
|
28
|
+
RAILPACK_NODE = ("next", "nextjs", "nuxt", "vite", "nestjs", "nest")
|
|
29
|
+
|
|
30
|
+
# Simple Node.js that can use our lean NodeBlueprint
|
|
31
|
+
# Only for Express-style apps without complex build steps
|
|
32
|
+
SOVEREIGN_NODE = ("express",)
|
|
33
|
+
|
|
34
|
+
if framework in SOVEREIGN_PYTHON:
|
|
35
|
+
# Check for complex package managers (uv, poetry, pipenv) -> Use Railpack
|
|
36
|
+
if any(f in file_names for f in ("uv.lock", "poetry.lock", "Pipfile.lock")):
|
|
37
|
+
return RailpackBlueprint
|
|
38
|
+
return PythonBlueprint
|
|
39
|
+
|
|
40
|
+
# Route modern frameworks to RailpackBlueprint
|
|
41
|
+
if framework in RAILPACK_NODE:
|
|
42
|
+
return RailpackBlueprint
|
|
43
|
+
|
|
44
|
+
# Generic "node" or "nodejs" → check file_manifest for framework hints
|
|
45
|
+
if framework in ("node", "nodejs"):
|
|
46
|
+
# Check for Next.js, Nuxt, etc. config files
|
|
47
|
+
nextjs_configs = {"next.config.js", "next.config.ts", "next.config.mjs"}
|
|
48
|
+
nuxt_configs = {"nuxt.config.js", "nuxt.config.ts"}
|
|
49
|
+
vite_configs = {"vite.config.js", "vite.config.ts"}
|
|
50
|
+
|
|
51
|
+
if nextjs_configs & file_names or nuxt_configs & file_names or vite_configs & file_names:
|
|
52
|
+
return RailpackBlueprint
|
|
53
|
+
|
|
54
|
+
# Default to RailpackBlueprint for generic Node.js (safer, handles more cases)
|
|
55
|
+
return RailpackBlueprint
|
|
56
|
+
|
|
57
|
+
if framework in SOVEREIGN_NODE:
|
|
58
|
+
return NodeBlueprint
|
|
59
|
+
|
|
60
|
+
# 2. FILE-BASED DETECTION FALLBACK (only if no explicit framework)
|
|
61
|
+
# Check for Python Sovereign Path (Strict Golden Path)
|
|
62
|
+
if "requirements.txt" in file_names and "pyproject.toml" not in file_names:
|
|
63
|
+
return PythonBlueprint
|
|
64
|
+
|
|
65
|
+
# Check for Node Sovereign Path (Strict Golden Path)
|
|
66
|
+
# Only for simple Express apps with package-lock.json
|
|
67
|
+
if "package-lock.json" in file_names and "package.json" in file_names:
|
|
68
|
+
# If has modern framework config files, use Railpack
|
|
69
|
+
nextjs_configs = {"next.config.js", "next.config.ts", "next.config.mjs"}
|
|
70
|
+
if nextjs_configs & file_names:
|
|
71
|
+
return RailpackBlueprint
|
|
72
|
+
# Otherwise, still prefer RailpackBlueprint for safety
|
|
73
|
+
return RailpackBlueprint
|
|
74
|
+
|
|
75
|
+
# 3. Specialist Path (The Delegate)
|
|
76
|
+
# Handles EVERYTHING else: Rust, Go, Bun, UV, Poetry, etc.
|
|
77
|
+
return RailpackBlueprint
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def render_blueprint(context: dict) -> Dict[str, str]:
|
|
81
|
+
"""
|
|
82
|
+
Main entry point for manifest generation.
|
|
83
|
+
Resolves the blueprint and renders it to strings.
|
|
84
|
+
|
|
85
|
+
ZEN GAP FIX: Automatically injects tier-based resource limits.
|
|
86
|
+
"""
|
|
87
|
+
# Inject resource limits if tier is provided but resource_limits isn't
|
|
88
|
+
if "resource_limits" not in context and "tier" in context:
|
|
89
|
+
limits = get_resource_limits(context["tier"])
|
|
90
|
+
context["resource_limits"] = {
|
|
91
|
+
"memory": limits.memory,
|
|
92
|
+
"cpus": limits.cpus,
|
|
93
|
+
"memory_reserved": limits.memory_reserved,
|
|
94
|
+
"cpus_reserved": limits.cpus_reserved,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
blueprint_class = resolve_blueprint_class(context)
|
|
98
|
+
blueprint = blueprint_class(context)
|
|
99
|
+
return blueprint.render()
|