xenfra-sdk 0.2.0__tar.gz → 0.2.1__tar.gz

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.
Files changed (31) hide show
  1. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/PKG-INFO +1 -1
  2. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/pyproject.toml +1 -1
  3. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/engine.py +46 -22
  4. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/resources/deployments.py +12 -5
  5. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/templates/docker-compose.yml.j2 +3 -9
  6. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/README.md +0 -0
  7. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/__init__.py +0 -0
  8. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/cli/__init__.py +0 -0
  9. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/cli/main.py +0 -0
  10. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/client.py +0 -0
  11. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/client_with_hooks.py +0 -0
  12. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/config.py +0 -0
  13. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/db/__init__.py +0 -0
  14. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/db/models.py +0 -0
  15. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/db/session.py +0 -0
  16. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/dependencies.py +0 -0
  17. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/dockerizer.py +0 -0
  18. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/exceptions.py +0 -0
  19. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/mcp_client.py +0 -0
  20. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/models.py +0 -0
  21. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/patterns.json +0 -0
  22. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/privacy.py +0 -0
  23. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/recipes.py +0 -0
  24. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/resources/__init__.py +0 -0
  25. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/resources/base.py +0 -0
  26. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/resources/intelligence.py +0 -0
  27. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/resources/projects.py +0 -0
  28. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/security.py +0 -0
  29. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/templates/Dockerfile.j2 +0 -0
  30. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/templates/cloud-init.sh.j2 +0 -0
  31. {xenfra_sdk-0.2.0 → xenfra_sdk-0.2.1}/src/xenfra_sdk/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: xenfra-sdk
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Xenfra SDK: Core engine and utilities for the Xenfra platform.
5
5
  Author: xenfra-cloud
6
6
  Author-email: xenfra-cloud <xenfracloud@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "xenfra-sdk"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Xenfra SDK: Core engine and utilities for the Xenfra platform."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -162,7 +162,7 @@ class InfraEngine:
162
162
  # 3. Stream logs
163
163
  ip_address = droplet.ip_address
164
164
  with self._get_connection(ip_address) as conn:
165
- conn.run("cd /root/app && docker-compose logs -f app", pty=True)
165
+ conn.run("cd /root/app && docker compose logs -f app", pty=True)
166
166
 
167
167
  def get_account_balance(self) -> dict:
