xenfra-sdk 0.1.1__py3-none-any.whl → 0.1.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 CHANGED
@@ -1,21 +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
- ]
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
+ ]
xenfra_sdk/cli/main.py CHANGED
@@ -1,226 +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()
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()
xenfra_sdk/client.py CHANGED
@@ -6,7 +6,6 @@ from .exceptions import AuthenticationError, XenfraAPIError, XenfraError
6
6
  from .resources.deployments import DeploymentsManager
7
7
  from .resources.intelligence import IntelligenceManager
8
8
  from .resources.projects import ProjectsManager
9
- from .utils import safe_json_parse
10
9
 
11
10
 
12
11
  class XenfraClient:
@@ -53,7 +52,9 @@ class XenfraClient:
53
52
  if "application/json" in content_type:
54
53
  try:
55
54
  error_data = e.response.json()
56
- detail = error_data.get("detail", e.response.text[:500] if e.response.text else "Unknown error")
55
+ detail = error_data.get(
56
+ "detail", e.response.text[:500] if e.response.text else "Unknown error"
57
+ )
57
58
  except (ValueError, TypeError):
58
59
  detail = e.response.text[:500] if e.response.text else "Unknown error"
59
60
  else:
@@ -82,5 +83,5 @@ class XenfraClient:
82
83
 
83
84
  def __del__(self):
84
85
  """Destructor - cleanup if not already closed."""
85
- if hasattr(self, '_closed') and not self._closed:
86
+ if hasattr(self, "_closed") and not self._closed:
86
87
  self.close()
@@ -12,7 +12,6 @@ from .exceptions import AuthenticationError, XenfraAPIError, XenfraError
12
12
  from .resources.deployments import DeploymentsManager
13
13
  from .resources.intelligence import IntelligenceManager
14
14
  from .resources.projects import ProjectsManager
15
- from .utils import safe_json_parse
16
15
 
17
16
  logger = logging.getLogger(__name__)
18
17
 
@@ -187,7 +186,10 @@ class XenfraClient:
187
186
  if "application/json" in content_type:
188
187
  try:
189
188
  error_data = e.response.json()
190
- detail = error_data.get("detail", e.response.text[:500] if e.response.text else "Unknown error")
189
+ detail = error_data.get(
190
+ "detail",
191
+ e.response.text[:500] if e.response.text else "Unknown error",
192
+ )
191
193
  except (ValueError, TypeError):
192
194
  detail = e.response.text[:500] if e.response.text else "Unknown error"
193
195
  else:
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,27 +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
- Note: user_id references a User in the SSO service database.
15
- In a microservices architecture, we store the ID but don't enforce
16
- a foreign key constraint across service boundaries.
17
- """
18
- id: Optional[int] = Field(default=None, primary_key=True)
19
- droplet_id: int = Field(unique=True, index=True)
20
- name: str
21
- ip_address: str
22
- status: str
23
- region: str
24
- size: str
25
- # user_id is a reference to User.id in the SSO service database
26
- # No foreign key constraint since databases are separate in microservices architecture
27
- 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