xenfra 0.2.5__py3-none-any.whl → 0.2.7__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/commands/auth.py +12 -10
- xenfra/main.py +7 -0
- xenfra/utils/auth.py +1 -1
- xenfra/utils/codebase.py +2 -0
- xenfra/utils/config.py +9 -2
- xenfra/utils/security.py +2 -2
- xenfra/utils/validation.py +8 -3
- {xenfra-0.2.5.dist-info → xenfra-0.2.7.dist-info}/METADATA +1 -1
- xenfra-0.2.7.dist-info/RECORD +18 -0
- xenfra-0.2.5.dist-info/RECORD +0 -18
- {xenfra-0.2.5.dist-info → xenfra-0.2.7.dist-info}/WHEEL +0 -0
- {xenfra-0.2.5.dist-info → xenfra-0.2.7.dist-info}/entry_points.txt +0 -0
xenfra/commands/auth.py
CHANGED
|
@@ -15,9 +15,9 @@ import keyring
|
|
|
15
15
|
from rich.console import Console
|
|
16
16
|
from tenacity import (
|
|
17
17
|
retry,
|
|
18
|
+
retry_if_exception_type,
|
|
18
19
|
stop_after_attempt,
|
|
19
20
|
wait_exponential,
|
|
20
|
-
retry_if_exception_type,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
from ..utils.auth import (
|
|
@@ -38,15 +38,19 @@ console = Console()
|
|
|
38
38
|
HTTP_TIMEOUT = 30.0
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
@click.group()
|
|
42
|
+
def auth():
|
|
43
|
+
"""Authentication commands (login, logout, whoami)."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
41
47
|
@retry(
|
|
42
48
|
stop=stop_after_attempt(3),
|
|
43
49
|
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
44
50
|
retry=retry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)),
|
|
45
51
|
reraise=True,
|
|
46
52
|
)
|
|
47
|
-
def _exchange_code_for_tokens_with_retry(
|
|
48
|
-
code: str, code_verifier: str, redirect_uri: str
|
|
49
|
-
) -> dict:
|
|
53
|
+
def _exchange_code_for_tokens_with_retry(code: str, code_verifier: str, redirect_uri: str) -> dict:
|
|
50
54
|
"""
|
|
51
55
|
Exchange authorization code for tokens with retry logic.
|
|
52
56
|
|
|
@@ -141,9 +145,7 @@ def login():
|
|
|
141
145
|
try:
|
|
142
146
|
webbrowser.open(auth_url)
|
|
143
147
|
except Exception as e:
|
|
144
|
-
console.print(
|
|
145
|
-
f"[yellow]Warning: Could not open browser automatically: {e}[/yellow]"
|
|
146
|
-
)
|
|
148
|
+
console.print(f"[yellow]Warning: Could not open browser automatically: {e}[/yellow]")
|
|
147
149
|
console.print(f"[dim]Please open the URL manually: {auth_url}[/dim]")
|
|
148
150
|
|
|
149
151
|
# 5. Run local server to capture redirect
|
|
@@ -197,11 +199,11 @@ def login():
|
|
|
197
199
|
try:
|
|
198
200
|
keyring.set_password(SERVICE_ID, "access_token", access_token)
|
|
199
201
|
keyring.set_password(SERVICE_ID, "refresh_token", refresh_token)
|
|
200
|
-
console.print("[bold green]Login successful! Tokens saved securely.[/bold green]")
|
|
201
|
-
except keyring.errors.KeyringError as e:
|
|
202
202
|
console.print(
|
|
203
|
-
|
|
203
|
+
"[bold green]Login successful! Tokens saved securely.[/bold green]"
|
|
204
204
|
)
|
|
205
|
+
except keyring.errors.KeyringError as e:
|
|
206
|
+
console.print(f"[bold red]Failed to save tokens to keyring: {e}[/bold red]")
|
|
205
207
|
console.print("[yellow]Tokens were received but not saved.[/yellow]")
|
|
206
208
|
else:
|
|
207
209
|
console.print("[bold red]Login failed: No tokens received.[/bold red]")
|
xenfra/main.py
CHANGED
|
@@ -5,8 +5,10 @@ A modern, AI-powered CLI for deploying Python apps to DigitalOcean.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
|
+
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
import click
|
|
11
|
+
from dotenv import load_dotenv
|
|
10
12
|
from rich.console import Console
|
|
11
13
|
|
|
12
14
|
from .commands.auth import auth
|
|
@@ -17,6 +19,11 @@ from .commands.security_cmd import security
|
|
|
17
19
|
|
|
18
20
|
console = Console()
|
|
19
21
|
|
|
22
|
+
# Load .env file from project root (searches parent directories)
|
|
23
|
+
# This allows CLI to use XENFRA_API_URL and other vars from .env
|
|
24
|
+
load_dotenv(dotenv_path=Path.cwd() / ".env", override=False)
|
|
25
|
+
load_dotenv(override=False) # Also check current directory and parents
|
|
26
|
+
|
|
20
27
|
|
|
21
28
|
@click.group()
|
|
22
29
|
@click.version_option(version="0.2.3")
|
xenfra/utils/auth.py
CHANGED
xenfra/utils/codebase.py
CHANGED
|
@@ -80,6 +80,7 @@ def scan_codebase(max_files: int = 10, max_size: int = 50000) -> dict[str, str]:
|
|
|
80
80
|
except (IOError, OSError, PermissionError, UnicodeDecodeError) as e:
|
|
81
81
|
# Skip files that can't be read (log but don't crash)
|
|
82
82
|
import logging
|
|
83
|
+
|
|
83
84
|
logger = logging.getLogger(__name__)
|
|
84
85
|
logger.debug(f"Skipping file {filename}: {type(e).__name__}")
|
|
85
86
|
continue
|
|
@@ -112,6 +113,7 @@ def scan_codebase(max_files: int = 10, max_size: int = 50000) -> dict[str, str]:
|
|
|
112
113
|
except (IOError, OSError, PermissionError, UnicodeDecodeError) as e:
|
|
113
114
|
# Skip files that can't be read
|
|
114
115
|
import logging
|
|
116
|
+
|
|
115
117
|
logger = logging.getLogger(__name__)
|
|
116
118
|
logger.debug(f"Skipping file {filepath}: {type(e).__name__}")
|
|
117
119
|
continue
|
xenfra/utils/config.py
CHANGED
|
@@ -7,8 +7,8 @@ import shutil
|
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
-
import yaml
|
|
11
10
|
import click
|
|
11
|
+
import yaml
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
from rich.prompt import Confirm, IntPrompt, Prompt
|
|
14
14
|
from xenfra_sdk import CodebaseAnalysisResponse
|
|
@@ -159,7 +159,9 @@ def apply_patch(patch: dict, target_file: str = None, create_backup_file: bool =
|
|
|
159
159
|
|
|
160
160
|
operation = patch.get("operation")
|
|
161
161
|
if operation not in ["add", "replace", "remove"]:
|
|
162
|
-
raise ValueError(
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"Invalid patch operation: {operation}. Must be 'add', 'replace', or 'remove'"
|
|
164
|
+
)
|
|
163
165
|
"""
|
|
164
166
|
Apply a JSON patch to a configuration file with automatic backup.
|
|
165
167
|
|
|
@@ -228,6 +230,7 @@ def apply_patch(patch: dict, target_file: str = None, create_backup_file: bool =
|
|
|
228
230
|
# For JSON files
|
|
229
231
|
elif file_to_patch.endswith(".json"):
|
|
230
232
|
import json
|
|
233
|
+
|
|
231
234
|
with open(file_to_patch, "r") as f:
|
|
232
235
|
config_data = json.load(f)
|
|
233
236
|
|
|
@@ -278,6 +281,9 @@ def apply_patch(patch: dict, target_file: str = None, create_backup_file: bool =
|
|
|
278
281
|
with open(file_to_patch, "w") as f:
|
|
279
282
|
f.write(str(value))
|
|
280
283
|
else:
|
|
284
|
+
# Design decision: Only support auto-patching for common dependency files
|
|
285
|
+
# Other file types should be manually edited to avoid data loss
|
|
286
|
+
# See docs/future-enhancements.md #4 for potential extensions
|
|
281
287
|
raise NotImplementedError(f"Patching not supported for file type: {file_to_patch}")
|
|
282
288
|
|
|
283
289
|
return backup_path
|
|
@@ -309,6 +315,7 @@ def manual_prompt_for_config(filename: str = "xenfra.yaml") -> str:
|
|
|
309
315
|
port = IntPrompt.ask("Application port", default=8000)
|
|
310
316
|
# Validate port
|
|
311
317
|
from .validation import validate_port
|
|
318
|
+
|
|
312
319
|
is_valid, error_msg = validate_port(port)
|
|
313
320
|
if not is_valid:
|
|
314
321
|
console.print(f"[bold red]Invalid port: {error_msg}[/bold red]")
|
xenfra/utils/security.py
CHANGED
|
@@ -220,8 +220,8 @@ def create_secure_client(url: str, token: str = None) -> httpx.Client:
|
|
|
220
220
|
ssl_context.verify_mode = ssl.CERT_REQUIRED
|
|
221
221
|
|
|
222
222
|
# Note: Full certificate pinning implementation would require custom verification
|
|
223
|
-
# For now, we use strict certificate validation
|
|
224
|
-
#
|
|
223
|
+
# For now, we use strict certificate validation with system CA bundle
|
|
224
|
+
# Future enhancement: Certificate fingerprint pinning (see docs/future-enhancements.md #3)
|
|
225
225
|
|
|
226
226
|
return httpx.Client(
|
|
227
227
|
base_url=url,
|
xenfra/utils/validation.py
CHANGED
|
@@ -109,7 +109,10 @@ def validate_project_name(project_name: str) -> tuple[bool, Optional[str]]:
|
|
|
109
109
|
|
|
110
110
|
# Alphanumeric, hyphens, underscores, dots
|
|
111
111
|
if not re.match(r"^[a-zA-Z0-9._-]+$", project_name):
|
|
112
|
-
return
|
|
112
|
+
return (
|
|
113
|
+
False,
|
|
114
|
+
"Project name can only contain alphanumeric characters, dots, hyphens, and underscores",
|
|
115
|
+
)
|
|
113
116
|
|
|
114
117
|
# Reserved names
|
|
115
118
|
reserved_names = ["admin", "api", "www", "root", "system", "xenfra"]
|
|
@@ -135,7 +138,10 @@ def validate_branch_name(branch: str) -> tuple[bool, Optional[str]]:
|
|
|
135
138
|
|
|
136
139
|
# Git branch name rules: no spaces, no special chars except /, -, _
|
|
137
140
|
if not re.match(r"^[a-zA-Z0-9/._-]+$", branch):
|
|
138
|
-
return
|
|
141
|
+
return (
|
|
142
|
+
False,
|
|
143
|
+
"Branch name can only contain alphanumeric characters, slashes, dots, hyphens, and underscores",
|
|
144
|
+
)
|
|
139
145
|
|
|
140
146
|
# Cannot start with . or end with .lock
|
|
141
147
|
if branch.startswith(".") or branch.endswith(".lock"):
|
|
@@ -226,4 +232,3 @@ def validate_codebase_scan_limits(max_files: int, max_size: int) -> tuple[bool,
|
|
|
226
232
|
return False, "max_size must be between 1KB and 10MB"
|
|
227
233
|
|
|
228
234
|
return True, None
|
|
229
|
-
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
xenfra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
xenfra/commands/__init__.py,sha256=bdugTOErbWUhDvnwFl17KkGPPV7gtmDkSOzhF_NEHX0,40
|
|
3
|
+
xenfra/commands/auth.py,sha256=W7ncG9ombHRKpjxKEZov5Fj3Tlr8Cx0mbze5nrPedlw,10175
|
|
4
|
+
xenfra/commands/deployments.py,sha256=-185BevHVrUT-LAU2k_uZNpKJPCcwpCDEHOFPfD0Wmw,19348
|
|
5
|
+
xenfra/commands/intelligence.py,sha256=w8GxwGu63KQ5fwhPpTNTDeW1Xg5g3aFzzIBuP_CeRQo,13541
|
|
6
|
+
xenfra/commands/projects.py,sha256=O2tG--iDWN5oCcHOv1jp88kl9bAK61oGRCLJ60M0b7E,6492
|
|
7
|
+
xenfra/commands/security_cmd.py,sha256=MJxbjQksKrtRn21FSAhTY3ESn_S_tUCGfdNRWL7kNsc,7094
|
|
8
|
+
xenfra/main.py,sha256=KLIqdvMpo2ahoz_vnoxq9yPwOJhnyRJKQ4pDgr4FovY,2110
|
|
9
|
+
xenfra/utils/__init__.py,sha256=57o8j7Tibrhyid84zTFLHjFmRP5sCnNbtLEfpRqIpMk,42
|
|
10
|
+
xenfra/utils/auth.py,sha256=oDxDiIWC9851fu_gL-7TVJ60uJT3sZ_DvMIy69SUAEM,8308
|
|
11
|
+
xenfra/utils/codebase.py,sha256=vx-1pMpnefPJ_Xy1UoH7wgHJ2c5ZAsVX1g1IXAfkI28,4018
|
|
12
|
+
xenfra/utils/config.py,sha256=6A6WAggaH2Rco4RJydALxcKteOzXLCKDV0ZxjHhAJHk,11584
|
|
13
|
+
xenfra/utils/security.py,sha256=IR_Hzgfc4KeT-qr2LVBxSvBTO4AP4MCkIlU47_yKN_0,11947
|
|
14
|
+
xenfra/utils/validation.py,sha256=6mGC5CqAbx-CBp06omWLBpKjnEWXsEzlYWq71wjDeX8,6678
|
|
15
|
+
xenfra-0.2.7.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
|
|
16
|
+
xenfra-0.2.7.dist-info/entry_points.txt,sha256=a_2cGhYK__X6eW05Ba8uB6RIM_61c2sHtXsPY8N0mic,45
|
|
17
|
+
xenfra-0.2.7.dist-info/METADATA,sha256=6BeWMSpOmcy-enLutVCeq8XOR0MfuRJRaz4gEbSD_bA,3751
|
|
18
|
+
xenfra-0.2.7.dist-info/RECORD,,
|
xenfra-0.2.5.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
xenfra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
xenfra/commands/__init__.py,sha256=bdugTOErbWUhDvnwFl17KkGPPV7gtmDkSOzhF_NEHX0,40
|
|
3
|
-
xenfra/commands/auth.py,sha256=tG49BfzzivnE9ZN3QmVCZywOEhW6Ab1wgVxnTqTRnnQ,10114
|
|
4
|
-
xenfra/commands/deployments.py,sha256=-185BevHVrUT-LAU2k_uZNpKJPCcwpCDEHOFPfD0Wmw,19348
|
|
5
|
-
xenfra/commands/intelligence.py,sha256=w8GxwGu63KQ5fwhPpTNTDeW1Xg5g3aFzzIBuP_CeRQo,13541
|
|
6
|
-
xenfra/commands/projects.py,sha256=O2tG--iDWN5oCcHOv1jp88kl9bAK61oGRCLJ60M0b7E,6492
|
|
7
|
-
xenfra/commands/security_cmd.py,sha256=MJxbjQksKrtRn21FSAhTY3ESn_S_tUCGfdNRWL7kNsc,7094
|
|
8
|
-
xenfra/main.py,sha256=bv6EslYtRMXShmHfcSuzecNjJWbCvPCi2LKI4kwL_oQ,1790
|
|
9
|
-
xenfra/utils/__init__.py,sha256=57o8j7Tibrhyid84zTFLHjFmRP5sCnNbtLEfpRqIpMk,42
|
|
10
|
-
xenfra/utils/auth.py,sha256=UIWx8m7I9Qm4UiKP8g7hCiVAXgkKo1JfSl4twChXkSA,8308
|
|
11
|
-
xenfra/utils/codebase.py,sha256=Gw3e6N-8WfbBeDEMrTD9j3CGPFFJFgorwHXAmZ93vbw,4016
|
|
12
|
-
xenfra/utils/config.py,sha256=EFmk6DVOIaskon6yWlYSWyveDYuG_zC587P1akllKjc,11336
|
|
13
|
-
xenfra/utils/security.py,sha256=5_rE3hYMTfUzZ05XEoJwMQfQFZGElpw7wc_WbcpzW5M,11894
|
|
14
|
-
xenfra/utils/validation.py,sha256=AslcMtdXFjXVdmCwjKWHhnlCW6g1QyTTj3wP7s1_bZU,6605
|
|
15
|
-
xenfra-0.2.5.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
|
|
16
|
-
xenfra-0.2.5.dist-info/entry_points.txt,sha256=a_2cGhYK__X6eW05Ba8uB6RIM_61c2sHtXsPY8N0mic,45
|
|
17
|
-
xenfra-0.2.5.dist-info/METADATA,sha256=C9Ns60_5AA99ATsa_-ljs1M9gNT2FcsfGyHgt05mSBU,3751
|
|
18
|
-
xenfra-0.2.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|