xenfra-sdk 0.1.0__py3-none-any.whl → 0.1.1__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 CHANGED
@@ -6,10 +6,15 @@ 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
9
10
 
10
11
 
11
12
  class XenfraClient:
12
- def __init__(self, token: str = None, api_url: str = "http://localhost:8000"):
13
+ def __init__(self, token: str = None, api_url: str = None):
14
+ # Use provided URL, or fall back to env var, or default to production
15
+ if api_url is None:
16
+ api_url = os.getenv("XENFRA_API_URL", "https://api.xenfra.tech")
17
+
13
18
  self.api_url = api_url
14
19
  self._token = token or os.getenv("XENFRA_TOKEN")
15
20
  if not self._token:
@@ -42,7 +47,19 @@ class XenfraClient:
42
47
  return response
43
48
  except httpx.HTTPStatusError as e:
44
49
  # Convert httpx error to our custom SDK error
45
- detail = e.response.json().get("detail", e.response.text)
50
+ # Safe JSON parsing with fallback
51
+ try:
52
+ content_type = e.response.headers.get("content-type", "")
53
+ if "application/json" in content_type:
54
+ try:
55
+ error_data = e.response.json()
56
+ detail = error_data.get("detail", e.response.text[:500] if e.response.text else "Unknown error")
57
+ except (ValueError, TypeError):
58
+ detail = e.response.text[:500] if e.response.text else "Unknown error"
59
+ else:
60
+ detail = e.response.text[:500] if e.response.text else "Unknown error"
61
+ except Exception:
62
+ detail = "Unknown error"
46
63
  raise XenfraAPIError(status_code=e.response.status_code, detail=detail) from e
47
64
  except httpx.RequestError as e:
48
65
  # Handle connection errors, timeouts, etc.
@@ -1,9 +1,10 @@
1
1
  """
2
2
  Enhanced XenfraClient with context management and lifecycle hooks.
3
3
  """
4
+
4
5
  import logging
5
6
  import os
6
- from typing import Any, Callable, Optional
7
+ from typing import Callable
7
8
 
8
9
  import httpx
9
10
 
@@ -11,6 +12,7 @@ from .exceptions import AuthenticationError, XenfraAPIError, XenfraError
11
12
  from .resources.deployments import DeploymentsManager
12
13
  from .resources.intelligence import IntelligenceManager
13
14
  from .resources.projects import ProjectsManager
15
+ from .utils import safe_json_parse
14
16
 
15
17
  logger = logging.getLogger(__name__)
16
18
 
@@ -102,7 +104,7 @@ class XenfraClient:
102
104
  headers={
103
105
  "Authorization": f"Bearer {self._token}",
104
106
  "Content-Type": "application/json",
105
- "User-Agent": "Xenfra-SDK/0.2.0",
107
+ "User-Agent": "Xenfra-SDK/0.2.4",
106
108
  },
107
109
  timeout=timeout,
108
110
  transport=transport,
@@ -178,7 +180,22 @@ class XenfraClient:
178
180
 
179
181
  except httpx.HTTPStatusError as e:
180
182
  # API error (4xx, 5xx)
181
- detail = e.response.json().get("detail", e.response.text) if e.response else str(e)
183
+ # Safe JSON parsing with fallback
184
+ if e.response:
185
+ try:
186
+ content_type = e.response.headers.get("content-type", "")
187
+ if "application/json" in content_type:
188
+ try:
189
+ error_data = e.response.json()
190
+ detail = error_data.get("detail", e.response.text[:500] if e.response.text else "Unknown error")
191
+ except (ValueError, TypeError):
192
+ detail = e.response.text[:500] if e.response.text else "Unknown error"
193
+ else:
194
+ detail = e.response.text[:500] if e.response.text else "Unknown error"
195
+ except Exception:
196
+ detail = "Unknown error"
197
+ else:
198
+ detail = str(e)
182
199
 
183
200
  # Run error hooks
