xenfra-sdk 0.1.3__tar.gz → 0.1.5__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.1.3 → xenfra_sdk-0.1.5}/PKG-INFO +1 -1
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/pyproject.toml +1 -1
- xenfra_sdk-0.1.5/src/xenfra_sdk/resources/deployments.py +185 -0
- xenfra_sdk-0.1.3/src/xenfra_sdk/resources/deployments.py +0 -89
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/README.md +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/__init__.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/cli/__init__.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/cli/main.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/client.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/client_with_hooks.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/config.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/db/__init__.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/db/models.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/db/session.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/dependencies.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/dockerizer.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/engine.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/exceptions.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/mcp_client.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/models.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/patterns.json +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/privacy.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/recipes.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/resources/__init__.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/resources/base.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/resources/intelligence.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/resources/projects.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/security.py +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/templates/Dockerfile.j2 +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/templates/cloud-init.sh.j2 +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/templates/docker-compose.yml.j2 +0 -0
- {xenfra_sdk-0.1.3 → xenfra_sdk-0.1.5}/src/xenfra_sdk/utils.py +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Iterator
|
|
4
|
+
|
|
5
|
+
# Import Deployment model when it's defined in models.py
|
|
6
|
+
# from ..models import Deployment
|
|
7
|
+
from ..exceptions import XenfraAPIError, XenfraError # Add XenfraError
|
|
8
|
+
from ..utils import safe_get_json_field, safe_json_parse
|
|
9
|
+
from .base import BaseManager
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DeploymentsManager(BaseManager):
|
|
15
|
+
def create(self, project_name: str, git_repo: str, branch: str, framework: str, region: str = None, size_slug: str = None) -> dict:
|
|
16
|
+
"""Creates a new deployment."""
|
|
17
|
+
try:
|
|
18
|
+
payload = {
|
|
19
|
+
"project_name": project_name,
|
|
20
|
+
"git_repo": git_repo,
|
|
21
|
+
"branch": branch,
|
|
22
|
+
"framework": framework,
|
|
23
|
+
}
|
|
24
|
+
if region:
|
|
25
|
+
payload["region"] = region
|
|
26
|
+
if size_slug:
|
|
27
|
+
payload["size_slug"] = size_slug
|
|
28
|
+
|
|
29
|
+
response = self._client._request("POST", "/deployments", json=payload)
|
|
30
|
+
# Safe JSON parsing
|
|
31
|
+
return safe_json_parse(response)
|
|
32
|
+
except XenfraAPIError:
|
|
33
|
+
raise
|
|
34
|
+
except Exception as e:
|
|
35
|
+
raise XenfraError(f"Failed to create deployment: {e}")
|
|
36
|
+
|
|
37
|
+
def get_status(self, deployment_id: str) -> dict:
|
|
38
|
+
"""Get status for a specific deployment.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
deployment_id: The unique identifier for the deployment.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
dict: Deployment status information including state, progress, etc.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
XenfraAPIError: If the API returns an error (e.g., 404 not found).
|
|
48
|
+
XenfraError: If there's a network or parsing error.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
response = self._client._request("GET", f"/deployments/{deployment_id}/status")
|
|
52
|
+
logger.debug(
|
|
53
|
+
f"DeploymentsManager.get_status({deployment_id}) response: {response.status_code}"
|
|
54
|
+
)
|
|
55
|
+
# Safe JSON parsing - _request() already handles status codes
|
|
56
|
+
return safe_json_parse(response)
|
|
57
|
+
except XenfraAPIError:
|
|
58
|
+
raise # Re-raise API errors
|
|
59
|
+
except Exception as e:
|
|
60
|
+
raise XenfraError(f"Failed to get status for deployment {deployment_id}: {e}")
|
|
61
|
+
|
|
62
|
+
def get_logs(self, deployment_id: str) -> str:
|
|
63
|
+
"""Get logs for a specific deployment.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
deployment_id: The unique identifier for the deployment.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
str: The deployment logs as plain text.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
XenfraAPIError: If the API returns an error (e.g., 404 not found).
|
|
73
|
+
XenfraError: If there's a network or parsing error.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
response = self._client._request("GET", f"/deployments/{deployment_id}/logs")
|
|
77
|
+
logger.debug(
|
|
78
|
+
f"DeploymentsManager.get_logs({deployment_id}) response: {response.status_code}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Safe JSON parsing with structure validation - _request() already handles status codes
|
|
82
|
+
data = safe_json_parse(response)
|
|
83
|
+
if not isinstance(data, dict):
|
|
84
|
+
raise XenfraError(f"Expected dictionary response, got {type(data).__name__}")
|
|
85
|
+
|
|
86
|
+
logs = safe_get_json_field(data, "logs", "")
|
|
87
|
+
|
|
88
|
+
if not logs:
|
|
89
|
+
logger.warning(f"No logs found for deployment {deployment_id}")
|
|
90
|
+
|
|
91
|
+
return logs
|
|
92
|
+
|
|
93
|
+
except XenfraAPIError:
|
|
94
|
+
raise # Re-raise API errors
|
|
95
|
+
except Exception as e:
|
|
96
|
+
raise XenfraError(f"Failed to get logs for deployment {deployment_id}: {e}")
|
|
97
|
+
|
|
98
|
+
def create_stream(self, project_name: str, git_repo: str, branch: str, framework: str, region: str = None, size_slug: str = None) -> Iterator[dict]:
|
|
99
|
+
"""
|
|
100
|
+
Creates a new deployment with real-time SSE log streaming.
|
|
101
|
+
|
|
102
|
+
Yields SSE events as dictionaries with 'event' and 'data' keys.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
project_name: Name of the project
|
|
106
|
+
git_repo: Git repository URL
|
|
107
|
+
branch: Git branch to deploy
|
|
108
|
+
framework: Framework type (fastapi, flask, django)
|
|
109
|
+
region: DigitalOcean region (optional)
|
|
110
|
+
size_slug: DigitalOcean droplet size (optional)
|
|
111
|
+
|
|
112
|
+
Yields:
|
|
113
|
+
dict: SSE events with 'event' and 'data' fields
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
for event in client.deployments.create_stream(...):
|
|
117
|
+
if event['event'] == 'log':
|
|
118
|
+
print(event['data'])
|
|
119
|
+
elif event['event'] == 'deployment_complete':
|
|
120
|
+
print("Done!")
|
|
121
|
+
"""
|
|
122
|
+
payload = {
|
|
123
|
+
"project_name": project_name,
|
|
124
|
+
"git_repo": git_repo,
|
|
125
|
+
"branch": branch,
|
|
126
|
+
"framework": framework,
|
|
127
|
+
}
|
|
128
|
+
if region:
|
|
129
|
+
payload["region"] = region
|
|
130
|
+
if size_slug:
|
|
131
|
+
payload["size_slug"] = size_slug
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
# Use httpx to stream the SSE response
|
|
135
|
+
import httpx
|
|
136
|
+
|
|
137
|
+
headers = {
|
|
138
|
+
"Authorization": f"Bearer {self._client._token}",
|
|
139
|
+
"Accept": "text/event-stream",
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
url = f"{self._client.api_url}/deployments/stream"
|
|
144
|
+
|
|
145
|
+
with httpx.stream(
|
|
146
|
+
"POST",
|
|
147
|
+
url,
|
|
148
|
+
json=payload,
|
|
149
|
+
headers=headers,
|
|
150
|
+
timeout=600.0, # 10 minute timeout for deployments
|
|
151
|
+
) as response:
|
|
152
|
+
if response.status_code not in [200, 201, 202]:
|
|
153
|
+
error_text = response.text
|
|
154
|
+
raise XenfraAPIError(
|
|
155
|
+
status_code=response.status_code,
|
|
156
|
+
detail=f"Deployment failed: {error_text}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Parse SSE events
|
|
160
|
+
for line in response.iter_lines():
|
|
161
|
+
line = line.strip()
|
|
162
|
+
if not line:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
# SSE format: "event: eventname" or "data: eventdata"
|
|
166
|
+
if line.startswith("event:"):
|
|
167
|
+
current_event = line[6:].strip()
|
|
168
|
+
elif line.startswith("data:"):
|
|
169
|
+
data = line[5:].strip()
|
|
170
|
+
try:
|
|
171
|
+
# Try to parse as JSON
|
|
172
|
+
data_parsed = json.loads(data)
|
|
173
|
+
yield {"event": current_event if 'current_event' in locals() else "message", "data": data_parsed}
|
|
174
|
+
except json.JSONDecodeError:
|
|
175
|
+
# If not JSON, yield as plain text
|
|
176
|
+
yield {"event": current_event if 'current_event' in locals() else "message", "data": data}
|
|
177
|
+
|
|
178
|
+
# Reset current_event
|
|
179
|
+
if 'current_event' in locals():
|
|
180
|
+
del current_event
|
|
181
|
+
|
|
182
|
+
except httpx.HTTPError as e:
|
|
183
|
+
raise XenfraError(f"HTTP error during streaming deployment: {e}")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
raise XenfraError(f"Failed to create streaming deployment: {e}")
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
# Import Deployment model when it's defined in models.py
|
|
4
|
-
# from ..models import Deployment
|
|
5
|
-
from ..exceptions import XenfraAPIError, XenfraError # Add XenfraError
|
|
6
|
-
from ..utils import safe_get_json_field, safe_json_parse
|
|
7
|
-
from .base import BaseManager
|
|
8
|
-
|
|
9
|
-
logger = logging.getLogger(__name__)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class DeploymentsManager(BaseManager):
|
|
13
|
-
def create(self, project_name: str, git_repo: str, branch: str, framework: str) -> dict:
|
|
14
|
-
"""Creates a new deployment."""
|
|
15
|
-
try:
|
|
16
|
-
payload = {
|
|
17
|
-
"project_name": project_name,
|
|
18
|
-
"git_repo": git_repo,
|
|
19
|
-
"branch": branch,
|
|
20
|
-
"framework": framework,
|
|
21
|
-
}
|
|
22
|
-
response = self._client._request("POST", "/deployments", json=payload)
|
|
23
|
-
# Safe JSON parsing
|
|
24
|
-
return safe_json_parse(response)
|
|
25
|
-
except XenfraAPIError:
|
|
26
|
-
raise
|
|
27
|
-
except Exception as e:
|
|
28
|
-
raise XenfraError(f"Failed to create deployment: {e}")
|
|
29
|
-
|
|
30
|
-
def get_status(self, deployment_id: str) -> dict:
|
|
31
|
-
"""Get status for a specific deployment.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
deployment_id: The unique identifier for the deployment.
|
|
35
|
-
|
|
36
|
-
Returns:
|
|
37
|
-
dict: Deployment status information including state, progress, etc.
|
|
38
|
-
|
|
39
|
-
Raises:
|
|
40
|
-
XenfraAPIError: If the API returns an error (e.g., 404 not found).
|
|
41
|
-
XenfraError: If there's a network or parsing error.
|
|
42
|
-
"""
|
|
43
|
-
try:
|
|
44
|
-
response = self._client._request("GET", f"/deployments/{deployment_id}/status")
|
|
45
|
-
logger.debug(
|
|
46
|
-
f"DeploymentsManager.get_status({deployment_id}) response: {response.status_code}"
|
|
47
|
-
)
|
|
48
|
-
# Safe JSON parsing - _request() already handles status codes
|
|
49
|
-
return safe_json_parse(response)
|
|
50
|
-
except XenfraAPIError:
|
|
51
|
-
raise # Re-raise API errors
|
|
52
|
-
except Exception as e:
|
|
53
|
-
raise XenfraError(f"Failed to get status for deployment {deployment_id}: {e}")
|
|
54
|
-
|
|
55
|
-
def get_logs(self, deployment_id: str) -> str:
|
|
56
|
-
"""Get logs for a specific deployment.
|
|
57
|
-
|
|
58
|
-
Args:
|
|
59
|
-
deployment_id: The unique identifier for the deployment.
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
str: The deployment logs as plain text.
|
|
63
|
-
|
|
64
|
-
Raises:
|
|
65
|
-
XenfraAPIError: If the API returns an error (e.g., 404 not found).
|
|
66
|
-
XenfraError: If there's a network or parsing error.
|
|
67
|
-
"""
|
|
68
|
-
try:
|
|
69
|
-
response = self._client._request("GET", f"/deployments/{deployment_id}/logs")
|
|
70
|
-
logger.debug(
|
|
71
|
-
f"DeploymentsManager.get_logs({deployment_id}) response: {response.status_code}"
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
# Safe JSON parsing with structure validation - _request() already handles status codes
|
|
75
|
-
data = safe_json_parse(response)
|
|
76
|
-
if not isinstance(data, dict):
|
|
77
|
-
raise XenfraError(f"Expected dictionary response, got {type(data).__name__}")
|
|
78
|
-
|
|
79
|
-
logs = safe_get_json_field(data, "logs", "")
|
|
80
|
-
|
|
81
|
-
if not logs:
|
|
82
|
-
logger.warning(f"No logs found for deployment {deployment_id}")
|
|
83
|
-
|
|
84
|
-
return logs
|
|
85
|
-
|
|
86
|
-
except XenfraAPIError:
|
|
87
|
-
raise # Re-raise API errors
|
|
88
|
-
except Exception as e:
|
|
89
|
-
raise XenfraError(f"Failed to get logs for deployment {deployment_id}: {e}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|