xenfra-sdk 0.1.1__py3-none-any.whl → 0.1.2__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/client.py +4 -3
- xenfra_sdk/client_with_hooks.py +4 -2
- xenfra_sdk/db/models.py +1 -0
- xenfra_sdk/dependencies.py +1 -0
- xenfra_sdk/models.py +17 -5
- xenfra_sdk/privacy.py +8 -6
- xenfra_sdk/resources/deployments.py +7 -3
- xenfra_sdk/resources/intelligence.py +5 -15
- xenfra_sdk/resources/projects.py +5 -10
- xenfra_sdk/utils.py +2 -4
- {xenfra_sdk-0.1.1.dist-info → xenfra_sdk-0.1.2.dist-info}/METADATA +1 -1
- {xenfra_sdk-0.1.1.dist-info → xenfra_sdk-0.1.2.dist-info}/RECORD +13 -13
- {xenfra_sdk-0.1.1.dist-info → xenfra_sdk-0.1.2.dist-info}/WHEEL +0 -0
xenfra_sdk/client.py
CHANGED
|
@@ -6,7 +6,6 @@ from .exceptions import AuthenticationError, XenfraAPIError, XenfraError
|
|
|
6
6
|
from .resources.deployments import DeploymentsManager
|
|
7
7
|
from .resources.intelligence import IntelligenceManager
|
|
8
8
|
from .resources.projects import ProjectsManager
|
|
9
|
-
from .utils import safe_json_parse
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
class XenfraClient:
|
|
@@ -53,7 +52,9 @@ class XenfraClient:
|
|
|
53
52
|
if "application/json" in content_type:
|
|
54
53
|
try:
|
|
55
54
|
error_data = e.response.json()
|
|
56
|
-
detail = error_data.get(
|
|
55
|
+
detail = error_data.get(
|
|
56
|
+
"detail", e.response.text[:500] if e.response.text else "Unknown error"
|
|
57
|
+
)
|
|
57
58
|
except (ValueError, TypeError):
|
|
58
59
|
detail = e.response.text[:500] if e.response.text else "Unknown error"
|
|
59
60
|
else:
|
|
@@ -82,5 +83,5 @@ class XenfraClient:
|
|
|
82
83
|
|
|
83
84
|
def __del__(self):
|
|
84
85
|
"""Destructor - cleanup if not already closed."""
|
|
85
|
-
if hasattr(self,
|
|
86
|
+
if hasattr(self, "_closed") and not self._closed:
|
|
86
87
|
self.close()
|
xenfra_sdk/client_with_hooks.py
CHANGED
|
@@ -12,7 +12,6 @@ from .exceptions import AuthenticationError, XenfraAPIError, XenfraError
|
|
|
12
12
|
from .resources.deployments import DeploymentsManager
|
|
13
13
|
from .resources.intelligence import IntelligenceManager
|
|
14
14
|
from .resources.projects import ProjectsManager
|
|
15
|
-
from .utils import safe_json_parse
|
|
16
15
|
|
|
17
16
|
logger = logging.getLogger(__name__)
|
|
18
17
|
|
|
@@ -187,7 +186,10 @@ class XenfraClient:
|
|
|
187
186
|
if "application/json" in content_type:
|
|
188
187
|
try:
|
|
189
188
|
error_data = e.response.json()
|
|
190
|
-
detail = error_data.get(
|
|
189
|
+
detail = error_data.get(
|
|
190
|
+
"detail",
|
|
191
|
+
e.response.text[:500] if e.response.text else "Unknown error",
|
|
192
|
+
)
|
|
191
193
|
except (ValueError, TypeError):
|
|
192
194
|
detail = e.response.text[:500] if e.response.text else "Unknown error"
|
|
193
195
|
else:
|
xenfra_sdk/db/models.py
CHANGED
|
@@ -15,6 +15,7 @@ class Project(SQLModel, table=True):
|
|
|
15
15
|
In a microservices architecture, we store the ID but don't enforce
|
|
16
16
|
a foreign key constraint across service boundaries.
|
|
17
17
|
"""
|
|
18
|
+
|
|
18
19
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
19
20
|
droplet_id: int = Field(unique=True, index=True)
|
|
20
21
|
name: str
|
xenfra_sdk/dependencies.py
CHANGED
xenfra_sdk/models.py
CHANGED
|
@@ -119,12 +119,15 @@ class ProjectRead(BaseModel):
|
|
|
119
119
|
|
|
120
120
|
# Intelligence Service Models
|
|
121
121
|
|
|
122
|
+
|
|
122
123
|
class PatchObject(BaseModel):
|
|
123
124
|
"""
|
|
124
125
|
Represents a structured patch for a configuration file.
|
|
125
126
|
"""
|
|
126
127
|
|
|
127
|
-
file: str | None = Field(
|
|
128
|
+
file: str | None = Field(
|
|
129
|
+
None, description="The name of the file to be patched (e.g., 'requirements.txt')"
|
|
130
|
+
)
|
|
128
131
|
operation: str | None = Field(None, description="The patch operation (e.g., 'add', 'replace')")
|
|
129
132
|
path: str | None = Field(None, description="A JSON-like path to the field to be changed")
|
|
130
133
|
value: str | None = Field(None, description="The new value to apply")
|
|
@@ -160,11 +163,20 @@ class CodebaseAnalysisResponse(BaseModel):
|
|
|
160
163
|
cache: str | None = Field(None, description="Detected cache (redis, memcached, none)")
|
|
161
164
|
workers: list[str] | None = Field(None, description="Detected background workers (celery, rq)")
|
|
162
165
|
env_vars: list[str] | None = Field(None, description="Required environment variables")
|
|
163
|
-
package_manager: str = Field(
|
|
164
|
-
|
|
166
|
+
package_manager: str = Field(
|
|
167
|
+
..., description="Detected package manager (uv, pip, poetry, npm, pnpm, yarn, go, bundler)"
|
|
168
|
+
)
|
|
169
|
+
dependency_file: str = Field(
|
|
170
|
+
...,
|
|
171
|
+
description="Dependency manifest file (pyproject.toml, requirements.txt, package.json, go.mod, Gemfile)",
|
|
172
|
+
)
|
|
165
173
|
has_conflict: bool = Field(False, description="True if multiple package managers detected")
|
|
166
|
-
detected_package_managers: list[PackageManagerOption] | None = Field(
|
|
167
|
-
|
|
174
|
+
detected_package_managers: list[PackageManagerOption] | None = Field(
|
|
175
|
+
None, description="All detected package managers (if conflict)"
|
|
176
|
+
)
|
|
177
|
+
instance_size: str = Field(
|
|
178
|
+
..., description="Recommended instance size (basic, standard, premium)"
|
|
179
|
+
)
|
|
168
180
|
estimated_cost_monthly: float = Field(..., description="Estimated monthly cost in USD")
|
|
169
181
|
confidence: float = Field(..., description="Confidence score (0.0-1.0)")
|
|
170
182
|
notes: str | None = Field(None, description="Additional observations")
|
xenfra_sdk/privacy.py
CHANGED
|
@@ -45,11 +45,11 @@ async def _refresh_patterns_from_url(url: str) -> Optional[List[str]]:
|
|
|
45
45
|
# Configure timeout from environment or default to 30 seconds
|
|
46
46
|
timeout_seconds = float(os.getenv("XENFRA_SDK_TIMEOUT", "30.0"))
|
|
47
47
|
timeout = httpx.Timeout(timeout_seconds, connect=10.0)
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
50
50
|
response = await client.get(url)
|
|
51
51
|
response.raise_for_status()
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
# Safe JSON parsing with content-type check
|
|
54
54
|
content_type = response.headers.get("content-type", "")
|
|
55
55
|
if "application/json" not in content_type:
|
|
@@ -58,17 +58,19 @@ async def _refresh_patterns_from_url(url: str) -> Optional[List[str]]:
|
|
|
58
58
|
"Skipping pattern refresh."
|
|
59
59
|
)
|
|
60
60
|
return None
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
try:
|
|
63
63
|
config = response.json()
|
|
64
64
|
except (ValueError, TypeError) as e:
|
|
65
65
|
logger.error(f"Failed to parse JSON from patterns URL {url}: {e}")
|
|
66
66
|
return None
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
if not isinstance(config, dict):
|
|
69
|
-
logger.error(
|
|
69
|
+
logger.error(
|
|
70
|
+
f"Expected dictionary from patterns URL {url}, got {type(config).__name__}"
|
|
71
|
+
)
|
|
70
72
|
return None
|
|
71
|
-
|
|
73
|
+
|
|
72
74
|
return config.get("redaction_patterns", [])
|
|
73
75
|
except httpx.TimeoutException as e:
|
|
74
76
|
logger.warning(f"Timeout fetching patterns from {url}: {e}")
|
|
@@ -42,7 +42,9 @@ class DeploymentsManager(BaseManager):
|
|
|
42
42
|
"""
|
|
43
43
|
try:
|
|
44
44
|
response = self._client._request("GET", f"/deployments/{deployment_id}/status")
|
|
45
|
-
logger.debug(
|
|
45
|
+
logger.debug(
|
|
46
|
+
f"DeploymentsManager.get_status({deployment_id}) response: {response.status_code}"
|
|
47
|
+
)
|
|
46
48
|
# Safe JSON parsing - _request() already handles status codes
|
|
47
49
|
return safe_json_parse(response)
|
|
48
50
|
except XenfraAPIError:
|
|
@@ -65,13 +67,15 @@ class DeploymentsManager(BaseManager):
|
|
|
65
67
|
"""
|
|
66
68
|
try:
|
|
67
69
|
response = self._client._request("GET", f"/deployments/{deployment_id}/logs")
|
|
68
|
-
logger.debug(
|
|
70
|
+
logger.debug(
|
|
71
|
+
f"DeploymentsManager.get_logs({deployment_id}) response: {response.status_code}"
|
|
72
|
+
)
|
|
69
73
|
|
|
70
74
|
# Safe JSON parsing with structure validation - _request() already handles status codes
|
|
71
75
|
data = safe_json_parse(response)
|
|
72
76
|
if not isinstance(data, dict):
|
|
73
77
|
raise XenfraError(f"Expected dictionary response, got {type(data).__name__}")
|
|
74
|
-
|
|
78
|
+
|
|
75
79
|
logs = safe_get_json_field(data, "logs", "")
|
|
76
80
|
|
|
77
81
|
if not logs:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Intelligence resource manager for Xenfra SDK.
|
|
3
3
|
Provides AI-powered deployment diagnosis and codebase analysis.
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
import logging
|
|
6
7
|
|
|
7
8
|
from ..exceptions import XenfraAPIError, XenfraError
|
|
@@ -22,10 +23,7 @@ class IntelligenceManager(BaseManager):
|
|
|
22
23
|
"""
|
|
23
24
|
|
|
24
25
|
def diagnose(
|
|
25
|
-
self,
|
|
26
|
-
logs: str,
|
|
27
|
-
package_manager: str | None = None,
|
|
28
|
-
dependency_file: str | None = None
|
|
26
|
+
self, logs: str, package_manager: str | None = None, dependency_file: str | None = None
|
|
29
27
|
) -> DiagnosisResponse:
|
|
30
28
|
"""
|
|
31
29
|
Diagnose deployment failure from logs using AI.
|
|
@@ -52,15 +50,9 @@ class IntelligenceManager(BaseManager):
|
|
|
52
50
|
if dependency_file:
|
|
53
51
|
payload["dependency_file"] = dependency_file
|
|
54
52
|
|
|
55
|
-
response = self._client._request(
|
|
56
|
-
"POST",
|
|
57
|
-
"/intelligence/diagnose",
|
|
58
|
-
json=payload
|
|
59
|
-
)
|
|
53
|
+
response = self._client._request("POST", "/intelligence/diagnose", json=payload)
|
|
60
54
|
|
|
61
|
-
logger.debug(
|
|
62
|
-
f"IntelligenceManager.diagnose response: status={response.status_code}"
|
|
63
|
-
)
|
|
55
|
+
logger.debug(f"IntelligenceManager.diagnose response: status={response.status_code}")
|
|
64
56
|
|
|
65
57
|
# Safe JSON parsing
|
|
66
58
|
data = safe_json_parse(response)
|
|
@@ -87,9 +79,7 @@ class IntelligenceManager(BaseManager):
|
|
|
87
79
|
"""
|
|
88
80
|
try:
|
|
89
81
|
response = self._client._request(
|
|
90
|
-
"POST",
|
|
91
|
-
"/intelligence/analyze-codebase",
|
|
92
|
-
json={"code_snippets": code_snippets}
|
|
82
|
+
"POST", "/intelligence/analyze-codebase", json={"code_snippets": code_snippets}
|
|
93
83
|
)
|
|
94
84
|
|
|
95
85
|
logger.debug(
|
xenfra_sdk/resources/projects.py
CHANGED
|
@@ -24,7 +24,9 @@ class ProjectsManager(BaseManager):
|
|
|
24
24
|
projects = safe_get_json_field(data, "projects", [])
|
|
25
25
|
|
|
26
26
|
if not isinstance(projects, list):
|
|
27
|
-
raise XenfraError(
|
|
27
|
+
raise XenfraError(
|
|
28
|
+
f"Expected 'projects' to be a list, got {type(projects).__name__}"
|
|
29
|
+
)
|
|
28
30
|
|
|
29
31
|
return [ProjectRead(**p) for p in projects]
|
|
30
32
|
except XenfraAPIError:
|
|
@@ -58,10 +60,7 @@ class ProjectsManager(BaseManager):
|
|
|
58
60
|
raise XenfraError(f"Failed to get project {project_id}: {e}")
|
|
59
61
|
|
|
60
62
|
def create(
|
|
61
|
-
self,
|
|
62
|
-
name: str,
|
|
63
|
-
region: str = "nyc3",
|
|
64
|
-
size_slug: str = "s-1vcpu-1gb"
|
|
63
|
+
self, name: str, region: str = "nyc3", size_slug: str = "s-1vcpu-1gb"
|
|
65
64
|
) -> ProjectRead:
|
|
66
65
|
"""Create a new project.
|
|
67
66
|
|
|
@@ -78,11 +77,7 @@ class ProjectsManager(BaseManager):
|
|
|
78
77
|
XenfraError: If there's a network or parsing error.
|
|
79
78
|
"""
|
|
80
79
|
try:
|
|
81
|
-
payload = {
|
|
82
|
-
"name": name,
|
|
83
|
-
"region": region,
|
|
84
|
-
"size_slug": size_slug
|
|
85
|
-
}
|
|
80
|
+
payload = {"name": name, "region": region, "size_slug": size_slug}
|
|
86
81
|
logger.debug(f"ProjectsManager.create payload: {payload}")
|
|
87
82
|
response = self._client._request("POST", "/projects/", json=payload)
|
|
88
83
|
# Safe JSON parsing
|
xenfra_sdk/utils.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import tomllib # Python 3.11+
|
|
3
|
-
from typing import Any, Dict
|
|
3
|
+
from typing import Any, Dict
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
|
|
@@ -92,9 +92,7 @@ def safe_json_parse(response: httpx.Response) -> Dict[str, Any]:
|
|
|
92
92
|
if "application/json" not in content_type:
|
|
93
93
|
# Try to get error text for better error messages
|
|
94
94
|
error_text = response.text[:500] if response.text else "Unknown error"
|
|
95
|
-
raise XenfraError(
|
|
96
|
-
f"Expected JSON response, got {content_type}. Response: {error_text}"
|
|
97
|
-
)
|
|
95
|
+
raise XenfraError(f"Expected JSON response, got {content_type}. Response: {error_text}")
|
|
98
96
|
|
|
99
97
|
try:
|
|
100
98
|
return response.json()
|
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
xenfra_sdk/__init__.py,sha256=lk9xo2msYvs_JgBePTIuxRb2sBW-egJS_EAOq4w4xQo,467
|
|
2
2
|
xenfra_sdk/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
xenfra_sdk/cli/main.py,sha256=541nlIUYFFeu4h1sCXivaHMC7SqpskazI0YocM8ylh4,7958
|
|
4
|
-
xenfra_sdk/client.py,sha256=
|
|
5
|
-
xenfra_sdk/client_with_hooks.py,sha256=
|
|
4
|
+
xenfra_sdk/client.py,sha256=ufxjjVY3UG1roR0q8LYBmCHOJ13uRQuycqTqYuHgqTs,3514
|
|
5
|
+
xenfra_sdk/client_with_hooks.py,sha256=iN-xTGdeSPizktM6UG-aZEsuwQc5OgosNYUf1_Tq4Dc,10046
|
|
6
6
|
xenfra_sdk/config.py,sha256=gaT4k5iJgW9guNVmlnYkCFGDSU1_er4LRZA2CgfKmJ0,588
|
|
7
7
|
xenfra_sdk/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
xenfra_sdk/db/models.py,sha256=
|
|
8
|
+
xenfra_sdk/db/models.py,sha256=LqnpUbtSZ21jdPVzdyFyxpYqOXUkBVl5JGXq85ASI7g,875
|
|
9
9
|
xenfra_sdk/db/session.py,sha256=LoTKFO3FTsx5AtZ-0ZsplxXjAdzOgcr3Yk-dkeJsz5U,823
|
|
10
|
-
xenfra_sdk/dependencies.py,sha256=
|
|
10
|
+
xenfra_sdk/dependencies.py,sha256=WHGfIrEYkss5yuBd_uOFrB6lPBWM2X0mEuG26ILAjXI,1211
|
|
11
11
|
xenfra_sdk/dockerizer.py,sha256=73CFwztKogBmlX00tek0M8evncnZRDThQKmg7auw6mc,3059
|
|
12
12
|
xenfra_sdk/engine.py,sha256=5qrtDc8qXC9xuZGPUwcUeGEtVuVksWY64sCxh-Wqa7o,15517
|
|
13
13
|
xenfra_sdk/exceptions.py,sha256=aMVtDVlzG7-FT2G_b-pJSuuey22B4YvC-b-L37GaImM,477
|
|
14
14
|
xenfra_sdk/mcp_client.py,sha256=NZtQz_qK_8i504rVPXlE1vPdzt75hg8Lkp4d8BA8dk0,5777
|
|
15
|
-
xenfra_sdk/models.py,sha256=
|
|
15
|
+
xenfra_sdk/models.py,sha256=o03GUy4qcgAJa9KzR9UXWKLPWTR5gvE8GF6KSAj80eM,7003
|
|
16
16
|
xenfra_sdk/patterns.json,sha256=xHxbc0ogHDwysMczi30_hW1Ylfdsf-nsQdAom7RZ4KI,446
|
|
17
|
-
xenfra_sdk/privacy.py,sha256=
|
|
17
|
+
xenfra_sdk/privacy.py,sha256=Bscv7bopCQTeJhvU8Z2jxdBfc5tCZDY5kuODonTcgSk,5509
|
|
18
18
|
xenfra_sdk/recipes.py,sha256=J6-7tDWVXv8IelkdBta5pDRhD0o5VgAM_jWDKvPvm5g,852
|
|
19
19
|
xenfra_sdk/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
20
|
xenfra_sdk/resources/base.py,sha256=5n-HTKAnIX2lTgXwio0xtwoaBn-nksjdm8qRTpe3iDk,81
|
|
21
|
-
xenfra_sdk/resources/deployments.py,sha256=
|
|
22
|
-
xenfra_sdk/resources/intelligence.py,sha256=
|
|
23
|
-
xenfra_sdk/resources/projects.py,sha256=
|
|
21
|
+
xenfra_sdk/resources/deployments.py,sha256=QIOd-9L1m-FW5I6eXZEtQSteg_gkIzk2nnqr_hQs5gc,3339
|
|
22
|
+
xenfra_sdk/resources/intelligence.py,sha256=ihkhv8jj4FDB4pbSezAnUL-5syZ77lLXPI6X38THcnQ,3323
|
|
23
|
+
xenfra_sdk/resources/projects.py,sha256=EsCVXmqkhWl_Guz_8WDQDi3kAm1Wyg1rjXcyAigPD6E,3712
|
|
24
24
|
xenfra_sdk/security.py,sha256=6vMZpbglhkRGBVVj4RCTu45-MCnQ15wt94-996zmaT8,1199
|
|
25
25
|
xenfra_sdk/templates/Dockerfile.j2,sha256=apWts895OOoUYwj_fOa6OiylFB5m8zFEYvJ1Nki32YM,664
|
|
26
26
|
xenfra_sdk/templates/cloud-init.sh.j2,sha256=QCWG8hL1V05bAQ7BQ70QfuhIvS4tnsL8ZTCVtyi9F0A,2222
|
|
27
27
|
xenfra_sdk/templates/docker-compose.yml.j2,sha256=zKUT2cd_FrxXvRxE-vAAjuQk3-nLNQjRe-StkhAWRQA,860
|
|
28
|
-
xenfra_sdk/utils.py,sha256=
|
|
29
|
-
xenfra_sdk-0.1.
|
|
30
|
-
xenfra_sdk-0.1.
|
|
31
|
-
xenfra_sdk-0.1.
|
|
28
|
+
xenfra_sdk/utils.py,sha256=d8eCjjV32QwqoJa759CEcETnnsjG5qVKDLQ84yYtlus,3898
|
|
29
|
+
xenfra_sdk-0.1.2.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
|
|
30
|
+
xenfra_sdk-0.1.2.dist-info/METADATA,sha256=sFTH52KuMEL0mWE3gFhKAYoOE_rS6iX_FOFJp19bhtU,3889
|
|
31
|
+
xenfra_sdk-0.1.2.dist-info/RECORD,,
|
|
File without changes
|