184
201
  error_context = {**request_context, "error": e, "response": e.response}
@@ -189,11 +206,12 @@ class XenfraClient:
189
206
  logger.warning(f"on_error hook failed: {hook_error}")
190
207
 
191
208
  # Log error
192
- logger.error(f"{method} {path} failed: {e.response.status_code if e.response else 'unknown'}")
209
+ logger.error(
210
+ f"{method} {path} failed: {e.response.status_code if e.response else 'unknown'}"
211
+ )
193
212
 
194
213
  raise XenfraAPIError(
195
- status_code=e.response.status_code if e.response else 500,
196
- detail=detail
214
+ status_code=e.response.status_code if e.response else 500, detail=detail
197
215
  ) from e
198
216
 
199
217
  except httpx.RequestError as e:
@@ -227,7 +245,9 @@ class XenfraClient:
227
245
  def __del__(self):
228
246
  """Destructor - cleanup if not already closed."""
229
247
  if not self._closed:
230
- logger.warning("XenfraClient was not properly closed. Use 'with' statement or call close().")
248
+ logger.warning(
249
+ "XenfraClient was not properly closed. Use 'with' statement or call close()."
250
+ )
231
251
  self.close()
232
252
 
233
253
  def __repr__(self):
@@ -238,6 +258,7 @@ class XenfraClient:
238
258
 
239
259
  # Example hooks for common use cases
240
260
 
261
+
241
262
  def logging_hook_before(request_context):
242
263
  """Example: Log all requests."""
243
264
  print(f"→ {request_context['method']} {request_context['url']}")
@@ -264,12 +285,14 @@ def rate_limit_tracker_hook(request_context, response):
264
285
  def request_timing_hook(request_context):
265
286
  """Example: Track request timing."""
266
287
  import time
288
+
267
289
  request_context["start_time"] = time.time()
268
290
 
269
291
 
270
292
  def response_timing_hook(request_context, response):
271
293
  """Example: Calculate request duration."""
272
294
  import time
295
+
273
296
  if "start_time" in request_context:
274
297
  duration = time.time() - request_context["start_time"]
275
298
  print(f"Request took {duration:.3f}s")
xenfra_sdk/config.py CHANGED
@@ -10,8 +10,8 @@ class Settings(BaseSettings):
10
10
  SECRET_KEY: str
11
11
  ENCRYPTION_KEY: str
12
12
 
13
- GITHUB_CLIENT_ID: str
14
- GITHUB_CLIENT_SECRET: str
13
+ GH_CLIENT_ID: str
14
+ GH_CLIENT_SECRET: str
15
15
  GITHUB_REDIRECT_URI: str
16
16
  GITHUB_WEBHOOK_SECRET: str
17
17
 
xenfra_sdk/privacy.py CHANGED
@@ -5,12 +5,16 @@ before it is sent to diagnostic endpoints, upholding privacy-first principles.
5
5
  """
6
6
 
7
7
  import json
8
+ import logging
9
+ import os
8
10
  import re
9
11
  from pathlib import Path
10
12
  from typing import List, Optional
11
13
 
12
14
  import httpx # For fetching patterns from URL
13
15
 
16
+ logger = logging.getLogger(__name__)
17
+
14
18
  # Path to the patterns file within the SDK
15
19
  _PATTERNS_FILE_PATH = Path(__file__).parent / "patterns.json"
16
20
  _REDACTION_PLACEHOLDER = "[REDACTED]"
@@ -20,8 +24,8 @@ _CACHED_PATTERNS: List[re.Pattern] = []
20
24
  def _load_patterns_from_file(file_path: Path) -> List[str]:
21
25
  """Loads raw regex patterns from a JSON file."""
22
26
  if not file_path.exists():
23
- print(
24
- f"Warning: Patterns file not found at {file_path}. No patterns will be used for scrubbing."
27
+ logger.warning(
28
+ f"Patterns file not found at {file_path}. No patterns will be used for scrubbing."
25
29
  )
26
30
  return []
27
31
  try:
@@ -29,7 +33,7 @@ def _load_patterns_from_file(file_path: Path) -> List[str]:
29
33
  config = json.load(f)
30
34
  return config.get("redaction_patterns", [])
31
35
  except json.JSONDecodeError as e:
32
- print(f"Error decoding patterns.json: {e}. Falling back to empty patterns.")
36
+ logger.error(f"Error decoding patterns.json: {e}. Falling back to empty patterns.")
33
37
  return []
34
38
 
35
39
 
@@ -38,16 +42,45 @@ async def _refresh_patterns_from_url(url: str) -> Optional[List[str]]:
38
42
  Fetches updated patterns from a URL asynchronously.
39
43
  """
