xenfra-sdk 0.1.0__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.0/PKG-INFO +118 -0
- xenfra_sdk-0.1.0/README.md +91 -0
- xenfra_sdk-0.1.0/pyproject.toml +37 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/__init__.py +21 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/cli/__init__.py +0 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/cli/main.py +226 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/client.py +69 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/client_with_hooks.py +275 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/config.py +26 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/db/__init__.py +0 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/db/models.py +27 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/db/session.py +30 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/dependencies.py +38 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/dockerizer.py +87 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/engine.py +388 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/exceptions.py +19 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/mcp_client.py +154 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/models.py +170 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/patterns.json +14 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/privacy.py +118 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/recipes.py +25 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/resources/__init__.py +0 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/resources/base.py +3 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/resources/deployments.py +83 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/resources/intelligence.py +102 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/resources/projects.py +95 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/security.py +41 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/templates/Dockerfile.j2 +25 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/templates/cloud-init.sh.j2 +68 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/templates/docker-compose.yml.j2 +33 -0
- xenfra_sdk-0.1.0/src/xenfra_sdk/utils.py +70 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: xenfra-sdk
|
|
3
|
+
Version: 0.1.0
|
|
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
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "xenfra-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Xenfra SDK: Core engine and utilities for the Xenfra platform."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "xenfra-cloud", email = "xenfracloud@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Topic :: Software Development :: Build Tools",
|
|
17
|
+
"Topic :: System :: Systems Administration",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
dependencies = [
|
|
21
|
+
"fabric>=3.2.2",
|
|
22
|
+
"python-digitalocean>=1.17.0",
|
|
23
|
+
"python-dotenv>=1.2.1",
|
|
24
|
+
"rich>=14.2.0",
|
|
25
|
+
"sqlmodel>=0.0.16",
|
|
26
|
+
"PyYAML>=6.0.1",
|
|
27
|
+
"httpx>=0.27.0",
|
|
28
|
+
"Jinja2>=3.1.3",
|
|
29
|
+
"python-jose[cryptography]>=3.3.0",
|
|
30
|
+
"passlib>=1.7.4",
|
|
31
|
+
"cryptography>=41.0.0",
|
|
32
|
+
]
|
|
33
|
+
requires-python = ">=3.13"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
37
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# This file makes src/xenfra_sdk a Python package.
|
|
2
|
+
|
|
3
|
+
from .client import XenfraClient
|
|
4
|
+
from .exceptions import AuthenticationError, XenfraAPIError, XenfraError
|
|
5
|
+
from .models import (
|
|
6
|
+
CodebaseAnalysisResponse,
|
|
7
|
+
DiagnosisResponse,
|
|
8
|
+
PatchObject,
|
|
9
|
+
ProjectRead,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"XenfraClient",
|
|
14
|
+
"XenfraError",
|
|
15
|
+
"AuthenticationError",
|
|
16
|
+
"XenfraAPIError",
|
|
17
|
+
"DiagnosisResponse",
|
|
18
|
+
"CodebaseAnalysisResponse",
|
|
19
|
+
"PatchObject",
|
|
20
|
+
"ProjectRead",
|
|
21
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import yaml
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
from xenfra_sdk import dockerizer
|
|
6
|
+
from xenfra_sdk.db.session import create_db_and_tables
|
|
7
|
+
from xenfra_sdk.engine import DeploymentError, InfraEngine
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group()
|
|
13
|
+
@click.pass_context
|
|
14
|
+
def main(ctx):
|
|
15
|
+
"""
|
|
16
|
+
Xenfra CLI: A 'Zen Mode' infrastructure engine for Python developers.
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
create_db_and_tables()
|
|
20
|
+
ctx.obj = {"engine": InfraEngine()}
|
|
21
|
+
user_info = ctx.obj["engine"].get_user_info()
|
|
22
|
+
console.print(
|
|
23
|
+
f"[bold underline]Xenfra CLI[/bold underline] - Logged in as [green]{user_info.email}[/green]"
|
|
24
|
+
)
|
|
25
|
+
except Exception as e:
|
|
26
|
+
console.print(f"[bold red]CRITICAL ERROR:[/bold red] Failed to initialize engine: {e}")
|
|
27
|
+
exit(1)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@main.command()
|
|
31
|
+
@click.pass_context
|
|
32
|
+
def init(ctx):
|
|
33
|
+
"""Initializes a project by creating a xenfra.yaml configuration file."""
|
|
34
|
+
console.print("\n[bold blue]🔎 INITIALIZING PROJECT[/bold blue]")
|
|
35
|
+
|
|
36
|
+
framework, _, _ = dockerizer.detect_framework()
|
|
37
|
+
if not framework:
|
|
38
|
+
console.print("[yellow] Warning: No recognizable web framework detected.[/yellow]")
|
|
39
|
+
|
|
40
|
+
console.print(f" - Detected [cyan]{framework or 'unknown'}[/cyan] project.")
|
|
41
|
+
|
|
42
|
+
use_db = click.confirm(
|
|
43
|
+
"\n Would you like to add a PostgreSQL database to your deployment?", default=False
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
config = {
|
|
47
|
+
"name": "xenfra-app",
|
|
48
|
+
"digitalocean": {"region": "nyc3", "size": "s-1vcpu-1gb", "image": "ubuntu-22-04-x64"},
|
|
49
|
+
"app": {"framework": framework},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if use_db:
|
|
53
|
+
config["database"] = {
|
|
54
|
+
"type": "postgres",
|
|
55
|
+
"user": "db_user",
|
|
56
|
+
"password": "db_password", # In a real scenario, this should be handled more securely
|
|
57
|
+
"name": "app_db",
|
|
58
|
+
}
|
|
59
|
+
console.print(" - Added [bold green]PostgreSQL[/bold green] to the configuration.")
|
|
60
|
+
|
|
61
|
+
with open("xenfra.yaml", "w") as f:
|
|
62
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
63
|
+
|
|
64
|
+
console.print("\n[bold green]✅ SUCCESS![/bold green]")
|
|
65
|
+
console.print(" - Created [cyan]xenfra.yaml[/cyan].")
|
|
66
|
+
console.print("\n Next step: Review the configuration and run 'xenfra deploy'!")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@main.command()
|
|
70
|
+
@click.pass_context
|
|
71
|
+
def deploy(ctx):
|
|
72
|
+
"""Deploys the project based on the xenfra.yaml configuration."""
|
|
73
|
+
console.print("\n[bold green]🚀 INITIATING DEPLOYMENT FROM CONFIGURATION[/bold green]")
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
with open("xenfra.yaml", "r") as f:
|
|
77
|
+
config = yaml.safe_load(f)
|
|
78
|
+
except FileNotFoundError:
|
|
79
|
+
raise click.ClickException(
|
|
80
|
+
"No 'xenfra.yaml' found. Run 'xenfra init' to create a configuration file."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
engine = ctx.obj["engine"]
|
|
84
|
+
|
|
85
|
+
# Extract config values
|
|
86
|
+
name = config.get("name", "xenfra-app")
|
|
87
|
+
do_config = config.get("digitalocean", {})
|
|
88
|
+
region = do_config.get("region", "nyc3")
|
|
89
|
+
size = do_config.get("size", "s-1vcpu-1gb")
|
|
90
|
+
image = do_config.get("image", "ubuntu-22-04-x64")
|
|
91
|
+
|
|
92
|
+
# Build context for templates
|
|
93
|
+
template_context = {
|
|
94
|
+
"database": config.get("database", {}).get("type"),
|
|
95
|
+
"db_user": config.get("database", {}).get("user"),
|
|
96
|
+
"db_password": config.get("database", {}).get("password"),
|
|
97
|
+
"db_name": config.get("database", {}).get("name"),
|
|
98
|
+
"email": ctx.obj["engine"].get_user_info().email,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.print(f" - App Name: [cyan]{name}[/cyan]")
|
|
102
|
+
console.print(f" - Region: [cyan]{region}[/cyan], Size: [cyan]{size}[/cyan]")
|
|
103
|
+
if template_context.get("database"):
|
|
104
|
+
console.print(f" - Including Database: [cyan]{template_context['database']}[/cyan]")
|
|
105
|
+
|
|
106
|
+
if not click.confirm(f"\n Ready to deploy '{name}' from 'xenfra.yaml'?"):
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
with console.status("[bold green]Deployment in progress...[/bold green]"):
|
|
110
|
+
result = engine.deploy_server(
|
|
111
|
+
name=name, region=region, size=size, image=image, logger=console.log, **template_context
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
console.print("\n[bold green]✅ DEPLOYMENT COMPLETE![/bold green]")
|
|
115
|
+
console.print(result)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@main.command(name="list")
|
|
119
|
+
@click.option("--refresh", is_flag=True, help="Sync with the cloud provider before listing.")
|
|
120
|
+
@click.pass_context
|
|
121
|
+
def list_projects(ctx, refresh):
|
|
122
|
+
"""Lists all active Xenfra projects from the local database."""
|
|
123
|
+
engine = ctx.obj["engine"]
|
|
124
|
+
|
|
125
|
+
if refresh:
|
|
126
|
+
console.print("\n[bold]📡 SYNCING WITH CLOUD PROVIDER...[/bold]")
|
|
127
|
+
with console.status("Calling DigitalOcean API and reconciling state..."):
|
|
128
|
+
projects = engine.sync_with_provider()
|
|
129
|
+
else:
|
|
130
|
+
console.print("\n[bold]⚡️ LISTING PROJECTS FROM LOCAL DATABASE[/bold]")
|
|
131
|
+
projects = engine.list_projects_from_db()
|
|
132
|
+
|
|
133
|
+
if not projects:
|
|
134
|
+
console.print(
|
|
135
|
+
"[yellow] No active projects found. Run 'xenfra deploy' to create one.[/yellow]"
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
139
|
+
table.add_column("Droplet ID", style="dim", width=12)
|
|
140
|
+
table.add_column("Name", style="cyan")
|
|
141
|
+
table.add_column("IP Address", style="green")
|
|
142
|
+
table.add_column("Status")
|
|
143
|
+
table.add_column("Region")
|
|
144
|
+
table.add_column("Size")
|
|
145
|
+
for p in projects:
|
|
146
|
+
table.add_row(str(p.droplet_id), p.name, p.ip_address, p.status, p.region, p.size)
|
|
147
|
+
console.print(table)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@main.command(name="logs")
|
|
151
|
+
@click.pass_context
|
|
152
|
+
def logs(ctx):
|
|
153
|
+
"""Streams real-time logs from a deployed project."""
|
|
154
|
+
engine = ctx.obj["engine"]
|
|
155
|
+
|
|
156
|
+
console.print("\n[bold yellow]📡 SELECT A PROJECT TO STREAM LOGS[/bold yellow]")
|
|
157
|
+
projects = engine.list_projects_from_db()
|
|
158
|
+
|
|
159
|
+
if not projects:
|
|
160
|
+
console.print("[yellow] No active projects to stream logs from.[/yellow]")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
project_map = {str(i + 1): p for i, p in enumerate(projects)}
|
|
164
|
+
for k, p in project_map.items():
|
|
165
|
+
console.print(f" [{k}] {p.name} ({p.ip_address})")
|
|
166
|
+
|
|
167
|
+
choice_key = click.prompt(
|
|
168
|
+
"\n Select Project (0 to cancel)",
|
|
169
|
+
type=click.Choice(["0"] + list(project_map.keys())),
|
|
170
|
+
show_choices=False,
|
|
171
|
+
)
|
|
172
|
+
if choice_key == "0":
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
target = project_map[choice_key]
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
console.print(
|
|
179
|
+
f"\n[bold green]-- Attaching to logs for {target.name} (Press Ctrl+C to stop) --[/bold green]"
|
|
180
|
+
)
|
|
181
|
+
engine.stream_logs(target.droplet_id)
|
|
182
|
+
except DeploymentError as e:
|
|
183
|
+
console.print(f"[bold red]ERROR:[/bold red] {e.message}")
|
|
184
|
+
except KeyboardInterrupt:
|
|
185
|
+
console.print("\n[bold yellow]-- Log streaming stopped by user. --[/bold yellow]")
|
|
186
|
+
except Exception as e:
|
|
187
|
+
console.print(f"[bold red]An unexpected error occurred:[/bold red] {e}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@main.command()
|
|
191
|
+
@click.pass_context
|
|
192
|
+
def destroy(ctx):
|
|
193
|
+
"""Destroys a deployed project."""
|
|
194
|
+
engine = ctx.obj["engine"]
|
|
195
|
+
|
|
196
|
+
console.print("\n[bold red]🧨 SELECT A PROJECT TO DESTROY[/bold red]")
|
|
197
|
+
projects = engine.list_projects_from_db()
|
|
198
|
+
|
|
199
|
+
if not projects:
|
|
200
|
+
console.print("[yellow] No active projects to destroy.[/yellow]")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
project_map = {str(i + 1): p for i, p in enumerate(projects)}
|
|
204
|
+
for k, p in project_map.items():
|
|
205
|
+
console.print(f" [{k}] {p.name} ({p.ip_address})")
|
|
206
|
+
|
|
207
|
+
choice_key = click.prompt(
|
|
208
|
+
"\n Select Project to DESTROY (0 to cancel)",
|
|
209
|
+
type=click.Choice(["0"] + list(project_map.keys())),
|
|
210
|
+
show_choices=False,
|
|
211
|
+
)
|
|
212
|
+
if choice_key == "0":
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
target = project_map[choice_key]
|
|
216
|
+
|
|
217
|
+
if click.confirm(
|
|
218
|
+
f" Are you SURE you want to permanently delete [red]{target.name}[/red] (Droplet ID: {target.droplet_id})? This action cannot be undone."
|
|
219
|
+
):
|
|
220
|
+
with console.status(f"💥 Destroying {target.name}..."):
|
|
221
|
+
engine.destroy_server(target.droplet_id)
|
|
222
|
+
console.print(f"[green] Project '{target.name}' has been destroyed.[/green]")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
main()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from .exceptions import AuthenticationError, XenfraAPIError, XenfraError
|
|
6
|
+
from .resources.deployments import DeploymentsManager
|
|
7
|
+
from .resources.intelligence import IntelligenceManager
|
|
8
|
+
from .resources.projects import ProjectsManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class XenfraClient:
|
|
12
|
+
def __init__(self, token: str = None, api_url: str = "http://localhost:8000"):
|
|
13
|
+
self.api_url = api_url
|
|
14
|
+
self._token = token or os.getenv("XENFRA_TOKEN")
|
|
15
|
+
if not self._token:
|
|
16
|
+
raise AuthenticationError(
|
|
17
|
+
"No API token provided. Pass it to the client or set XENFRA_TOKEN."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
self._http_client = httpx.Client(
|
|
21
|
+
base_url=self.api_url,
|
|
22
|
+
headers={"Authorization": f"Bearer {self._token}", "Content-Type": "application/json"},
|
|
23
|
+
timeout=30.0, # Add a reasonable timeout
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Track if client is closed
|
|
27
|
+
self._closed = False
|
|
28
|
+
|
|
29
|
+
# Initialize resource managers
|
|
30
|
+
self.projects = ProjectsManager(self)
|
|
31
|
+
self.deployments = DeploymentsManager(self)
|
|
32
|
+
self.intelligence = IntelligenceManager(self)
|
|
33
|
+
|
|
34
|
+
def _request(self, method: str, path: str, json: dict = None) -> httpx.Response:
|
|
35
|
+
"""Internal method to handle all HTTP requests."""
|
|
36
|
+
if self._closed:
|
|
37
|
+
raise XenfraError("Client is closed. Create a new client or use context manager.")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
response = self._http_client.request(method, path, json=json)
|
|
41
|
+
response.raise_for_status() # Raise HTTPStatusError for 4xx/5xx
|
|
42
|
+
return response
|
|
43
|
+
except httpx.HTTPStatusError as e:
|
|
44
|
+
# Convert httpx error to our custom SDK error
|
|
45
|
+
detail = e.response.json().get("detail", e.response.text)
|
|
46
|
+
raise XenfraAPIError(status_code=e.response.status_code, detail=detail) from e
|
|
47
|
+
except httpx.RequestError as e:
|
|
48
|
+
# Handle connection errors, timeouts, etc.
|
|
49
|
+
raise XenfraError(f"HTTP request failed: {e}")
|
|
50
|
+
|
|
51
|
+
def close(self):
|
|
52
|
+
"""Close the HTTP client and cleanup resources."""
|
|
53
|
+
if not self._closed:
|
|
54
|
+
self._http_client.close()
|
|
55
|
+
self._closed = True
|
|
56
|
+
|
|
57
|
+
def __enter__(self):
|
|
58
|
+
"""Context manager entry - allows 'with XenfraClient() as client:' usage."""
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
62
|
+
"""Context manager exit - ensures cleanup."""
|
|
63
|
+
self.close()
|
|
64
|
+
return False # Don't suppress exceptions
|
|
65
|
+
|
|
66
|
+
def __del__(self):
|
|
67
|
+
"""Destructor - cleanup if not already closed."""
|
|
68
|
+
if hasattr(self, '_closed') and not self._closed:
|
|
69
|
+
self.close()
|