xenfra 0.1.6__py3-none-any.whl → 0.1.8__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/api/auth.py +51 -0
- xenfra/api/billing.py +80 -0
- xenfra/api/connections.py +163 -0
- xenfra/api/main.py +175 -0
- xenfra/api/webhooks.py +146 -0
- xenfra/cli/main.py +211 -0
- xenfra/config.py +24 -0
- xenfra/db/models.py +51 -0
- xenfra/db/session.py +17 -0
- xenfra/dependencies.py +35 -0
- xenfra/dockerizer.py +45 -109
- xenfra/engine.py +228 -229
- xenfra/mcp_client.py +149 -0
- xenfra/models.py +54 -0
- xenfra/recipes.py +20 -118
- xenfra/security.py +58 -0
- xenfra/templates/Dockerfile.j2 +25 -0
- xenfra/templates/cloud-init.sh.j2 +72 -0
- xenfra/templates/docker-compose.yml.j2 +33 -0
- {xenfra-0.1.6.dist-info → xenfra-0.1.8.dist-info}/METADATA +30 -63
- xenfra-0.1.8.dist-info/RECORD +25 -0
- xenfra-0.1.8.dist-info/entry_points.txt +3 -0
- xenfra/cli.py +0 -169
- xenfra-0.1.6.dist-info/RECORD +0 -10
- xenfra-0.1.6.dist-info/entry_points.txt +0 -3
- {xenfra-0.1.6.dist-info → xenfra-0.1.8.dist-info}/WHEEL +0 -0
xenfra/models.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
class DeploymentStatus(str, Enum):
|
|
6
|
+
PENDING = "pending"
|
|
7
|
+
IN_PROGRESS = "in_progress"
|
|
8
|
+
SUCCESS = "success"
|
|
9
|
+
FAILED = "failed"
|
|
10
|
+
|
|
11
|
+
class SourceType(str, Enum):
|
|
12
|
+
LOCAL = "local"
|
|
13
|
+
GIT = "git"
|
|
14
|
+
|
|
15
|
+
class Deployment(BaseModel):
|
|
16
|
+
id: str = Field(..., description="Unique identifier for the deployment")
|
|
17
|
+
projectId: str = Field(..., description="Identifier of the project being deployed")
|
|
18
|
+
status: DeploymentStatus = Field(..., description="Current status of the deployment")
|
|
19
|
+
source: str = Field(..., description="Source of the deployment (e.g., 'cli', 'api')")
|
|
20
|
+
created_at: datetime = Field(..., description="Timestamp when the deployment was created")
|
|
21
|
+
finished_at: datetime | None = Field(None, description="Timestamp when the deployment finished")
|
|
22
|
+
|
|
23
|
+
class DeploymentRecord(BaseModel):
|
|
24
|
+
deployment_id: str = Field(..., description="Unique identifier for this deployment instance.")
|
|
25
|
+
timestamp: datetime = Field(..., description="Timestamp of when the deployment succeeded.")
|
|
26
|
+
source_type: SourceType = Field(..., description="The type of the source code (local or git).")
|
|
27
|
+
source_identifier: str = Field(..., description="The identifier for the source (commit SHA for git, archive path for local).")
|
|
28
|
+
|
|
29
|
+
class BalanceRead(BaseModel):
|
|
30
|
+
month_to_date_balance: str
|
|
31
|
+
account_balance: str
|
|
32
|
+
month_to_date_usage: str
|
|
33
|
+
generated_at: str
|
|
34
|
+
error: str | None = None
|
|
35
|
+
|
|
36
|
+
class DropletCostRead(BaseModel):
|
|
37
|
+
id: int
|
|
38
|
+
name: str
|
|
39
|
+
ip_address: str
|
|
40
|
+
status: str
|
|
41
|
+
size_slug: str
|
|
42
|
+
monthly_price: float
|
|
43
|
+
|
|
44
|
+
class ProjectRead(BaseModel):
|
|
45
|
+
id: int
|
|
46
|
+
name: str
|
|
47
|
+
ip_address: str | None = None
|
|
48
|
+
status: str
|
|
49
|
+
region: str
|
|
50
|
+
size_slug: str
|
|
51
|
+
estimated_monthly_cost: float | None = None
|
|
52
|
+
created_at: datetime
|
|
53
|
+
|
|
54
|
+
|
xenfra/recipes.py
CHANGED
|
@@ -1,121 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
from jinja2 import Environment, FileSystemLoader
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
def generate_stack(context: dict):
|
|
2
5
|
"""
|
|
3
|
-
Generates a startup script.
|
|
4
|
-
|
|
6
|
+
Generates a cloud-init startup script from a Jinja2 template.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
context: A dictionary containing information for rendering the template,
|
|
10
|
+
e.g., {'domain': 'example.com', 'email': 'user@example.com'}
|
|
5
11
|
"""
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# Create App Directory
|
|
19
|
-
mkdir -p /root/app
|
|
20
|
-
cd /root/app
|
|
21
|
-
|
|
22
|
-
# --- AGGRESSIVE FIX: KILL BACKGROUND UPDATES ---
|
|
23
|
-
echo "⚔️ [0/6] Stopping Background Updates..." >> $LOG
|
|
24
|
-
systemctl stop unattended-upgrades.service
|
|
25
|
-
systemctl stop apt-daily.service
|
|
26
|
-
systemctl stop apt-daily-upgrade.service
|
|
27
|
-
systemctl kill --kill-who=all apt-daily.service
|
|
28
|
-
systemctl kill --kill-who=all apt-daily-upgrade.service
|
|
29
|
-
|
|
30
|
-
# Force remove locks if they exist (The Nuclear Option)
|
|
31
|
-
rm -f /var/lib/dpkg/lock*
|
|
32
|
-
rm -f /var/lib/apt/lists/lock
|
|
33
|
-
rm -f /var/cache/apt/archives/lock
|
|
34
|
-
dpkg --configure -a
|
|
35
|
-
# -----------------------------------------------
|
|
36
|
-
|
|
37
|
-
# 1. System Updates
|
|
38
|
-
echo "🔄 [1/6] Refreshing Package Lists..." >> $LOG
|
|
39
|
-
apt-get update
|
|
40
|
-
apt-get install -y python3-pip python3-venv git curl
|
|
41
|
-
|
|
42
|
-
# 2. Install Docker & Compose
|
|
43
|
-
echo "🐳 [2/6] Installing Docker..." >> $LOG
|
|
44
|
-
apt-get install -y docker.io || (curl -fsSL https://get.docker.com | sh)
|
|
45
|
-
echo "🎶 [3/6] Installing Docker Compose..." >> $LOG
|
|
46
|
-
apt-get install -y docker-compose-v2
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
if is_dockerized:
|
|
50
|
-
script += """
|
|
51
|
-
# --- DOCKERIZED DEPLOYMENT ---
|
|
52
|
-
echo "📦 [4/6] Installing Caddy..." >> $LOG
|
|
53
|
-
apt-get install -y debian-keyring debian-archive-keyring apt-transport-https
|
|
54
|
-
curl -LsSf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
|
55
|
-
curl -LsSf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
|
|
56
|
-
apt-get update
|
|
57
|
-
apt-get install -y caddy
|
|
58
|
-
|
|
59
|
-
# --- Stop and disable global Caddy to prevent port conflicts ---
|
|
60
|
-
echo "🛑 Stopping global Caddy service to free up ports..." >> $LOG
|
|
61
|
-
systemctl stop caddy || true
|
|
62
|
-
systemctl disable caddy || true
|
|
63
|
-
# ------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
echo "🚀 [5/6] Starting application with Docker Compose..." >> $LOG
|
|
66
|
-
cd /root/app
|
|
67
|
-
docker-compose -f /root/app/docker-compose.yml up -d
|
|
68
|
-
|
|
69
|
-
echo "🔒 [6/6] Reloading Caddy..." >> $LOG
|
|
70
|
-
# Caddy will automatically use the Caddyfile from the mounted volume in docker-compose
|
|
71
|
-
# We just need to make sure it's running. Docker-compose handles this.
|
|
72
|
-
# A system reload might be useful if Caddy was installed as a host service.
|
|
73
|
-
systemctl reload caddy || systemctl start caddy
|
|
74
|
-
"""
|
|
75
|
-
else:
|
|
76
|
-
# 3. Virtual Environment Setup (Non-Dockerized)
|
|
77
|
-
if project_type == "uv":
|
|
78
|
-
script += """
|
|
79
|
-
# --- STANDARD NON-DOCKER DEPLOYMENT (UV) ---
|
|
80
|
-
echo "⚡ [4/6] Installing UV..." >> $LOG
|
|
81
|
-
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
82
|
-
source $HOME/.local/bin/env
|
|
83
|
-
export PATH="/root/.local/bin:$PATH"
|
|
84
|
-
|
|
85
|
-
echo "⚡ [5/6] Creating UV Venv..." >> $LOG
|
|
86
|
-
uv venv /root/venv
|
|
87
|
-
source /root/venv/bin/activate
|
|
88
|
-
"""
|
|
89
|
-
else:
|
|
90
|
-
script += """
|
|
91
|
-
# --- STANDARD NON-DOCKER DEPLOYMENT (PIP) ---
|
|
92
|
-
echo "🐍 [4/6] Creating Python Venv..." >> $LOG
|
|
93
|
-
python3 -m venv /root/venv
|
|
94
|
-
source /root/venv/bin/activate
|
|
95
|
-
echo "🐍 [5/6] Upgrading Pip..." >> $LOG
|
|
96
|
-
pip install --upgrade pip
|
|
97
|
-
"""
|
|
98
|
-
# 4. Install Packages (Non-Dockerized)
|
|
99
|
-
if packages:
|
|
100
|
-
if project_type == "uv":
|
|
101
|
-
script += f"""
|
|
102
|
-
echo "📦 [6/6] UV Installing Deps: {packages}..." >> $LOG
|
|
103
|
-
source /root/venv/bin/activate
|
|
104
|
-
uv pip install {packages}
|
|
105
|
-
"""
|
|
106
|
-
else:
|
|
107
|
-
script += f"""
|
|
108
|
-
echo "📦 [6/6] Pip Installing Deps: {packages}..." >> $LOG
|
|
109
|
-
source /root/venv/bin/activate
|
|
110
|
-
pip install {packages}
|
|
111
|
-
"""
|
|
112
|
-
else:
|
|
113
|
-
script += 'echo "ℹ️ [6/6] No packages detected." >> $LOG\n'
|
|
114
|
-
|
|
115
|
-
# Finish
|
|
116
|
-
script += """
|
|
117
|
-
|
|
118
|
-
echo "✅ SETUP COMPLETE" >> $LOG
|
|
119
|
-
touch /root/setup_complete
|
|
120
|
-
"""
|
|
12
|
+
# Path to the templates directory
|
|
13
|
+
template_dir = Path(__file__).parent / "templates"
|
|
14
|
+
env = Environment(loader=FileSystemLoader(template_dir))
|
|
15
|
+
|
|
16
|
+
template = env.get_template("cloud-init.sh.j2")
|
|
17
|
+
|
|
18
|
+
# The non-dockerized logic has been removed as we are focusing on
|
|
19
|
+
# a purely Docker-based deployment strategy for simplicity and scalability.
|
|
20
|
+
# The context will contain all necessary variables for the template.
|
|
21
|
+
script = template.render(context)
|
|
22
|
+
|
|
121
23
|
return script
|
xenfra/security.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# src/xenfra/security.py
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from passlib.context import CryptContext
|
|
6
|
+
from jose import JWTError, jwt
|
|
7
|
+
from cryptography.fernet import Fernet
|
|
8
|
+
|
|
9
|
+
from xenfra.config import settings
|
|
10
|
+
|
|
11
|
+
# --- Configuration ---
|
|
12
|
+
SECRET_KEY = settings.SECRET_KEY
|
|
13
|
+
ALGORITHM = "HS256"
|
|
14
|
+
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
|
15
|
+
|
|
16
|
+
# This key MUST be 32 url-safe base64-encoded bytes.
|
|
17
|
+
fernet = Fernet(settings.ENCRYPTION_KEY.encode())
|
|
18
|
+
|
|
19
|
+
# --- Password Hashing ---
|
|
20
|
+
# Explicitly use passlib.backends.bcrypt
|
|
21
|
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
22
|
+
|
|
23
|
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
24
|
+
return pwd_context.verify(plain_password, hashed_password)
|
|
25
|
+
|
|
26
|
+
def get_password_hash(password: str) -> str:
|
|
27
|
+
# bcrypt passwords cannot be longer than 72 bytes. Truncate if necessary.
|
|
28
|
+
# Note: Frontend should also enforce password length limits.
|
|
29
|
+
if len(password.encode('utf-8')) > 72:
|
|
30
|
+
password = password[:72]
|
|
31
|
+
return pwd_context.hash(password)
|
|
32
|
+
|
|
33
|
+
# --- JWT Handling ---
|
|
34
|
+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
|
35
|
+
to_encode = data.copy()
|
|
36
|
+
if expires_delta:
|
|
37
|
+
expire = datetime.now(timezone.utc) + expires_delta
|
|
38
|
+
else:
|
|
39
|
+
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
40
|
+
to_encode.update({"exp": expire})
|
|
41
|
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
|
42
|
+
return encoded_jwt
|
|
43
|
+
|
|
44
|
+
def decode_token(token: str) -> Optional[dict]:
|
|
45
|
+
try:
|
|
46
|
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
|
47
|
+
return payload
|
|
48
|
+
except JWTError:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
# --- Token Encryption ---
|
|
52
|
+
def encrypt_token(token: str) -> str:
|
|
53
|
+
"""Encrypts a token using Fernet symmetric encryption."""
|
|
54
|
+
return fernet.encrypt(token.encode()).decode()
|
|
55
|
+
|
|
56
|
+
def decrypt_token(encrypted_token: str) -> str:
|
|
57
|
+
"""Decrypts a token."""
|
|
58
|
+
return fernet.decrypt(encrypted_token.encode()).decode()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Dockerfile template for Python web applications
|
|
2
|
+
FROM {{ python_version | default('python:3.11-slim') }}
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
# Install uv, our preferred package manager
|
|
7
|
+
RUN apt-get update && apt-get install -y curl && \
|
|
8
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
|
9
|
+
apt-get remove -y curl && \
|
|
10
|
+
apt-get clean && \
|
|
11
|
+
rm -rf /var/lib/apt/lists/*
|
|
12
|
+
|
|
13
|
+
COPY requirements.txt .
|
|
14
|
+
|
|
15
|
+
# Install dependencies
|
|
16
|
+
RUN /root/.cargo/bin/uv pip install --system --no-cache -r requirements.txt
|
|
17
|
+
|
|
18
|
+
COPY . .
|
|
19
|
+
|
|
20
|
+
# Expose the application port
|
|
21
|
+
EXPOSE {{ port | default(8000) }}
|
|
22
|
+
|
|
23
|
+
# The command to run the application will be in docker-compose.yml
|
|
24
|
+
# This allows for more flexibility
|
|
25
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
3
|
+
LOG="/root/setup.log"
|
|
4
|
+
touch $LOG
|
|
5
|
+
|
|
6
|
+
echo "--------------------------------" >> $LOG
|
|
7
|
+
echo "🧘 XENFRA: Context-Aware Boot" >> $LOG
|
|
8
|
+
echo "--------------------------------" >> $LOG
|
|
9
|
+
|
|
10
|
+
# Create App Directory
|
|
11
|
+
mkdir -p /root/app
|
|
12
|
+
cd /root/app
|
|
13
|
+
|
|
14
|
+
# --- AGGRESSIVE FIX: KILL BACKGROUND UPDATES ---
|
|
15
|
+
echo "⚔️ [0/6] Stopping Background Updates..." >> $LOG
|
|
16
|
+
systemctl stop unattended-upgrades.service || true
|
|
17
|
+
systemctl stop apt-daily.service || true
|
|
18
|
+
systemctl stop apt-daily-upgrade.service || true
|
|
19
|
+
systemctl kill --kill-who=all apt-daily.service || true
|
|
20
|
+
systemctl kill --kill-who=all apt-daily-upgrade.service || true
|
|
21
|
+
|
|
22
|
+
# Force remove locks if they exist
|
|
23
|
+
rm -f /var/lib/dpkg/lock*
|
|
24
|
+
rm -f /var/lib/apt/lists/lock
|
|
25
|
+
rm -f /var/cache/apt/archives/lock
|
|
26
|
+
dpkg --configure -a || true
|
|
27
|
+
# -----------------------------------------------
|
|
28
|
+
|
|
29
|
+
# 1. System Updates
|
|
30
|
+
echo "🔄 [1/6] Refreshing Package Lists..." >> $LOG
|
|
31
|
+
apt-get update
|
|
32
|
+
apt-get install -y python3-pip git curl
|
|
33
|
+
|
|
34
|
+
# 2. Install Docker & Compose
|
|
35
|
+
echo "🐳 [2/6] Installing Docker..." >> $LOG
|
|
36
|
+
apt-get install -y docker.io || (curl -fsSL https://get.docker.com | sh)
|
|
37
|
+
echo "🎶 [3/6] Installing Docker Compose..." >> $LOG
|
|
38
|
+
apt-get install -y docker-compose-v2
|
|
39
|
+
|
|
40
|
+
# --- DOCKERIZED DEPLOYMENT ---
|
|
41
|
+
echo "📦 [4/6] Installing Caddy..." >> $LOG
|
|
42
|
+
apt-get install -y debian-keyring debian-archive-keyring apt-transport-https
|
|
43
|
+
curl -LsSf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
|
44
|
+
curl -LsSf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
|
|
45
|
+
apt-get update
|
|
46
|
+
apt-get install -y caddy
|
|
47
|
+
|
|
48
|
+
{% if domain %}
|
|
49
|
+
# Dynamically generate Caddyfile content
|
|
50
|
+
echo "🔒 Writing Caddyfile for {{ domain }}..." >> $LOG
|
|
51
|
+
cat << EOF > /etc/caddy/Caddyfile
|
|
52
|
+
{{ domain }}:80, {{ domain }}:443 {
|
|
53
|
+
reverse_proxy localhost:{{ port | default(8000) }}
|
|
54
|
+
tls {{ email }}
|
|
55
|
+
}
|
|
56
|
+
EOF
|
|
57
|
+
{% endif %}
|
|
58
|
+
|
|
59
|
+
echo "🚀 [5/6] Starting application with Docker Compose..." >> $LOG
|
|
60
|
+
cd /root/app
|
|
61
|
+
docker-compose -f /root/app/docker-compose.yml up -d
|
|
62
|
+
|
|
63
|
+
{% if domain %}
|
|
64
|
+
echo "🔒 [6/6] Starting Caddy..." >> $LOG
|
|
65
|
+
systemctl restart caddy
|
|
66
|
+
{% else %}
|
|
67
|
+
echo "✅ [6/6] Skipping Caddy start (no domain specified)." >> $LOG
|
|
68
|
+
{% endif %}
|
|
69
|
+
|
|
70
|
+
# Finish
|
|
71
|
+
echo "✅ SETUP SCRIPT COMPLETE" >> $LOG
|
|
72
|
+
touch /root/setup_complete
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# docker-compose.yml template
|
|
2
|
+
version: '3.8'
|
|
3
|
+
|
|
4
|
+
services:
|
|
5
|
+
app:
|
|
6
|
+
build: .
|
|
7
|
+
ports:
|
|
8
|
+
- "{{ port | default(8000) }}:{{ port | default(8000) }}"
|
|
9
|
+
volumes:
|
|
10
|
+
- .:/app
|
|
11
|
+
command: {{ command }}
|
|
12
|
+
{% if database == 'postgres' %}
|
|
13
|
+
depends_on:
|
|
14
|
+
- db
|
|
15
|
+
environment:
|
|
16
|
+
- DATABASE_URL=postgresql://{{ db_user | default('user') }}:{{ db_password | default('password') }}@db:5432/{{ db_name | default('appdb') }}
|
|
17
|
+
{% endif %}
|
|
18
|
+
|
|
19
|
+
{% if database == 'postgres' %}
|
|
20
|
+
db:
|
|
21
|
+
image: postgres:15-alpine
|
|
22
|
+
volumes:
|
|
23
|
+
- postgres_data:/var/lib/postgresql/data/
|
|
24
|
+
environment:
|
|
25
|
+
- POSTGRES_USER={{ db_user | default('user') }}
|
|
26
|
+
- POSTGRES_PASSWORD={{ db_password | default('password') }}
|
|
27
|
+
- POSTGRES_DB={{ db_name | default('appdb') }}
|
|
28
|
+
{% endif %}
|
|
29
|
+
|
|
30
|
+
volumes:
|
|
31
|
+
{% if database == 'postgres' %}
|
|
32
|
+
postgres_data:
|
|
33
|
+
{% endif %}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: xenfra
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: A 'Zen Mode' infrastructure engine for Python developers.
|
|
5
5
|
Author: xenfra-cloud
|
|
6
6
|
Author-email: xenfra-cloud <xenfracloud@gmail.com>
|
|
@@ -15,6 +15,19 @@ Requires-Dist: fabric>=3.2.2
|
|
|
15
15
|
Requires-Dist: python-digitalocean>=1.17.0
|
|
16
16
|
Requires-Dist: python-dotenv>=1.2.1
|
|
17
17
|
Requires-Dist: rich>=14.2.0
|
|
18
|
+
Requires-Dist: fastapi>=0.110.0
|
|
19
|
+
Requires-Dist: uvicorn[standard]>=0.27.1
|
|
20
|
+
Requires-Dist: click>=8.1.7
|
|
21
|
+
Requires-Dist: sqlmodel>=0.0.16
|
|
22
|
+
Requires-Dist: psycopg2-binary>=2.9.9
|
|
23
|
+
Requires-Dist: python-jose[cryptography]>=3.3.0
|
|
24
|
+
Requires-Dist: passlib>=1.7.4
|
|
25
|
+
Requires-Dist: httpx>=0.27.0
|
|
26
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
27
|
+
Requires-Dist: python-multipart>=0.0.21
|
|
28
|
+
Requires-Dist: bcrypt==4.0.1
|
|
29
|
+
Requires-Dist: jinja2>=3.1.3
|
|
30
|
+
Requires-Dist: pytest>=9.0.2
|
|
18
31
|
Requires-Dist: pytest>=8.0.0 ; extra == 'test'
|
|
19
32
|
Requires-Dist: pytest-mock>=3.12.0 ; extra == 'test'
|
|
20
33
|
Requires-Python: >=3.13
|
|
@@ -35,83 +48,37 @@ It handles the complexity of server provisioning, context-aware configuration, D
|
|
|
35
48
|
* **Clients as the Face**: Frontends like the default CLI (`xenfra.cli`) are thin, stateless clients responsible only for user interaction.
|
|
36
49
|
* **Zen Mode**: If a server setup fails due to common issues like a locked package manager, the Engine automatically fixes it without exposing raw errors to the user.
|
|
37
50
|
|
|
38
|
-
## 🚀 Quickstart
|
|
51
|
+
## 🚀 Quickstart
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
Using the Xenfra CLI involves a simple workflow: **Configure**, **Initialize**, and then **Deploy & Manage**.
|
|
41
54
|
|
|
42
|
-
### 1.
|
|
55
|
+
### 1. Configure
|
|
43
56
|
|
|
44
|
-
|
|
45
|
-
# Install from PyPI (once published)
|
|
46
|
-
pip install xenfra
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### 2. Prerequisites
|
|
50
|
-
|
|
51
|
-
Ensure your DigitalOcean API token is available as an environment variable:
|
|
57
|
+
Xenfra needs your DigitalOcean API token to manage infrastructure on your behalf. Export it as an environment variable:
|
|
52
58
|
|
|
53
59
|
```bash
|
|
54
|
-
export DIGITAL_OCEAN_TOKEN="
|
|
60
|
+
export DIGITAL_OCEAN_TOKEN="dop_v1_your_secret_token_here"
|
|
55
61
|
```
|
|
56
62
|
|
|
57
|
-
###
|
|
58
|
-
|
|
59
|
-
```python
|
|
60
|
-
from xenfra.engine import InfraEngine
|
|
63
|
+
### 2. Initialize
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
def my_logger(message):
|
|
64
|
-
print(f"[My App] {message}")
|
|
65
|
-
|
|
66
|
-
try:
|
|
67
|
-
# The engine automatically finds and validates the API token
|
|
68
|
-
engine = InfraEngine()
|
|
69
|
-
|
|
70
|
-
# Define the server and deploy
|
|
71
|
-
result = engine.deploy_server(
|
|
72
|
-
name="my-app-server",
|
|
73
|
-
region="nyc3",
|
|
74
|
-
size="s-1vcpu-1gb",
|
|
75
|
-
image="ubuntu-24-04-x64",
|
|
76
|
-
logger=my_logger
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
print(f"🎉 Deployment successful! IP Address: {result.get('ip')}")
|
|
80
|
-
|
|
81
|
-
except Exception as e:
|
|
82
|
-
print(f"❌ Deployment failed: {e}")
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## 💻 CLI Usage
|
|
86
|
-
|
|
87
|
-
Xenfra also provides a beautiful, interactive CLI for manual control.
|
|
88
|
-
|
|
89
|
-
### 1. Installation
|
|
65
|
+
Navigate to your project's root directory and run the `init` command. This command scans your project, asks a few questions, and creates a `xenfra.yaml` configuration file.
|
|
90
66
|
|
|
91
67
|
```bash
|
|
92
|
-
|
|
68
|
+
xenfra init
|
|
93
69
|
```
|
|
70
|
+
You should review the generated `xenfra.yaml` and commit it to your repository.
|
|
94
71
|
|
|
95
|
-
###
|
|
72
|
+
### 3. Deploy & Manage
|
|
96
73
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
```env
|
|
100
|
-
DIGITAL_OCEAN_TOKEN=dop_v1_your_token_here
|
|
101
|
-
```
|
|
74
|
+
Once your project is initialized, you can use the following commands to manage your application:
|
|
102
75
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
xenfra
|
|
109
|
-
```
|
|
76
|
+
* **`xenfra deploy`**: Deploys your application based on the settings in `xenfra.yaml`.
|
|
77
|
+
* **`xenfra list`**: Instantly lists all your deployed projects from a local cache.
|
|
78
|
+
* Use `xenfra list --refresh` to force a sync with your cloud provider.
|
|
79
|
+
* **`xenfra logs`**: Streams real-time logs from a selected project.
|
|
80
|
+
* **`xenfra destroy`**: Decommissions and deletes a deployed project.
|
|
110
81
|
|
|
111
|
-
This will launch the interactive menu where you can:
|
|
112
|
-
- **🚀 Deploy New Server**: A guided workflow to provision and deploy your application.
|
|
113
|
-
- **📋 List Active Servers**: View your current DigitalOcean droplets.
|
|
114
|
-
- **🧨 Destroy a Server**: Decommission servers you no longer need.
|
|
115
82
|
|
|
116
83
|
## 📦 Supported Frameworks & Features
|
|
117
84
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
xenfra/__init__.py,sha256=ja0-Vc9T61EXZnPeX84Fi-UOjoGAWu5r90_1t-0WjVw,46
|
|
2
|
+
xenfra/api/auth.py,sha256=WxepMKeF8Xnf9imqYip_EyGT0xFXgKoAhFIYx7uqWvA,1848
|
|
3
|
+
xenfra/api/billing.py,sha256=hoviKgpzjMoWcz0KW7I5HUhwLKPlRov5-3TIqzbdpAs,2871
|
|
4
|
+
xenfra/api/connections.py,sha256=IQfOA9jZV6Gcz0_E69Sgp3poAMQKwGwn19GV84Sn61I,7328
|
|
5
|
+
xenfra/api/main.py,sha256=psdVAOKsSM9vMmTSdZBuGceJNG50mqtJFbfa3UrTpmo,6392
|
|
6
|
+
xenfra/api/webhooks.py,sha256=Ek--AVzzKxvukway-vLt_wprBMaQjuWy7q35BbUKeiA,6367
|
|
7
|
+
xenfra/cli/main.py,sha256=1RiznGbrlaSDw37nD8i7wrpXMQsHnNuqk1YOYzxWdII,7841
|
|
8
|
+
xenfra/config.py,sha256=g7ahyI4kbYITRcliCbzXCRfKJ3uh8E2VMjmpR-n51s8,621
|
|
9
|
+
xenfra/db/models.py,sha256=nRqbG9cbcX8LWz2Jr4inKysAq47yALcxNl3_UYEpSoQ,1305
|
|
10
|
+
xenfra/db/session.py,sha256=Q03RIiPlYBtPD-qVHlTqQtacnOPTZnOy6_f9p6lABPU,510
|
|
11
|
+
xenfra/dependencies.py,sha256=pQ7bFNZHC24chMxRa_69mu2fIqcoc8DG9MD1POSGgjA,1211
|
|
12
|
+
xenfra/dockerizer.py,sha256=5qzgqw2MZrBwI4IFzXPJZ6k3F3IRdFnX-KGIpv5cjXc,3126
|
|
13
|
+
xenfra/engine.py,sha256=fzdBIW1CmOQtTETjlaMtltt0cgH8r4HN7ImH-s2Wnzk,11793
|
|
14
|
+
xenfra/mcp_client.py,sha256=IxkTC3NPHvPJn-2dl5BUHQ3d-A9MBiKP9fIftzhmCuA,5756
|
|
15
|
+
xenfra/models.py,sha256=9Dvr1_kb29LJIR0a-uwx_tsCWt3GZeko6FKQtYY8F4g,1852
|
|
16
|
+
xenfra/recipes.py,sha256=q5ilpDzGuUM6GyvlW97pOpPS9nzQAJGI4pgzSU_URik,862
|
|
17
|
+
xenfra/security.py,sha256=w7aIFnHDHOH2zDjYD_LnJnSE01NeGeoAh_V8hC1HipY,2003
|
|
18
|
+
xenfra/templates/Dockerfile.j2,sha256=apWts895OOoUYwj_fOa6OiylFB5m8zFEYvJ1Nki32YM,664
|
|
19
|
+
xenfra/templates/cloud-init.sh.j2,sha256=Ln7XO4rwaHgauevMKfkxAgBFNLHPnWv-TnJl3NJbU8c,2359
|
|
20
|
+
xenfra/templates/docker-compose.yml.j2,sha256=zKUT2cd_FrxXvRxE-vAAjuQk3-nLNQjRe-StkhAWRQA,860
|
|
21
|
+
xenfra/utils.py,sha256=aGXjJm-pwVCHuCn5UBdrxRcYvM8aJwHQ1kihl7gcxiM,2387
|
|
22
|
+
xenfra-0.1.8.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
|
|
23
|
+
xenfra-0.1.8.dist-info/entry_points.txt,sha256=O6JNPm-inoMzmW29WPYl6jHHlWCrcs8ShHEo-CXvERs,49
|
|
24
|
+
xenfra-0.1.8.dist-info/METADATA,sha256=hebQ0mFii9qF8Gs7PeVgzbBizBZ8qQ7GuFA0FycJTT4,4128
|
|
25
|
+
xenfra-0.1.8.dist-info/RECORD,,
|