40
44
  try:
41
- async with httpx.AsyncClient() as client:
42
- response = await client.get(url, timeout=5.0)
45
+ # Configure timeout from environment or default to 30 seconds
46
+ timeout_seconds = float(os.getenv("XENFRA_SDK_TIMEOUT", "30.0"))
47
+ timeout = httpx.Timeout(timeout_seconds, connect=10.0)
48
+
49
+ async with httpx.AsyncClient(timeout=timeout) as client:
50
+ response = await client.get(url)
43
51
  response.raise_for_status()
44
- config = response.json()
52
+
53
+ # Safe JSON parsing with content-type check
54
+ content_type = response.headers.get("content-type", "")
55
+ if "application/json" not in content_type:
56
+ logger.warning(
57
+ f"Expected JSON response from {url}, got {content_type}. "
58
+ "Skipping pattern refresh."
59
+ )
60
+ return None
61
+
62
+ try:
63
+ config = response.json()
64
+ except (ValueError, TypeError) as e:
65
+ logger.error(f"Failed to parse JSON from patterns URL {url}: {e}")
66
+ return None
67
+
68
+ if not isinstance(config, dict):
69
+ logger.error(f"Expected dictionary from patterns URL {url}, got {type(config).__name__}")
70
+ return None
71
+
45
72
  return config.get("redaction_patterns", [])
73
+ except httpx.TimeoutException as e:
74
+ logger.warning(f"Timeout fetching patterns from {url}: {e}")
75
+ return None
46
76
  except httpx.RequestError as e:
47
- print(f"Error fetching patterns from {url}: {e}")
77
+ logger.warning(f"Error fetching patterns from {url}: {e}")
48
78
  return None
49
79
  except json.JSONDecodeError as e:
50
- print(f"Error decoding JSON from patterns URL {url}: {e}")
80
+ logger.error(f"Error decoding JSON from patterns URL {url}: {e}")
81
+ return None
82
+ except Exception as e:
83
+ logger.error(f"Unexpected error fetching patterns from {url}: {e}")
51
84
  return None
52
85
 
53
86
 
@@ -3,6 +3,7 @@ import logging
3
3
  # Import Deployment model when it's defined in models.py
4
4
  # from ..models import Deployment
5
5
  from ..exceptions import XenfraAPIError, XenfraError # Add XenfraError
6
+ from ..utils import safe_get_json_field, safe_json_parse
6
7
  from .base import BaseManager
7
8
 
8
9
  logger = logging.getLogger(__name__)
@@ -19,9 +20,8 @@ class DeploymentsManager(BaseManager):
19
20
  "framework": framework,
20
21
  }
21
22
  response = self._client._request("POST", "/deployments", json=payload)
22
- response.raise_for_status()
23
- # Assuming the API returns a dict, which will be parsed into a Deployment model
24
- return response.json()
23
+ # Safe JSON parsing
24
+ return safe_json_parse(response)
25
25
  except XenfraAPIError:
26
26
  raise
27
27
  except Exception as e:
@@ -43,8 +43,8 @@ class DeploymentsManager(BaseManager):
43
43
  try:
44
44
  response = self._client._request("GET", f"/deployments/{deployment_id}/status")
