xenfra-sdk 0.2.2__py3-none-any.whl → 0.2.3__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 +61 -21
- xenfra_sdk/cli/main.py +226 -226
- xenfra_sdk/client.py +90 -90
- xenfra_sdk/config.py +26 -26
- xenfra_sdk/db/models.py +24 -24
- xenfra_sdk/db/session.py +30 -30
- xenfra_sdk/dependencies.py +39 -39
- xenfra_sdk/detection.py +396 -0
- xenfra_sdk/dockerizer.py +195 -194
- xenfra_sdk/engine.py +741 -619
- xenfra_sdk/exceptions.py +19 -19
- xenfra_sdk/manifest.py +212 -0
- xenfra_sdk/mcp_client.py +154 -154
- xenfra_sdk/models.py +184 -184
- xenfra_sdk/orchestrator.py +666 -0
- xenfra_sdk/patterns.json +13 -13
- xenfra_sdk/privacy.py +153 -153
- xenfra_sdk/recipes.py +26 -26
- xenfra_sdk/resources/base.py +3 -3
- xenfra_sdk/resources/deployments.py +278 -248
- xenfra_sdk/resources/files.py +101 -101
- xenfra_sdk/resources/intelligence.py +102 -95
- xenfra_sdk/security.py +41 -41
- xenfra_sdk/security_scanner.py +431 -0
- xenfra_sdk/templates/Caddyfile.j2 +14 -0
- xenfra_sdk/templates/Dockerfile.j2 +41 -38
- xenfra_sdk/templates/cloud-init.sh.j2 +90 -90
- xenfra_sdk/templates/docker-compose-multi.yml.j2 +29 -0
- xenfra_sdk/templates/docker-compose.yml.j2 +30 -30
- xenfra_sdk-0.2.3.dist-info/METADATA +116 -0
- xenfra_sdk-0.2.3.dist-info/RECORD +38 -0
- xenfra_sdk-0.2.2.dist-info/METADATA +0 -118
- xenfra_sdk-0.2.2.dist-info/RECORD +0 -32
- {xenfra_sdk-0.2.2.dist-info → xenfra_sdk-0.2.3.dist-info}/WHEEL +0 -0
xenfra_sdk/client.py
CHANGED
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
|
-
import httpx
|
|
4
|
-
|
|
5
|
-
from .exceptions import AuthenticationError, XenfraAPIError, XenfraError
|
|
6
|
-
from .resources.deployments import DeploymentsManager
|
|
7
|
-
from .resources.files import FilesManager
|
|
8
|
-
from .resources.intelligence import IntelligenceManager
|
|
9
|
-
from .resources.projects import ProjectsManager
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class XenfraClient:
|
|
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
|
-
|
|
18
|
-
self.api_url = api_url
|
|
19
|
-
self._token = token or os.getenv("XENFRA_TOKEN")
|
|
20
|
-
if not self._token:
|
|
21
|
-
raise AuthenticationError(
|
|
22
|
-
"No API token provided. Pass it to the client or set XENFRA_TOKEN."
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
self._http_client = httpx.Client(
|
|
26
|
-
base_url=self.api_url,
|
|
27
|
-
headers={"Authorization": f"Bearer {self._token}", "Content-Type": "application/json"},
|
|
28
|
-
timeout=30.0, # Add a reasonable timeout
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
# Track if client is closed
|
|
32
|
-
self._closed = False
|
|
33
|
-
|
|
34
|
-
# Initialize resource managers
|
|
35
|
-
self.projects = ProjectsManager(self)
|
|
36
|
-
self.deployments = DeploymentsManager(self)
|
|
37
|
-
self.intelligence = IntelligenceManager(self)
|
|
38
|
-
self.files = FilesManager(self)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _request(self, method: str, path: str, json: dict = None) -> httpx.Response:
|
|
42
|
-
"""Internal method to handle all HTTP requests."""
|
|
43
|
-
if self._closed:
|
|
44
|
-
raise XenfraError("Client is closed. Create a new client or use context manager.")
|
|
45
|
-
|
|
46
|
-
try:
|
|
47
|
-
response = self._http_client.request(method, path, json=json)
|
|
48
|
-
response.raise_for_status() # Raise HTTPStatusError for 4xx/5xx
|
|
49
|
-
return response
|
|
50
|
-
except httpx.HTTPStatusError as e:
|
|
51
|
-
# Convert httpx error to our custom SDK error
|
|
52
|
-
# Safe JSON parsing with fallback
|
|
53
|
-
try:
|
|
54
|
-
content_type = e.response.headers.get("content-type", "")
|
|
55
|
-
if "application/json" in content_type:
|
|
56
|
-
try:
|
|
57
|
-
error_data = e.response.json()
|
|
58
|
-
detail = error_data.get(
|
|
59
|
-
"detail", e.response.text[:500] if e.response.text else "Unknown error"
|
|
60
|
-
)
|
|
61
|
-
except (ValueError, TypeError):
|
|
62
|
-
detail = e.response.text[:500] if e.response.text else "Unknown error"
|
|
63
|
-
else:
|
|
64
|
-
detail = e.response.text[:500] if e.response.text else "Unknown error"
|
|
65
|
-
except Exception:
|
|
66
|
-
detail = "Unknown error"
|
|
67
|
-
raise XenfraAPIError(status_code=e.response.status_code, detail=detail) from e
|
|
68
|
-
except httpx.RequestError as e:
|
|
69
|
-
# Handle connection errors, timeouts, etc.
|
|
70
|
-
raise XenfraError(f"HTTP request failed: {e}")
|
|
71
|
-
|
|
72
|
-
def close(self):
|
|
73
|
-
"""Close the HTTP client and cleanup resources."""
|
|
74
|
-
if not self._closed:
|
|
75
|
-
self._http_client.close()
|
|
76
|
-
self._closed = True
|
|
77
|
-
|
|
78
|
-
def __enter__(self):
|
|
79
|
-
"""Context manager entry - allows 'with XenfraClient() as client:' usage."""
|
|
80
|
-
return self
|
|
81
|
-
|
|
82
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
83
|
-
"""Context manager exit - ensures cleanup."""
|
|
84
|
-
self.close()
|
|
85
|
-
return False # Don't suppress exceptions
|
|
86
|
-
|
|
87
|
-
def __del__(self):
|
|
88
|
-
"""Destructor - cleanup if not already closed."""
|
|
89
|
-
if hasattr(self, "_closed") and not self._closed:
|
|
90
|
-
self.close()
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from .exceptions import AuthenticationError, XenfraAPIError, XenfraError
|
|
6
|
+
from .resources.deployments import DeploymentsManager
|
|
7
|
+
from .resources.files import FilesManager
|
|
8
|
+
from .resources.intelligence import IntelligenceManager
|
|
9
|
+
from .resources.projects import ProjectsManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class XenfraClient:
|
|
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
|
+
|
|
18
|
+
self.api_url = api_url
|
|
19
|
+
self._token = token or os.getenv("XENFRA_TOKEN")
|
|
20
|
+
if not self._token:
|
|
21
|
+
raise AuthenticationError(
|
|
22
|
+
"No API token provided. Pass it to the client or set XENFRA_TOKEN."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
self._http_client = httpx.Client(
|
|
26
|
+
base_url=self.api_url,
|
|
27
|
+
headers={"Authorization": f"Bearer {self._token}", "Content-Type": "application/json"},
|
|
28
|
+
timeout=30.0, # Add a reasonable timeout
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Track if client is closed
|
|
32
|
+
self._closed = False
|
|
33
|
+
|
|
34
|
+
# Initialize resource managers
|
|
35
|
+
self.projects = ProjectsManager(self)
|
|
36
|
+
self.deployments = DeploymentsManager(self)
|
|
37
|
+
self.intelligence = IntelligenceManager(self)
|
|
38
|
+
self.files = FilesManager(self)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _request(self, method: str, path: str, json: dict = None) -> httpx.Response:
|
|
42
|
+
"""Internal method to handle all HTTP requests."""
|
|
43
|
+
if self._closed:
|
|
44
|
+
raise XenfraError("Client is closed. Create a new client or use context manager.")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
response = self._http_client.request(method, path, json=json)
|
|
48
|
+
response.raise_for_status() # Raise HTTPStatusError for 4xx/5xx
|
|
49
|
+
return response
|
|
50
|
+
except httpx.HTTPStatusError as e:
|
|
51
|
+
# Convert httpx error to our custom SDK error
|
|
52
|
+
# Safe JSON parsing with fallback
|
|
53
|
+
try:
|
|
54
|
+
content_type = e.response.headers.get("content-type", "")
|
|
55
|
+
if "application/json" in content_type:
|
|
56
|
+
try:
|
|
57
|
+
error_data = e.response.json()
|
|
58
|
+
detail = error_data.get(
|
|
59
|
+
"detail", e.response.text[:500] if e.response.text else "Unknown error"
|
|
60
|
+
)
|
|
61
|
+
except (ValueError, TypeError):
|
|
62
|
+
detail = e.response.text[:500] if e.response.text else "Unknown error"
|
|
63
|
+
else:
|
|
64
|
+
detail = e.response.text[:500] if e.response.text else "Unknown error"
|
|
65
|
+
except Exception:
|
|
66
|
+
detail = "Unknown error"
|
|
67
|
+
raise XenfraAPIError(status_code=e.response.status_code, detail=detail) from e
|
|
68
|
+
except httpx.RequestError as e:
|
|
69
|
+
# Handle connection errors, timeouts, etc.
|
|
70
|
+
raise XenfraError(f"HTTP request failed: {e}")
|
|
71
|
+
|
|
72
|
+
def close(self):
|
|
73
|
+
"""Close the HTTP client and cleanup resources."""
|
|
74
|
+
if not self._closed:
|
|
75
|
+
self._http_client.close()
|
|
76
|
+
self._closed = True
|
|
77
|
+
|
|
78
|
+
def __enter__(self):
|
|
79
|
+
"""Context manager entry - allows 'with XenfraClient() as client:' usage."""
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
83
|
+
"""Context manager exit - ensures cleanup."""
|
|
84
|
+
self.close()
|
|
85
|
+
return False # Don't suppress exceptions
|
|
86
|
+
|
|
87
|
+
def __del__(self):
|
|
88
|
+
"""Destructor - cleanup if not already closed."""
|
|
89
|
+
if hasattr(self, "_closed") and not self._closed:
|
|
90
|
+
self.close()
|
xenfra_sdk/config.py
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
# src/xenfra/config.py
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Settings(BaseSettings):
|
|
8
|
-
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
|
9
|
-
|
|
10
|
-
SECRET_KEY: str
|
|
11
|
-
ENCRYPTION_KEY: str
|
|
12
|
-
|
|
13
|
-
GH_CLIENT_ID: str
|
|
14
|
-
GH_CLIENT_SECRET: str
|
|
15
|
-
GITHUB_REDIRECT_URI: str
|
|
16
|
-
GITHUB_WEBHOOK_SECRET: str
|
|
17
|
-
|
|
18
|
-
DO_CLIENT_ID: str
|
|
19
|
-
DO_CLIENT_SECRET: str
|
|
20
|
-
DO_REDIRECT_URI: str
|
|
21
|
-
|
|
22
|
-
# Frontend redirect for successful OAuth (e.g., /dashboard/connections)
|
|
23
|
-
FRONTEND_OAUTH_REDIRECT_SUCCESS: str = "/dashboard/connections"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
settings = Settings()
|
|
1
|
+
# src/xenfra/config.py
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Settings(BaseSettings):
|
|
8
|
+
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
|
9
|
+
|
|
10
|
+
SECRET_KEY: str
|
|
11
|
+
ENCRYPTION_KEY: str
|
|
12
|
+
|
|
13
|
+
GH_CLIENT_ID: str
|
|
14
|
+
GH_CLIENT_SECRET: str
|
|
15
|
+
GITHUB_REDIRECT_URI: str
|
|
16
|
+
GITHUB_WEBHOOK_SECRET: str
|
|
17
|
+
|
|
18
|
+
DO_CLIENT_ID: str
|
|
19
|
+
DO_CLIENT_SECRET: str
|
|
20
|
+
DO_REDIRECT_URI: str
|
|
21
|
+
|
|
22
|
+
# Frontend redirect for successful OAuth (e.g., /dashboard/connections)
|
|
23
|
+
FRONTEND_OAUTH_REDIRECT_SUCCESS: str = "/dashboard/connections"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
settings = Settings()
|
xenfra_sdk/db/models.py
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
# src/xenfra/db/models.py
|
|
2
|
-
|
|
3
|
-
from typing import Optional
|
|
4
|
-
|
|
5
|
-
from sqlmodel import Field, SQLModel
|
|
6
|
-
|
|
7
|
-
# --- Project Model for CLI state ---
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class Project(SQLModel, table=True):
|
|
11
|
-
"""
|
|
12
|
-
Project model storing deployment state in the SDK's local database.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
id: Optional[int] = Field(default=None, primary_key=True)
|
|
16
|
-
droplet_id: int = Field(unique=True, index=True)
|
|
17
|
-
name: str
|
|
18
|
-
ip_address: str
|
|
19
|
-
status: str
|
|
20
|
-
region: str
|
|
21
|
-
size: str
|
|
22
|
-
# user_id is a reference to User.id in the SSO service database
|
|
23
|
-
# No foreign key constraint since databases are separate in microservices architecture
|
|
24
|
-
user_id: int = Field(index=True) # Index for query performance
|
|
1
|
+
# src/xenfra/db/models.py
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from sqlmodel import Field, SQLModel
|
|
6
|
+
|
|
7
|
+
# --- Project Model for CLI state ---
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Project(SQLModel, table=True):
|
|
11
|
+
"""
|
|
12
|
+
Project model storing deployment state in the SDK's local database.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
|
16
|
+
droplet_id: int = Field(unique=True, index=True)
|
|
17
|
+
name: str
|
|
18
|
+
ip_address: str
|
|
19
|
+
status: str
|
|
20
|
+
region: str
|
|
21
|
+
size: str
|
|
22
|
+
# user_id is a reference to User.id in the SSO service database
|
|
23
|
+
# No foreign key constraint since databases are separate in microservices architecture
|
|
24
|
+
user_id: int = Field(index=True) # Index for query performance
|
xenfra_sdk/db/session.py
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
# src/xenfra/db/session.py
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
#
|
|
10
|
-
app_dir = Path
|
|
11
|
-
app_dir.mkdir(parents=True, exist_ok=True)
|
|
12
|
-
db_path = app_dir / "xenfra.db"
|
|
13
|
-
|
|
14
|
-
# For now, we will use a simple SQLite database for ease of setup.
|
|
15
|
-
# In production, this should be a PostgreSQL database URL from environment variables.
|
|
16
|
-
DATABASE_URL = os.getenv("DATABASE_URL", f"sqlite:///{db_path}")
|
|
17
|
-
|
|
18
|
-
# Only echo SQL in development (set SQL_ECHO=1 to enable)
|
|
19
|
-
SQL_ECHO = os.getenv("SQL_ECHO", "0") == "1"
|
|
20
|
-
|
|
21
|
-
engine = create_engine(DATABASE_URL, echo=SQL_ECHO)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def create_db_and_tables():
|
|
25
|
-
SQLModel.metadata.create_all(engine)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def get_session():
|
|
29
|
-
with Session(engine) as session:
|
|
30
|
-
yield session
|
|
1
|
+
# src/xenfra/db/session.py
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from sqlmodel import Session, SQLModel, create_engine
|
|
7
|
+
|
|
8
|
+
# Get the app directory for the current user
|
|
9
|
+
# Use ~/.xenfra for cross-platform simplicity and to avoid 'click' dependency in SDK
|
|
10
|
+
app_dir = Path.home() / ".xenfra"
|
|
11
|
+
app_dir.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
db_path = app_dir / "xenfra.db"
|
|
13
|
+
|
|
14
|
+
# For now, we will use a simple SQLite database for ease of setup.
|
|
15
|
+
# In production, this should be a PostgreSQL database URL from environment variables.
|
|
16
|
+
DATABASE_URL = os.getenv("DATABASE_URL", f"sqlite:///{db_path}")
|
|
17
|
+
|
|
18
|
+
# Only echo SQL in development (set SQL_ECHO=1 to enable)
|
|
19
|
+
SQL_ECHO = os.getenv("SQL_ECHO", "0") == "1"
|
|
20
|
+
|
|
21
|
+
engine = create_engine(DATABASE_URL, echo=SQL_ECHO)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_db_and_tables():
|
|
25
|
+
SQLModel.metadata.create_all(engine)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_session():
|
|
29
|
+
with Session(engine) as session:
|
|
30
|
+
yield session
|
xenfra_sdk/dependencies.py
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
# src/xenfra/dependencies.py
|
|
2
|
-
|
|
3
|
-
from fastapi import Depends, HTTPException, status
|
|
4
|
-
from fastapi.security import OAuth2PasswordBearer
|
|
5
|
-
from sqlmodel import Session, select
|
|
6
|
-
|
|
7
|
-
from xenfra.db.models import User
|
|
8
|
-
from xenfra.db.session import get_session
|
|
9
|
-
from xenfra.security import decode_token
|
|
10
|
-
|
|
11
|
-
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def get_current_user(
|
|
15
|
-
token: str = Depends(oauth2_scheme), session: Session = Depends(get_session)
|
|
16
|
-
) -> User:
|
|
17
|
-
credentials_exception = HTTPException(
|
|
18
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
19
|
-
detail="Could not validate credentials",
|
|
20
|
-
headers={"WWW-Authenticate": "Bearer"},
|
|
21
|
-
)
|
|
22
|
-
payload = decode_token(token)
|
|
23
|
-
if payload is None:
|
|
24
|
-
raise credentials_exception
|
|
25
|
-
|
|
26
|
-
email: str = payload.get("sub")
|
|
27
|
-
if email is None:
|
|
28
|
-
raise credentials_exception
|
|
29
|
-
|
|
30
|
-
user = session.exec(select(User).where(User.email == email)).first()
|
|
31
|
-
if user is None:
|
|
32
|
-
raise credentials_exception
|
|
33
|
-
return user
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
|
|
37
|
-
if not current_user.is_active:
|
|
38
|
-
raise HTTPException(status_code=400, detail="Inactive user")
|
|
39
|
-
return current_user
|
|
1
|
+
# src/xenfra/dependencies.py
|
|
2
|
+
|
|
3
|
+
from fastapi import Depends, HTTPException, status
|
|
4
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
5
|
+
from sqlmodel import Session, select
|
|
6
|
+
|
|
7
|
+
from xenfra.db.models import User
|
|
8
|
+
from xenfra.db.session import get_session
|
|
9
|
+
from xenfra.security import decode_token
|
|
10
|
+
|
|
11
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_current_user(
|
|
15
|
+
token: str = Depends(oauth2_scheme), session: Session = Depends(get_session)
|
|
16
|
+
) -> User:
|
|
17
|
+
credentials_exception = HTTPException(
|
|
18
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
19
|
+
detail="Could not validate credentials",
|
|
20
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
21
|
+
)
|
|
22
|
+
payload = decode_token(token)
|
|
23
|
+
if payload is None:
|
|
24
|
+
raise credentials_exception
|
|
25
|
+
|
|
26
|
+
email: str = payload.get("sub")
|
|
27
|
+
if email is None:
|
|
28
|
+
raise credentials_exception
|
|
29
|
+
|
|
30
|
+
user = session.exec(select(User).where(User.email == email)).first()
|
|
31
|
+
if user is None:
|
|
32
|
+
raise credentials_exception
|
|
33
|
+
return user
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
|
|
37
|
+
if not current_user.is_active:
|
|
38
|
+
raise HTTPException(status_code=400, detail="Inactive user")
|
|
39
|
+
return current_user
|