168
168
  """
@@ -244,6 +244,9 @@ class InfraEngine:
244
244
  repo_url: Optional[str] = None,
245
245
  is_dockerized: bool = True,
246
246
  db_session: Session = None,
247
+ port: int = 8000,
248
+ command: str = None,
249
+ database: str = None,
247
250
  **kwargs,
248
251
  ):
249
252
  """A stateful, blocking orchestrator for deploying a new server."""
@@ -260,7 +263,10 @@ class InfraEngine:
260
263
  "email": email,
261
264
  "domain": domain,
262
265
  "repo_url": repo_url,
263
- **kwargs, # Pass db config, etc.
266
+ "port": port,
267
+ "command": command,
268
+ "database": database,
269
+ **kwargs, # Pass any additional config
264
270
  }
265
271
  files = dockerizer.generate_templated_assets(context)
266
272
  for file in files:
@@ -282,8 +288,8 @@ class InfraEngine:
282
288
  region=region,
283
289
  image=image,
284
290
  size_slug=size,
285
- ssh_keys=[ssh_key],
286
- userdata=cloud_init_script,
291
+ ssh_keys=[ssh_key.id],
292
+ user_data=cloud_init_script,
287
293
  tags=["xenfra"],
288
294
  )
289
295
  droplet.create()
@@ -324,22 +330,36 @@ class InfraEngine:
324
330
  if not conn or not conn.is_connected:
325
331
  raise DeploymentError("Could not establish SSH connection.", stage="Polling")
326
332
 
333
+ logger(" - [DEBUG] Entering SSH context for Phase 5 polling...")
327
334
  with conn:
328
335
  last_log_line = 0
329
- for i in range(120): # 20-minute timeout for cloud-init (installing Docker, Caddy, etc.)
330
- # Check for completion
331
- if conn.run("test -f /root/setup_complete", warn=True, hide=True).ok:
332
- logger(" - Cloud-init setup complete.")
333
- break
336
+ logger(" - Polling server setup log (/root/setup.log)...")
337
+ for i in range(120): # 20-minute timeout
338
+ # Heartbeat
339
+ if i % 3 == 0: # Every 30 seconds
340
+ logger(f" - Phase 5 Heartbeat: Waiting for setup completion ({i+1}/120)...")
341
+
342
+ # Check for completion with timeout
343
+ try:
344
+ check_result = conn.run("test -f /root/setup_complete", warn=True, hide=True, timeout=10)
345
+ if check_result.ok:
346
+ logger(" - Cloud-init setup complete.")
347
+ break
348
+ except Exception as e:
349
+ logger(f" - [Warning] Status check failed: {e}. Retrying...")
334
350
 
335
351
  # Tail the setup log for visibility
336
- log_result = conn.run(f"tail -n +{last_log_line + 1} /root/setup.log 2>/dev/null", warn=True, hide=True)
337
- if log_result.ok and log_result.stdout.strip():
338
- new_lines = log_result.stdout.strip().split("\n")
339
- for line in new_lines:
340
- if line.strip():
341
- logger(f" [Server Setup] {line.strip()}")
342
- last_log_line += len(new_lines)
352
+ try:
353
+ log_result = conn.run(f"tail -n +{last_log_line + 1} /root/setup.log 2>/dev/null", warn=True, hide=True, timeout=10)
354
+ if log_result.ok and log_result.stdout.strip():
355
+ new_lines = log_result.stdout.strip().split("\n")
356
+ for line in new_lines:
357
+ if line.strip():
358
+ logger(f" [Server Setup] {line.strip()}")
359
+ last_log_line += len(new_lines)
360
+ except Exception as e:
361
+ # Log doesn't exist yet or tail failed
362
+ pass
343
363
 
344
364
  time.sleep(10)
345
365
  else:
@@ -363,16 +383,20 @@ class InfraEngine:
363
383
  stage="Code Upload",
364
384
  )
365
385
 
366
- fabric.transfer.Transfer(conn).upload(
367
- ".", "/root/app", exclude=[".git", ".venv", "__pycache__"]
368
- )
386
+ # Use rsync for efficient local folder upload
387
+ private_key_path = str(Path.home() / ".ssh" / "id_rsa")
388
+ rsync_cmd = f'rsync -avz --exclude=".git" --exclude=".venv" --exclude="__pycache__" -e "ssh -i {private_key_path} -o StrictHostKeyChecking=no" . root@{ip_address}:/root/app/'
389
+ logger(f" - Uploading local code via rsync...")
390
+ result = subprocess.run(rsync_cmd, shell=True, capture_output=True, text=True)
391
+ if result.returncode != 0:
392
+ raise DeploymentError(f"rsync failed: {result.stderr}", stage="Code Upload")
369
393
  logger(" - Code upload complete.")
370
394
 
371
395
  # === 7. FINAL DEPLOY STAGE ===
372
396
  if is_dockerized:
373
397
  logger("\n[bold blue]PHASE 7: BUILDING AND DEPLOYING CONTAINERS[/bold blue]")
374
398
  with self._get_connection(ip_address) as conn:
375
- result = conn.run("cd /root/app && docker-compose up -d --build", hide=True)
399
+ result = conn.run("cd /root/app && docker compose up -d --build", hide=True)
376
400
  if result.failed:
377
401
  raise DeploymentError(f"docker-compose failed: {result.stderr}", stage="Deploy")
378
402
  logger(" - Docker containers are building in the background...")
@@ -393,7 +417,7 @@ class InfraEngine:
393
417
  with self._get_connection(ip_address) as conn:
394
418
  # Check if running
395
419
  if is_dockerized:
396
- ps_result = conn.run("cd /root/app && docker-compose ps", hide=True)
420
+ ps_result = conn.run("cd /root/app && docker compose ps", hide=True)
397
421
  running = "running" in ps_result.stdout
398
422
  else:
399
423
  ps_result = conn.run("ps aux | grep -v grep | grep python", hide=True)
@@ -432,7 +456,7 @@ class InfraEngine:
432
456
  else:
433
457
  # On failure, get logs and destroy droplet
434
458
  with self._get_connection(ip_address) as conn:
435
- logs = conn.run("cd /root/app && docker-compose logs", hide=True).stdout
459
+ logs = conn.run("cd /root/app && docker compose logs", hide=True).stdout
436
460
  raise DeploymentError(
437
461
  f"Application failed to become healthy in time. Logs:\n{logs}",
438
462
  stage="Verification",
@@ -97,7 +97,7 @@ class DeploymentsManager(BaseManager):
97
97
  except Exception as e:
98
98
  raise XenfraError(f"Failed to get logs for deployment {deployment_id}: {e}")
99
99
 
100
- def create_stream(self, project_name: str, git_repo: str, branch: str, framework: str, region: str = None, size_slug: str = None, is_dockerized: bool = True) -> Iterator[dict]:
100
+ def create_stream(self, project_name: str, git_repo: str, branch: str, framework: str, region: str = None, size_slug: str = None, is_dockerized: bool = True, port: int = None, command: str = None, database: str = None) -> Iterator[dict]:
101
101
  """