45
45
  logger.debug(f"DeploymentsManager.get_status({deployment_id}) response: {response.status_code}")
46
- response.raise_for_status()
47
- return response.json()
46
+ # Safe JSON parsing - _request() already handles status codes
47
+ return safe_json_parse(response)
48
48
  except XenfraAPIError:
49
49
  raise # Re-raise API errors
50
50
  except Exception as e:
@@ -66,11 +66,13 @@ class DeploymentsManager(BaseManager):
66
66
  try:
67
67
  response = self._client._request("GET", f"/deployments/{deployment_id}/logs")
68
68
  logger.debug(f"DeploymentsManager.get_logs({deployment_id}) response: {response.status_code}")
69
- response.raise_for_status()
70
69
 
71
- # Parse response - API should return {"logs": "log content"}
72
- data = response.json()
73
- logs = data.get("logs", "")
70
+ # Safe JSON parsing with structure validation - _request() already handles status codes
71
+ data = safe_json_parse(response)
72
+ if not isinstance(data, dict):
73
+ raise XenfraError(f"Expected dictionary response, got {type(data).__name__}")
74
+
75
+ logs = safe_get_json_field(data, "logs", "")
74
76
 
75
77
  if not logs:
76
78
  logger.warning(f"No logs found for deployment {deployment_id}")
@@ -6,6 +6,7 @@ import logging
6
6
 
7
7
  from ..exceptions import XenfraAPIError, XenfraError
8
8
  from ..models import CodebaseAnalysisResponse, DiagnosisResponse
9
+ from ..utils import safe_json_parse
9
10
  from .base import BaseManager
10
11
 
11
12
  logger = logging.getLogger(__name__)
@@ -61,8 +62,9 @@ class IntelligenceManager(BaseManager):
61
62
  f"IntelligenceManager.diagnose response: status={response.status_code}"
62
63
  )
63
64
 
64
- response.raise_for_status()
65
- return DiagnosisResponse(**response.json())
65
+ # Safe JSON parsing
66
+ data = safe_json_parse(response)
67
+ return DiagnosisResponse(**data)
66
68
  except XenfraAPIError:
67
69
  raise
68
70
  except Exception as e:
@@ -94,8 +96,9 @@ class IntelligenceManager(BaseManager):
94
96
  f"IntelligenceManager.analyze_codebase response: status={response.status_code}"
95
97
  )
96
98
 
97
- response.raise_for_status()
98
- return CodebaseAnalysisResponse(**response.json())
99
+ # Safe JSON parsing
100
+ data = safe_json_parse(response)
101
+ return CodebaseAnalysisResponse(**data)
99
102
  except XenfraAPIError:
100
103
  raise
101
104
  except Exception as e:
@@ -2,6 +2,7 @@ import logging
2
2
 
3
3
  from ..exceptions import XenfraAPIError, XenfraError # Add XenfraError
4
4
  from ..models import ProjectRead
5
+ from ..utils import safe_get_json_field, safe_json_parse
5
6
  from .base import BaseManager
6
7
 
7
8
  logger = logging.getLogger(__name__)
@@ -18,8 +19,14 @@ class ProjectsManager(BaseManager):
18
19
  f"body={response.text[:200]}..." # Truncate long responses
19
20
  )
20
21
 
21
- response.raise_for_status()
22
- return [ProjectRead(**p) for p in response.json()["projects"]]
22
+ # Safe JSON parsing with structure validation
23
+ data = safe_json_parse(response)
24
+ projects = safe_get_json_field(data, "projects", [])
25
+
26
+ if not isinstance(projects, list):
27
+ raise XenfraError(f"Expected 'projects' to be a list, got {type(projects).__name__}")
28
+
29
+ return [ProjectRead(**p) for p in projects]
23
30
  except XenfraAPIError:
24
31
  raise # Re-raise API errors
25
32
  except Exception as e:
