xenfra-sdk 0.1.2__py3-none-any.whl → 0.1.4__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/__init__.py +21 -21
- xenfra_sdk/cli/main.py +226 -226
- xenfra_sdk/config.py +26 -26
- xenfra_sdk/db/models.py +24 -28
- xenfra_sdk/db/session.py +30 -30
- xenfra_sdk/dependencies.py +39 -39
- xenfra_sdk/dockerizer.py +87 -87
- xenfra_sdk/engine.py +411 -388
- xenfra_sdk/exceptions.py +19 -19
- xenfra_sdk/mcp_client.py +154 -154
- xenfra_sdk/models.py +182 -182
- xenfra_sdk/patterns.json +13 -13
- xenfra_sdk/privacy.py +153 -153
- xenfra_sdk/recipes.py +25 -25
- xenfra_sdk/resources/base.py +3 -3
- xenfra_sdk/resources/deployments.py +185 -89
- xenfra_sdk/resources/intelligence.py +95 -95
- xenfra_sdk/security.py +41 -41
- xenfra_sdk/templates/Dockerfile.j2 +25 -25
- xenfra_sdk/templates/cloud-init.sh.j2 +68 -68
- xenfra_sdk/templates/docker-compose.yml.j2 +33 -33
- {xenfra_sdk-0.1.2.dist-info → xenfra_sdk-0.1.4.dist-info}/METADATA +92 -92
- xenfra_sdk-0.1.4.dist-info/RECORD +31 -0
- {xenfra_sdk-0.1.2.dist-info → xenfra_sdk-0.1.4.dist-info}/WHEEL +1 -1
- xenfra_sdk-0.1.2.dist-info/RECORD +0 -31
xenfra_sdk/exceptions.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
class XenfraError(Exception):
|
|
2
|
-
"""Base exception for all SDK errors."""
|
|
3
|
-
|
|
4
|
-
pass
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class AuthenticationError(XenfraError):
|
|
8
|
-
"""Raised for issues related to authentication."""
|
|
9
|
-
|
|
10
|
-
pass
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class XenfraAPIError(XenfraError):
|
|
14
|
-
"""Raised when the API returns a non-2xx status code."""
|
|
15
|
-
|
|
16
|
-
def __init__(self, status_code: int, detail: str):
|
|
17
|
-
self.status_code = status_code
|
|
18
|
-
self.detail = detail
|
|
19
|
-
super().__init__(f"API Error {status_code}: {detail}")
|
|
1
|
+
class XenfraError(Exception):
|
|
2
|
+
"""Base exception for all SDK errors."""
|
|
3
|
+
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AuthenticationError(XenfraError):
|
|
8
|
+
"""Raised for issues related to authentication."""
|
|
9
|
+
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class XenfraAPIError(XenfraError):
|
|
14
|
+
"""Raised when the API returns a non-2xx status code."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, status_code: int, detail: str):
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.detail = detail
|
|
19
|
+
super().__init__(f"API Error {status_code}: {detail}")
|
xenfra_sdk/mcp_client.py
CHANGED
|
@@ -1,154 +1,154 @@
|
|
|
1
|
-
# src/xenfra/mcp_client.py
|
|
2
|
-
|
|
3
|
-
import base64
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
import subprocess
|
|
7
|
-
import tempfile
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class MCPClient:
|
|
12
|
-
"""
|
|
13
|
-
A client for communicating with a local github-mcp-server process.
|
|
14
|
-
|
|
15
|
-
This client starts the MCP server as a subprocess and interacts with it
|
|
16
|
-
over stdin and stdout to download a full repository to a temporary directory.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
def __init__(self, mcp_server_path="github-mcp-server"):
|
|
20
|
-
"""
|
|
21
|
-
Initializes the MCPClient.
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
mcp_server_path (str): The path to the github-mcp-server executable.
|
|
25
|
-
Assumes it's in the system's PATH by default.
|
|
26
|
-
"""
|
|
27
|
-
self.mcp_server_path = mcp_server_path
|
|
28
|
-
self.process = None
|
|
29
|
-
|
|
30
|
-
def _start_server(self):
|
|
31
|
-
"""Starts the github-mcp-server subprocess."""
|
|
32
|
-
if self.process and self.process.poll() is None:
|
|
33
|
-
return
|
|
34
|
-
try:
|
|
35
|
-
self.process = subprocess.Popen(
|
|
36
|
-
[self.mcp_server_path],
|
|
37
|
-
stdin=subprocess.PIPE,
|
|
38
|
-
stdout=subprocess.PIPE,
|
|
39
|
-
stderr=subprocess.PIPE,
|
|
40
|
-
text=True,
|
|
41
|
-
env=os.environ,
|
|
42
|
-
)
|
|
43
|
-
except FileNotFoundError:
|
|
44
|
-
raise RuntimeError(
|
|
45
|
-
f"'{self.mcp_server_path}' not found. Ensure github-mcp-server is installed and in your PATH."
|
|
46
|
-
)
|
|
47
|
-
except Exception as e:
|
|
48
|
-
raise RuntimeError(f"Failed to start MCP server: {e}")
|
|
49
|
-
|
|
50
|
-
def _stop_server(self):
|
|
51
|
-
"""Stops the MCP server process."""
|
|
52
|
-
if self.process:
|
|
53
|
-
self.process.terminate()
|
|
54
|
-
self.process.wait()
|
|
55
|
-
self.process = None
|
|
56
|
-
|
|
57
|
-
def _send_request(self, method: str, params: dict) -> dict:
|
|
58
|
-
"""Sends a JSON-RPC request and returns the response."""
|
|
59
|
-
if not self.process or self.process.poll() is not None:
|
|
60
|
-
self._start_server()
|
|
61
|
-
|
|
62
|
-
request = {"jsonrpc": "2.0", "id": os.urandom(4).hex(), "method": method, "params": params}
|
|
63
|
-
|
|
64
|
-
try:
|
|
65
|
-
self.process.stdin.write(json.dumps(request) + "\n")
|
|
66
|
-
self.process.stdin.flush()
|
|
67
|
-
response_line = self.process.stdout.readline()
|
|
68
|
-
if not response_line:
|
|
69
|
-
error_output = self.process.stderr.read()
|
|
70
|
-
raise RuntimeError(f"MCP server closed stream unexpectedly. Error: {error_output}")
|
|
71
|
-
|
|
72
|
-
response = json.loads(response_line)
|
|
73
|
-
if "error" in response:
|
|
74
|
-
raise RuntimeError(f"MCP server returned an error: {response['error']}")
|
|
75
|
-
return response.get("result", {})
|
|
76
|
-
except (BrokenPipeError, json.JSONDecodeError) as e:
|
|
77
|
-
raise RuntimeError(f"Failed to communicate with MCP server: {e}")
|
|
78
|
-
|
|
79
|
-
def download_repo_to_tempdir(self, repo_url: str, commit_sha: str = "HEAD") -> str:
|
|
80
|
-
"""
|
|
81
|
-
Downloads an entire repository at a specific commit to a local temporary directory.
|
|
82
|
-
|
|
83
|
-
Args:
|
|
84
|
-
repo_url (str): The full URL of the GitHub repository.
|
|
85
|
-
commit_sha (str): The commit SHA to download. Defaults to "HEAD".
|
|
86
|
-
|
|
87
|
-
Returns:
|
|
88
|
-
The path to the temporary directory containing the downloaded code.
|
|
89
|
-
"""
|
|
90
|
-
try:
|
|
91
|
-
parts = repo_url.strip("/").split("/")
|
|
92
|
-
owner = parts[-2]
|
|
93
|
-
repo_name = parts[-1].replace(".git", "")
|
|
94
|
-
except IndexError:
|
|
95
|
-
raise ValueError(
|
|
96
|
-
"Invalid repository URL format. Expected format: https://github.com/owner/repo"
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
print(f" [MCP] Fetching file tree for {owner}/{repo_name} at {commit_sha}...")
|
|
100
|
-
tree_result = self._send_request(
|
|
101
|
-
method="git.get_repository_tree",
|
|
102
|
-
params={"owner": owner, "repo": repo_name, "tree_sha": commit_sha, "recursive": True},
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
tree = tree_result.get("tree", [])
|
|
106
|
-
if not tree:
|
|
107
|
-
raise RuntimeError("Could not retrieve repository file tree.")
|
|
108
|
-
|
|
109
|
-
temp_dir = tempfile.mkdtemp(prefix=f"xenfra_{repo_name}_")
|
|
110
|
-
print(f" [MCP] Downloading to temporary directory: {temp_dir}")
|
|
111
|
-
|
|
112
|
-
for item in tree:
|
|
113
|
-
item_path = item.get("path")
|
|
114
|
-
item_type = item.get("type")
|
|
115
|
-
|
|
116
|
-
if not item_path or item_type != "blob": # Only handle files
|
|
117
|
-
continue
|
|
118
|
-
|
|
119
|
-
# For downloading content, we can use the commit_sha in the 'ref' parameter
|
|
120
|
-
# to ensure we get the content from the correct version.
|
|
121
|
-
content_result = self._send_request(
|
|
122
|
-
method="repos.get_file_contents",
|
|
123
|
-
params={"owner": owner, "repo": repo_name, "path": item_path, "ref": commit_sha},
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
content_b64 = content_result.get("content")
|
|
127
|
-
if content_b64 is None:
|
|
128
|
-
print(f" [MCP] [Warning] Could not get content for {item_path}")
|
|
129
|
-
continue
|
|
130
|
-
|
|
131
|
-
try:
|
|
132
|
-
# Content is base64 encoded, with newlines.
|
|
133
|
-
decoded_content = base64.b64decode(content_b64.replace("\n", ""))
|
|
134
|
-
except (base64.binascii.Error, TypeError):
|
|
135
|
-
print(f" [MCP] [Warning] Could not decode content for {item_path}")
|
|
136
|
-
continue
|
|
137
|
-
|
|
138
|
-
# Create file and parent directories in the temp location
|
|
139
|
-
local_file_path = Path(temp_dir) / item_path
|
|
140
|
-
local_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
141
|
-
|
|
142
|
-
# Write the file content
|
|
143
|
-
with open(local_file_path, "wb") as f:
|
|
144
|
-
f.write(decoded_content)
|
|
145
|
-
|
|
146
|
-
print(" [MCP] ✅ Repository download complete.")
|
|
147
|
-
return temp_dir
|
|
148
|
-
|
|
149
|
-
def __enter__(self):
|
|
150
|
-
self._start_server()
|
|
151
|
-
return self
|
|
152
|
-
|
|
153
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
154
|
-
self._stop_server()
|
|
1
|
+
# src/xenfra/mcp_client.py
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MCPClient:
|
|
12
|
+
"""
|
|
13
|
+
A client for communicating with a local github-mcp-server process.
|
|
14
|
+
|
|
15
|
+
This client starts the MCP server as a subprocess and interacts with it
|
|
16
|
+
over stdin and stdout to download a full repository to a temporary directory.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, mcp_server_path="github-mcp-server"):
|
|
20
|
+
"""
|
|
21
|
+
Initializes the MCPClient.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
mcp_server_path (str): The path to the github-mcp-server executable.
|
|
25
|
+
Assumes it's in the system's PATH by default.
|
|
26
|
+
"""
|
|
27
|
+
self.mcp_server_path = mcp_server_path
|
|
28
|
+
self.process = None
|
|
29
|
+
|
|
30
|
+
def _start_server(self):
|
|
31
|
+
"""Starts the github-mcp-server subprocess."""
|
|
32
|
+
if self.process and self.process.poll() is None:
|
|
33
|
+
return
|
|
34
|
+
try:
|
|
35
|
+
self.process = subprocess.Popen(
|
|
36
|
+
[self.mcp_server_path],
|
|
37
|
+
stdin=subprocess.PIPE,
|
|
38
|
+
stdout=subprocess.PIPE,
|
|
39
|
+
stderr=subprocess.PIPE,
|
|
40
|
+
text=True,
|
|
41
|
+
env=os.environ,
|
|
42
|
+
)
|
|
43
|
+
except FileNotFoundError:
|
|
44
|
+
raise RuntimeError(
|
|
45
|
+
f"'{self.mcp_server_path}' not found. Ensure github-mcp-server is installed and in your PATH."
|
|
46
|
+
)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
raise RuntimeError(f"Failed to start MCP server: {e}")
|
|
49
|
+
|
|
50
|
+
def _stop_server(self):
|
|
51
|
+
"""Stops the MCP server process."""
|
|
52
|
+
if self.process:
|
|
53
|
+
self.process.terminate()
|
|
54
|
+
self.process.wait()
|
|
55
|
+
self.process = None
|
|
56
|
+
|
|
57
|
+
def _send_request(self, method: str, params: dict) -> dict:
|
|
58
|
+
"""Sends a JSON-RPC request and returns the response."""
|
|
59
|
+
if not self.process or self.process.poll() is not None:
|
|
60
|
+
self._start_server()
|
|
61
|
+
|
|
62
|
+
request = {"jsonrpc": "2.0", "id": os.urandom(4).hex(), "method": method, "params": params}
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
self.process.stdin.write(json.dumps(request) + "\n")
|
|
66
|
+
self.process.stdin.flush()
|
|
67
|
+
response_line = self.process.stdout.readline()
|
|
68
|
+
if not response_line:
|
|
69
|
+
error_output = self.process.stderr.read()
|
|
70
|
+
raise RuntimeError(f"MCP server closed stream unexpectedly. Error: {error_output}")
|
|
71
|
+
|
|
72
|
+
response = json.loads(response_line)
|
|
73
|
+
if "error" in response:
|
|
74
|
+
raise RuntimeError(f"MCP server returned an error: {response['error']}")
|
|
75
|
+
return response.get("result", {})
|
|
76
|
+
except (BrokenPipeError, json.JSONDecodeError) as e:
|
|
77
|
+
raise RuntimeError(f"Failed to communicate with MCP server: {e}")
|
|
78
|
+
|
|
79
|
+
def download_repo_to_tempdir(self, repo_url: str, commit_sha: str = "HEAD") -> str:
|
|
80
|
+
"""
|
|
81
|
+
Downloads an entire repository at a specific commit to a local temporary directory.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
repo_url (str): The full URL of the GitHub repository.
|
|
85
|
+
commit_sha (str): The commit SHA to download. Defaults to "HEAD".
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The path to the temporary directory containing the downloaded code.
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
parts = repo_url.strip("/").split("/")
|
|
92
|
+
owner = parts[-2]
|
|
93
|
+
repo_name = parts[-1].replace(".git", "")
|
|
94
|
+
except IndexError:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
"Invalid repository URL format. Expected format: https://github.com/owner/repo"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
print(f" [MCP] Fetching file tree for {owner}/{repo_name} at {commit_sha}...")
|
|
100
|
+
tree_result = self._send_request(
|
|
101
|
+
method="git.get_repository_tree",
|
|
102
|
+
params={"owner": owner, "repo": repo_name, "tree_sha": commit_sha, "recursive": True},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
tree = tree_result.get("tree", [])
|
|
106
|
+
if not tree:
|
|
107
|
+
raise RuntimeError("Could not retrieve repository file tree.")
|
|
108
|
+
|
|
109
|
+
temp_dir = tempfile.mkdtemp(prefix=f"xenfra_{repo_name}_")
|
|
110
|
+
print(f" [MCP] Downloading to temporary directory: {temp_dir}")
|
|
111
|
+
|
|
112
|
+
for item in tree:
|
|
113
|
+
item_path = item.get("path")
|
|
114
|
+
item_type = item.get("type")
|
|
115
|
+
|
|
116
|
+
if not item_path or item_type != "blob": # Only handle files
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# For downloading content, we can use the commit_sha in the 'ref' parameter
|
|
120
|
+
# to ensure we get the content from the correct version.
|
|
121
|
+
content_result = self._send_request(
|
|
122
|
+
method="repos.get_file_contents",
|
|
123
|
+
params={"owner": owner, "repo": repo_name, "path": item_path, "ref": commit_sha},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
content_b64 = content_result.get("content")
|
|
127
|
+
if content_b64 is None:
|
|
128
|
+
print(f" [MCP] [Warning] Could not get content for {item_path}")
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
# Content is base64 encoded, with newlines.
|
|
133
|
+
decoded_content = base64.b64decode(content_b64.replace("\n", ""))
|
|
134
|
+
except (base64.binascii.Error, TypeError):
|
|
135
|
+
print(f" [MCP] [Warning] Could not decode content for {item_path}")
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
# Create file and parent directories in the temp location
|
|
139
|
+
local_file_path = Path(temp_dir) / item_path
|
|
140
|
+
local_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
|
|
142
|
+
# Write the file content
|
|
143
|
+
with open(local_file_path, "wb") as f:
|
|
144
|
+
f.write(decoded_content)
|
|
145
|
+
|
|
146
|
+
print(" [MCP] ✅ Repository download complete.")
|
|
147
|
+
return temp_dir
|
|
148
|
+
|
|
149
|
+
def __enter__(self):
|
|
150
|
+
self._start_server()
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
154
|
+
self._stop_server()
|