xenfra 0.4.2__py3-none-any.whl → 0.4.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/commands/__init__.py +3 -3
- xenfra/commands/auth.py +144 -144
- xenfra/commands/auth_device.py +164 -164
- xenfra/commands/deployments.py +1133 -912
- xenfra/commands/intelligence.py +503 -412
- xenfra/commands/projects.py +204 -204
- xenfra/commands/security_cmd.py +233 -233
- xenfra/main.py +76 -75
- xenfra/utils/__init__.py +3 -3
- xenfra/utils/auth.py +374 -374
- xenfra/utils/codebase.py +169 -169
- xenfra/utils/config.py +459 -432
- xenfra/utils/errors.py +116 -116
- xenfra/utils/file_sync.py +286 -0
- xenfra/utils/security.py +336 -336
- xenfra/utils/validation.py +234 -234
- xenfra-0.4.4.dist-info/METADATA +113 -0
- xenfra-0.4.4.dist-info/RECORD +21 -0
- {xenfra-0.4.2.dist-info → xenfra-0.4.4.dist-info}/WHEEL +2 -2
- xenfra-0.4.2.dist-info/METADATA +0 -118
- xenfra-0.4.2.dist-info/RECORD +0 -20
- {xenfra-0.4.2.dist-info → xenfra-0.4.4.dist-info}/entry_points.txt +0 -0
xenfra/commands/projects.py
CHANGED
|
@@ -1,204 +1,204 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Project management commands for Xenfra CLI.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import click
|
|
6
|
-
from rich.console import Console
|
|
7
|
-
from rich.table import Table
|
|
8
|
-
from xenfra_sdk import XenfraClient
|
|
9
|
-
from xenfra_sdk.exceptions import XenfraAPIError, XenfraError
|
|
10
|
-
|
|
11
|
-
from ..utils.auth import API_BASE_URL, get_auth_token
|
|
12
|
-
from ..utils.validation import (
|
|
13
|
-
validate_project_id,
|
|
14
|
-
validate_project_name,
|
|
15
|
-
validate_region,
|
|
16
|
-
validate_size_slug,
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
console = Console()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def get_client() -> XenfraClient:
|
|
23
|
-
"""Get authenticated SDK client."""
|
|
24
|
-
token = get_auth_token()
|
|
25
|
-
if not token:
|
|
26
|
-
console.print("[bold red]Not logged in. Run 'xenfra login' first.[/bold red]")
|
|
27
|
-
raise click.Abort()
|
|
28
|
-
|
|
29
|
-
return XenfraClient(token=token, api_url=API_BASE_URL)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@click.group()
|
|
33
|
-
def projects():
|
|
34
|
-
"""Manage projects."""
|
|
35
|
-
pass
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@projects.command()
|
|
39
|
-
def list():
|
|
40
|
-
"""List all projects."""
|
|
41
|
-
try:
|
|
42
|
-
# Use context manager for proper cleanup
|
|
43
|
-
with get_client() as client:
|
|
44
|
-
projects_list = client.projects.list()
|
|
45
|
-
|
|
46
|
-
if not projects_list:
|
|
47
|
-
console.print("[bold yellow]No projects found.[/bold yellow]")
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
# Create a rich table
|
|
51
|
-
table = Table(title="Projects")
|
|
52
|
-
table.add_column("ID", style="cyan")
|
|
53
|
-
table.add_column("Name", style="green")
|
|
54
|
-
table.add_column("Status", style="yellow")
|
|
55
|
-
table.add_column("Region", style="blue")
|
|
56
|
-
table.add_column("IP Address", style="magenta")
|
|
57
|
-
table.add_column("Cost/Month", style="red")
|
|
58
|
-
|
|
59
|
-
for project in projects_list:
|
|
60
|
-
cost = (
|
|
61
|
-
f"${project.estimated_monthly_cost:.2f}"
|
|
62
|
-
if project.estimated_monthly_cost
|
|
63
|
-
else "N/A"
|
|
64
|
-
)
|
|
65
|
-
table.add_row(
|
|
66
|
-
str(project.id),
|
|
67
|
-
project.name,
|
|
68
|
-
project.status,
|
|
69
|
-
project.region,
|
|
70
|
-
project.ip_address or "N/A",
|
|
71
|
-
cost,
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
console.print(table)
|
|
75
|
-
|
|
76
|
-
except XenfraAPIError as e:
|
|
77
|
-
console.print(f"[bold red]API Error: {e.detail}[/bold red]")
|
|
78
|
-
except XenfraError as e:
|
|
79
|
-
console.print(f"[bold red]Error: {e}[/bold red]")
|
|
80
|
-
except click.Abort:
|
|
81
|
-
pass
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@projects.command()
|
|
85
|
-
@click.argument("project_id", type=int)
|
|
86
|
-
def show(project_id):
|
|
87
|
-
"""Show details for a specific project."""
|
|
88
|
-
# Validate project ID
|
|
89
|
-
is_valid, error_msg = validate_project_id(project_id)
|
|
90
|
-
if not is_valid:
|
|
91
|
-
console.print(f"[bold red]Invalid project ID: {error_msg}[/bold red]")
|
|
92
|
-
raise click.Abort()
|
|
93
|
-
|
|
94
|
-
try:
|
|
95
|
-
with get_client() as client:
|
|
96
|
-
project = client.projects.show(project_id)
|
|
97
|
-
|
|
98
|
-
# Create detailed panel
|
|
99
|
-
from rich.panel import Panel
|
|
100
|
-
|
|
101
|
-
details = f"""[cyan]Name:[/cyan] {project.name}
|
|
102
|
-
[cyan]Status:[/cyan] {project.status}
|
|
103
|
-
[cyan]Region:[/cyan] {project.region}
|
|
104
|
-
[cyan]IP Address:[/cyan] {project.ip_address or 'N/A'}
|
|
105
|
-
[cyan]Size:[/cyan] {project.size_slug}
|
|
106
|
-
[cyan]Cost/Month:[/cyan] ${project.estimated_monthly_cost:.2f} USD
|
|
107
|
-
[cyan]Created:[/cyan] {project.created_at}"""
|
|
108
|
-
|
|
109
|
-
panel = Panel(
|
|
110
|
-
details,
|
|
111
|
-
title=f"[bold green]Project {project.id}[/bold green]",
|
|
112
|
-
border_style="green",
|
|
113
|
-
)
|
|
114
|
-
console.print(panel)
|
|
115
|
-
|
|
116
|
-
except XenfraAPIError as e:
|
|
117
|
-
console.print(f"[bold red]API Error: {e.detail}[/bold red]")
|
|
118
|
-
except XenfraError as e:
|
|
119
|
-
console.print(f"[bold red]Error: {e}[/bold red]")
|
|
120
|
-
except click.Abort:
|
|
121
|
-
pass
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
@projects.command()
|
|
125
|
-
@click.argument("project_id", type=int)
|
|
126
|
-
@click.confirmation_option(prompt="Are you sure you want to delete this project?")
|
|
127
|
-
def delete(project_id):
|
|
128
|
-
"""Delete a project."""
|
|
129
|
-
# Validate project ID
|
|
130
|
-
is_valid, error_msg = validate_project_id(project_id)
|
|
131
|
-
if not is_valid:
|
|
132
|
-
console.print(f"[bold red]Invalid project ID: {error_msg}[/bold red]")
|
|
133
|
-
raise click.Abort()
|
|
134
|
-
|
|
135
|
-
try:
|
|
136
|
-
with get_client() as client:
|
|
137
|
-
client.projects.delete(str(project_id))
|
|
138
|
-
console.print(f"[bold green]Project {project_id} deletion initiated.[/bold green]")
|
|
139
|
-
|
|
140
|
-
except XenfraAPIError as e:
|
|
141
|
-
console.print(f"[bold red]API Error: {e.detail}[/bold red]")
|
|
142
|
-
except XenfraError as e:
|
|
143
|
-
console.print(f"[bold red]Error: {e}[/bold red]")
|
|
144
|
-
except click.Abort:
|
|
145
|
-
pass
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
@projects.command()
|
|
149
|
-
@click.argument("name")
|
|
150
|
-
@click.option("--region", default="nyc3", help="DigitalOcean region (default: nyc3)")
|
|
151
|
-
@click.option(
|
|
152
|
-
"--size", "size_slug", default="s-1vcpu-1gb", help="Droplet size (default: s-1vcpu-1gb)"
|
|
153
|
-
)
|
|
154
|
-
def create(name, region, size_slug):
|
|
155
|
-
"""Create a new project."""
|
|
156
|
-
# Validate project name
|
|
157
|
-
is_valid, error_msg = validate_project_name(name)
|
|
158
|
-
if not is_valid:
|
|
159
|
-
console.print(f"[bold red]Invalid project name: {error_msg}[/bold red]")
|
|
160
|
-
raise click.Abort()
|
|
161
|
-
|
|
162
|
-
# Validate region
|
|
163
|
-
is_valid, error_msg = validate_region(region)
|
|
164
|
-
if not is_valid:
|
|
165
|
-
console.print(f"[bold red]Invalid region: {error_msg}[/bold red]")
|
|
166
|
-
raise click.Abort()
|
|
167
|
-
|
|
168
|
-
# Validate size slug
|
|
169
|
-
is_valid, error_msg = validate_size_slug(size_slug)
|
|
170
|
-
if not is_valid:
|
|
171
|
-
console.print(f"[bold red]Invalid size slug: {error_msg}[/bold red]")
|
|
172
|
-
raise click.Abort()
|
|
173
|
-
|
|
174
|
-
try:
|
|
175
|
-
with get_client() as client:
|
|
176
|
-
console.print(f"[cyan]Creating project '{name}'...[/cyan]")
|
|
177
|
-
|
|
178
|
-
# Create project
|
|
179
|
-
project = client.projects.create(name=name, region=region, size_slug=size_slug)
|
|
180
|
-
|
|
181
|
-
# Display success message
|
|
182
|
-
console.print("[bold green]✓[/bold green] Project created successfully!")
|
|
183
|
-
|
|
184
|
-
# Show project details
|
|
185
|
-
from rich.panel import Panel
|
|
186
|
-
|
|
187
|
-
details = f"""[cyan]ID:[/cyan] {project.id}
|
|
188
|
-
[cyan]Name:[/cyan] {project.name}
|
|
189
|
-
[cyan]Status:[/cyan] {project.status}
|
|
190
|
-
[cyan]Region:[/cyan] {project.region}
|
|
191
|
-
[cyan]Size:[/cyan] {project.size_slug}
|
|
192
|
-
[cyan]Estimated Cost:[/cyan] ${project.estimated_monthly_cost:.2f}/month"""
|
|
193
|
-
|
|
194
|
-
panel = Panel(
|
|
195
|
-
details, title="[bold green]New Project[/bold green]", border_style="green"
|
|
196
|
-
)
|
|
197
|
-
console.print(panel)
|
|
198
|
-
|
|
199
|
-
except XenfraAPIError as e:
|
|
200
|
-
console.print(f"[bold red]API Error: {e.detail}[/bold red]")
|
|
201
|
-
except XenfraError as e:
|
|
202
|
-
console.print(f"[bold red]Error: {e}[/bold red]")
|
|
203
|
-
except click.Abort:
|
|
204
|
-
pass
|
|
1
|
+
"""
|
|
2
|
+
Project management commands for Xenfra CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from xenfra_sdk import XenfraClient
|
|
9
|
+
from xenfra_sdk.exceptions import XenfraAPIError, XenfraError
|
|
10
|
+
|
|
11
|
+
from ..utils.auth import API_BASE_URL, get_auth_token
|
|
12
|
+
from ..utils.validation import (
|
|
13
|
+
validate_project_id,
|
|
14
|
+
validate_project_name,
|
|
15
|
+
validate_region,
|
|
16
|
+
validate_size_slug,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_client() -> XenfraClient:
|
|
23
|
+
"""Get authenticated SDK client."""
|
|
24
|
+
token = get_auth_token()
|
|
25
|
+
if not token:
|
|
26
|
+
console.print("[bold red]Not logged in. Run 'xenfra login' first.[/bold red]")
|
|
27
|
+
raise click.Abort()
|
|
28
|
+
|
|
29
|
+
return XenfraClient(token=token, api_url=API_BASE_URL)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@click.group()
|
|
33
|
+
def projects():
|
|
34
|
+
"""Manage projects."""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@projects.command()
|
|
39
|
+
def list():
|
|
40
|
+
"""List all projects."""
|
|
41
|
+
try:
|
|
42
|
+
# Use context manager for proper cleanup
|
|
43
|
+
with get_client() as client:
|
|
44
|
+
projects_list = client.projects.list()
|
|
45
|
+
|
|
46
|
+
if not projects_list:
|
|
47
|
+
console.print("[bold yellow]No projects found.[/bold yellow]")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
# Create a rich table
|
|
51
|
+
table = Table(title="Projects")
|
|
52
|
+
table.add_column("ID", style="cyan")
|
|
53
|
+
table.add_column("Name", style="green")
|
|
54
|
+
table.add_column("Status", style="yellow")
|
|
55
|
+
table.add_column("Region", style="blue")
|
|
56
|
+
table.add_column("IP Address", style="magenta")
|
|
57
|
+
table.add_column("Cost/Month", style="red")
|
|
58
|
+
|
|
59
|
+
for project in projects_list:
|
|
60
|
+
cost = (
|
|
61
|
+
f"${project.estimated_monthly_cost:.2f}"
|
|
62
|
+
if project.estimated_monthly_cost
|
|
63
|
+
else "N/A"
|
|
64
|
+
)
|
|
65
|
+
table.add_row(
|
|
66
|
+
str(project.id),
|
|
67
|
+
project.name,
|
|
68
|
+
project.status,
|
|
69
|
+
project.region,
|
|
70
|
+
project.ip_address or "N/A",
|
|
71
|
+
cost,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
console.print(table)
|
|
75
|
+
|
|
76
|
+
except XenfraAPIError as e:
|
|
77
|
+
console.print(f"[bold red]API Error: {e.detail}[/bold red]")
|
|
78
|
+
except XenfraError as e:
|
|
79
|
+
console.print(f"[bold red]Error: {e}[/bold red]")
|
|
80
|
+
except click.Abort:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@projects.command()
|
|
85
|
+
@click.argument("project_id", type=int)
|
|
86
|
+
def show(project_id):
|
|
87
|
+
"""Show details for a specific project."""
|
|
88
|
+
# Validate project ID
|
|
89
|
+
is_valid, error_msg = validate_project_id(project_id)
|
|
90
|
+
if not is_valid:
|
|
91
|
+
console.print(f"[bold red]Invalid project ID: {error_msg}[/bold red]")
|
|
92
|
+
raise click.Abort()
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
with get_client() as client:
|
|
96
|
+
project = client.projects.show(project_id)
|
|
97
|
+
|
|
98
|
+
# Create detailed panel
|
|
99
|
+
from rich.panel import Panel
|
|
100
|
+
|
|
101
|
+
details = f"""[cyan]Name:[/cyan] {project.name}
|
|
102
|
+
[cyan]Status:[/cyan] {project.status}
|
|
103
|
+
[cyan]Region:[/cyan] {project.region}
|
|
104
|
+
[cyan]IP Address:[/cyan] {project.ip_address or 'N/A'}
|
|
105
|
+
[cyan]Size:[/cyan] {project.size_slug}
|
|
106
|
+
[cyan]Cost/Month:[/cyan] ${project.estimated_monthly_cost:.2f} USD
|
|
107
|
+
[cyan]Created:[/cyan] {project.created_at}"""
|
|
108
|
+
|
|
109
|
+
panel = Panel(
|
|
110
|
+
details,
|
|
111
|
+
title=f"[bold green]Project {project.id}[/bold green]",
|
|
112
|
+
border_style="green",
|
|
113
|
+
)
|
|
114
|
+
console.print(panel)
|
|
115
|
+
|
|
116
|
+
except XenfraAPIError as e:
|
|
117
|
+
console.print(f"[bold red]API Error: {e.detail}[/bold red]")
|
|
118
|
+
except XenfraError as e:
|
|
119
|
+
console.print(f"[bold red]Error: {e}[/bold red]")
|
|
120
|
+
except click.Abort:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@projects.command()
|
|
125
|
+
@click.argument("project_id", type=int)
|
|
126
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this project?")
|
|
127
|
+
def delete(project_id):
|
|
128
|
+
"""Delete a project."""
|
|
129
|
+
# Validate project ID
|
|
130
|
+
is_valid, error_msg = validate_project_id(project_id)
|
|
131
|
+
if not is_valid:
|
|
132
|
+
console.print(f"[bold red]Invalid project ID: {error_msg}[/bold red]")
|
|
133
|
+
raise click.Abort()
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
with get_client() as client:
|
|
137
|
+
client.projects.delete(str(project_id))
|
|
138
|
+
console.print(f"[bold green]Project {project_id} deletion initiated.[/bold green]")
|
|
139
|
+
|
|
140
|
+
except XenfraAPIError as e:
|
|
141
|
+
console.print(f"[bold red]API Error: {e.detail}[/bold red]")
|
|
142
|
+
except XenfraError as e:
|
|
143
|
+
console.print(f"[bold red]Error: {e}[/bold red]")
|
|
144
|
+
except click.Abort:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@projects.command()
|
|
149
|
+
@click.argument("name")
|
|
150
|
+
@click.option("--region", default="nyc3", help="DigitalOcean region (default: nyc3)")
|
|
151
|
+
@click.option(
|
|
152
|
+
"--size", "size_slug", default="s-1vcpu-1gb", help="Droplet size (default: s-1vcpu-1gb)"
|
|
153
|
+
)
|
|
154
|
+
def create(name, region, size_slug):
|
|
155
|
+
"""Create a new project."""
|
|
156
|
+
# Validate project name
|
|
157
|
+
is_valid, error_msg = validate_project_name(name)
|
|
158
|
+
if not is_valid:
|
|
159
|
+
console.print(f"[bold red]Invalid project name: {error_msg}[/bold red]")
|
|
160
|
+
raise click.Abort()
|
|
161
|
+
|
|
162
|
+
# Validate region
|
|
163
|
+
is_valid, error_msg = validate_region(region)
|
|
164
|
+
if not is_valid:
|
|
165
|
+
console.print(f"[bold red]Invalid region: {error_msg}[/bold red]")
|
|
166
|
+
raise click.Abort()
|
|
167
|
+
|
|
168
|
+
# Validate size slug
|
|
169
|
+
is_valid, error_msg = validate_size_slug(size_slug)
|
|
170
|
+
if not is_valid:
|
|
171
|
+
console.print(f"[bold red]Invalid size slug: {error_msg}[/bold red]")
|
|
172
|
+
raise click.Abort()
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
with get_client() as client:
|
|
176
|
+
console.print(f"[cyan]Creating project '{name}'...[/cyan]")
|
|
177
|
+
|
|
178
|
+
# Create project
|
|
179
|
+
project = client.projects.create(name=name, region=region, size_slug=size_slug)
|
|
180
|
+
|
|
181
|
+
# Display success message
|
|
182
|
+
console.print("[bold green]✓[/bold green] Project created successfully!")
|
|
183
|
+
|
|
184
|
+
# Show project details
|
|
185
|
+
from rich.panel import Panel
|
|
186
|
+
|
|
187
|
+
details = f"""[cyan]ID:[/cyan] {project.id}
|
|
188
|
+
[cyan]Name:[/cyan] {project.name}
|
|
189
|
+
[cyan]Status:[/cyan] {project.status}
|
|
190
|
+
[cyan]Region:[/cyan] {project.region}
|
|
191
|
+
[cyan]Size:[/cyan] {project.size_slug}
|
|
192
|
+
[cyan]Estimated Cost:[/cyan] ${project.estimated_monthly_cost:.2f}/month"""
|
|
193
|
+
|
|
194
|
+
panel = Panel(
|
|
195
|
+
details, title="[bold green]New Project[/bold green]", border_style="green"
|
|
196
|
+
)
|
|
197
|
+
console.print(panel)
|
|
198
|
+
|
|
199
|
+
except XenfraAPIError as e:
|
|
200
|
+
console.print(f"[bold red]API Error: {e.detail}[/bold red]")
|
|
201
|
+
except XenfraError as e:
|
|
202
|
+
console.print(f"[bold red]Error: {e}[/bold red]")
|
|
203
|
+
except click.Abort:
|
|
204
|
+
pass
|