@@ -42,8 +49,9 @@ class ProjectsManager(BaseManager):
42
49
  try:
43
50
  response = self._client._request("GET", f"/projects/{project_id}")
44
51
  logger.debug(f"ProjectsManager.show({project_id}) response: {response.status_code}")
45
- response.raise_for_status()
46
- return ProjectRead(**response.json())
52
+ # Safe JSON parsing
53
+ data = safe_json_parse(response)
54
+ return ProjectRead(**data)
47
55
  except XenfraAPIError:
48
56
  raise # Re-raise API errors
49
57
  except Exception as e:
@@ -77,8 +85,9 @@ class ProjectsManager(BaseManager):
77
85
  }
78
86
  logger.debug(f"ProjectsManager.create payload: {payload}")
79
87
  response = self._client._request("POST", "/projects/", json=payload)
80
- response.raise_for_status()
81
- return ProjectRead(**response.json())
88
+ # Safe JSON parsing
89
+ data = safe_json_parse(response)
90
+ return ProjectRead(**data)
82
91
  except XenfraAPIError:
83
92
  raise
84
93
  except Exception as e:
@@ -87,8 +96,8 @@ class ProjectsManager(BaseManager):
87
96
  def delete(self, project_id: str) -> None:
88
97
  """Deletes a project."""
89
98
  try:
90
- response = self._client._request("DELETE", f"/projects/{project_id}")
91
- response.raise_for_status()
99
+ # _request() already handles status codes and raises XenfraAPIError for non-2xx
100
+ self._client._request("DELETE", f"/projects/{project_id}")
92
101
  except XenfraAPIError:
93
102
  raise
94
103
  except Exception as e:
xenfra_sdk/utils.py CHANGED
@@ -1,5 +1,10 @@
1
1
  import os
2
2
  import tomllib # Python 3.11+
3
+ from typing import Any, Dict, Optional
4
+
5
+ import httpx
6
+
7
+ from .exceptions import XenfraError
3
8
 
4
9
 
5
10
  def get_project_context():
@@ -68,3 +73,49 @@ def get_project_context():
68
73
  }
69
74
 
70
75
  return context
76
+
77
+
78
+ def safe_json_parse(response: httpx.Response) -> Dict[str, Any]:
79
+ """
80
+ Safely parse JSON from HTTP response with content-type validation and error handling.
81
+
82
+ Args:
83
+ response: HTTP response object
84
+
85
+ Returns:
86
+ Parsed JSON dictionary
87
+
88
+ Raises:
89
+ XenfraError: If response is not JSON or parsing fails
90
+ """
91
+ content_type = response.headers.get("content-type", "")
92
+ if "application/json" not in content_type:
93
+ # Try to get error text for better error messages
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
+ )
98
+
99
+ try:
100
+ return response.json()
101
+ except (ValueError, TypeError) as e:
102
+ error_text = response.text[:500] if response.text else "Unknown error"
103
+ raise XenfraError(f"Failed to parse JSON response: {e}. Response: {error_text}")
104
+
105
+
106
+ def safe_get_json_field(data: Dict[str, Any], field: str, default: Any = None) -> Any:
107
+ """
108
+ Safely get a field from JSON data with validation.
109
+
110
+ Args:
111
+ data: JSON dictionary
112
+ field: Field name to retrieve
113
+ default: Default value if field is missing
114
+
115
+ Returns:
116
+ Field value or default
117
+ """
118
+ if not isinstance(data, dict):
119
+ raise XenfraError(f"Expected dictionary, got {type(data).__name__}")
120
+
121
+ return data.get(field, default)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: xenfra-sdk
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Xenfra SDK: Core engine and utilities for the Xenfra platform.
5
5
  Author: xenfra-cloud
6
6
  Author-email: xenfra-cloud <xenfracloud@gmail.com>