102
102
  Creates a new deployment with real-time SSE log streaming.
103
103
 
@@ -110,6 +110,10 @@ class DeploymentsManager(BaseManager):
110
110
  framework: Framework type (fastapi, flask, django)
111
111
  region: DigitalOcean region (optional)
112
112
  size_slug: DigitalOcean droplet size (optional)
113
+ is_dockerized: Whether to use Docker (optional)
114
+ port: Application port (optional, default 8000)
115
+ command: Start command (optional, auto-detected if not provided)
116
+ database: Database type (optional, e.g. 'postgres')
113
117
 
114
118
  Yields:
115
119
  dict: SSE events with 'event' and 'data' fields
@@ -133,6 +137,12 @@ class DeploymentsManager(BaseManager):
133
137
  payload["size_slug"] = size_slug
134
138
  if is_dockerized is not None:
135
139
  payload["is_dockerized"] = is_dockerized
140
+ if port:
141
+ payload["port"] = port
142
+ if command:
143
+ payload["command"] = command
144
+ if database:
145
+ payload["database"] = database
136
146
 
137
147
  try:
138
148
  # Use httpx to stream the SSE response
@@ -150,11 +160,8 @@ class DeploymentsManager(BaseManager):
150
160
  streaming_api_url = os.getenv("XENFRA_STREAMING_API_URL")
151
161
  if streaming_api_url:
152
162
  base_url = streaming_api_url
153
- elif self._client.api_url == "https://api.xenfra.tech":
154
- # Production: use non-proxied streaming subdomain
155
- base_url = "https://stream.xenfra.tech"
156
163
  else:
157
- # Local/dev: use regular API URL
164
+ # Local/dev/production: use regular API URL
158
165
  base_url = self._client.api_url
159
166
 
160
167
  url = f"{base_url}/deployments/stream"
@@ -6,17 +6,13 @@ services:
6
6
  build: .
7
7
  ports:
8
8
  - "{{ port | default(8000) }}:{{ port | default(8000) }}"
9
- volumes:
10
- - .:/app
11
- command: {{ command }}
12
- {% if database == 'postgres' %}
9
+ command: {{ command | default('uvicorn main:app --host 0.0.0.0 --port 8000') }}
10
+ {% if database == 'postgres' or database == 'postgresql' %}
13
11
  depends_on:
14
12
  - db
15
13
  environment:
16
14
  - DATABASE_URL=postgresql://{{ db_user | default('user') }}:{{ db_password | default('password') }}@db:5432/{{ db_name | default('appdb') }}
17
- {% endif %}
18
15
 
19
- {% if database == 'postgres' %}
20
16
  db:
21
17
  image: postgres:15-alpine
22
18
  volumes:
@@ -25,9 +21,7 @@ services:
25
21
  - POSTGRES_USER={{ db_user | default('user') }}
26
22
  - POSTGRES_PASSWORD={{ db_password | default('password') }}
27
23
  - POSTGRES_DB={{ db_name | default('appdb') }}
28
- {% endif %}
29
24
 
30
25
  volumes:
31
- {% if database == 'postgres' %}
32
26
  postgres_data:
33
- {% endif %}
27
+ {% endif %}
File without changes