@@ -1,9 +1,9 @@
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=vFtfAof8X270-qs6-gD1Qo8LGjpQKxCfwbEe5GWd6nE,2641
5
- xenfra_sdk/client_with_hooks.py,sha256=ViAWF4lnUml4gjVapTwC7UUQesJn5ZaeGHluZlRC-50,9165
6
- xenfra_sdk/config.py,sha256=5dlmpxzM9OXJvfhiwG7DsU2XXf1yte4-mISDJdbpzPk,596
4
+ xenfra_sdk/client.py,sha256=pnmdmmTtECyV7MmT2_DViXeaQP0q1W1eOhcqOTdmi80,3495
5
+ xenfra_sdk/client_with_hooks.py,sha256=5mKIF3rbN1hlZKCCNOlMyFBbuwugBeCU9_8ZcQ_8uOs,9986
6
+ xenfra_sdk/config.py,sha256=gaT4k5iJgW9guNVmlnYkCFGDSU1_er4LRZA2CgfKmJ0,588
7
7
  xenfra_sdk/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  xenfra_sdk/db/models.py,sha256=H29uHzXizkim5KnEAdmfMO8mytkCDCQl2NPzTgMMD7w,874
9
9
  xenfra_sdk/db/session.py,sha256=LoTKFO3FTsx5AtZ-0ZsplxXjAdzOgcr3Yk-dkeJsz5U,823
@@ -14,18 +14,18 @@ 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
15
  xenfra_sdk/models.py,sha256=73rZMHP2iPRcX9PC8KUgkmNEVgDozD4Tj09UneQhYJU,6923
16
16
  xenfra_sdk/patterns.json,sha256=xHxbc0ogHDwysMczi30_hW1Ylfdsf-nsQdAom7RZ4KI,446
17
- xenfra_sdk/privacy.py,sha256=-KmDyFDGmFTg4vS68qtVnDgXxbl9xJmKwPKDG1zPaFU,4176
17
+ xenfra_sdk/privacy.py,sha256=XXNLCrPZzqbYj8qrcootygE2WiDYyMVdMO7h0ow7vBQ,5527
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=qE9hPW0skt7w6QaqalVGaBAIFu6z4M97crvEn0xzMG4,3116
22
- xenfra_sdk/resources/intelligence.py,sha256=8W_XjOJSLH9qFNBepc6jgS9wer0NuML4BG8DU6LfXjY,3382
23
- xenfra_sdk/resources/projects.py,sha256=PNhLhu3-8jR1ucSGtKDRH5fvpNb1IQSzUNXEcojhmHY,3350
21
+ xenfra_sdk/resources/deployments.py,sha256=ybIV4ViNhnjWRkPaynp0wQjEKBjKby0GkAPd3_ohu-w,3291
22
+ xenfra_sdk/resources/intelligence.py,sha256=W1J2tr4W3xKB1v8Wya0edXB07Vr3zO0JZ7mIDk_dU1w,3470
23
+ xenfra_sdk/resources/projects.py,sha256=ZMox7siqSXWuXHsUBYkUb10Qik7wNvgPBZWN3C4gQnA,3760
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=J2q2ZATQxgrtoJXrbDCPRNyD7PFuv7ZAybcE99O2cCc,2400
29
- xenfra_sdk-0.1.0.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
30
- xenfra_sdk-0.1.0.dist-info/METADATA,sha256=UiaTkJ03P59x9D4MT-ONOk1rx3uhojR1VJ914kiRACQ,3889
31
- xenfra_sdk-0.1.0.dist-info/RECORD,,
28
+ xenfra_sdk/utils.py,sha256=uLlDb-kNRlr6RJvU8vJ6vXfgUEZeWZDfN2UtlrQoxfs,3930
29
+ xenfra_sdk-0.1.1.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
30
+ xenfra_sdk-0.1.1.dist-info/METADATA,sha256=jg_JTeKLR_SzyMovzvjYub641lc3nSObkPy7-V9GEe0,3889
31
+ xenfra_sdk-0.1.1.dist-info/RECORD,,