cycls 0.0.2.93__tar.gz → 0.0.2.95__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycls
3
- Version: 0.0.2.93
3
+ Version: 0.0.2.95
4
4
  Summary: Distribute Intelligence
5
5
  Author-email: "Mohammed J. AlRujayi" <mj@cycls.com>
6
6
  Requires-Python: >=3.10
@@ -79,6 +79,13 @@ class App(Function):
79
79
  self._prepare_func(prod=True)
80
80
  return super().deploy(port=port)
81
81
 
82
+ def _deploy(self, port=8080):
83
+ """Deploy to testing infrastructure."""
84
+ if self.api_key is None:
85
+ raise RuntimeError("Missing API key. Set cycls.api_key or CYCLS_API_KEY environment variable.")
86
+ self._prepare_func(prod=True)
87
+ return super()._deploy(port=port)
88
+
82
89
 
83
90
  def app(name=None, **kwargs):
84
91
  """Decorator that transforms a function into a deployable App."""
@@ -396,6 +396,82 @@ CMD ["python", "entrypoint.py"]
396
396
  print(f"Connection error: {e}")
397
397
  return None
398
398
 
399
+ def _deploy(self, *args, **kwargs):
400
+ import requests
401
+
402
+ base_url = self.base_url
403
+ port = kwargs.pop('port', 8080)
404
+
405
+ # Check name availability before uploading
406
+ print(f"Checking '{self.name}'...")
407
+ try:
408
+ check_resp = requests.get(
409
+ f"{base_url}/v1/deployment/check-name",
410
+ params={"name": self.name},
411
+ headers={"X-API-Key": self.api_key},
412
+ timeout=30,
413
+ )
414
+ if check_resp.status_code == 401:
415
+ print("Error: Invalid API key")
416
+ return None
417
+ check_resp.raise_for_status()
418
+ check_data = check_resp.json()
419
+ if not check_data.get("available"):
420
+ print(f"Error: {check_data.get('reason', 'Name unavailable')}")
421
+ return None
422
+ except requests.exceptions.RequestException as e:
423
+ print(f"Error checking name: {e}")
424
+ return None
425
+
426
+ print(f"Deploying '{self.name}'...")
427
+
428
+ payload = cloudpickle.dumps((self.func, args, {**kwargs, 'port': port}))
429
+ archive_name = f"{self.name}-{hashlib.sha256(payload).hexdigest()[:16]}.tar.gz"
430
+
431
+ with tempfile.TemporaryDirectory() as tmpdir:
432
+ workdir = Path(tmpdir)
433
+ self._prepare_deploy_context(workdir, port, args, kwargs)
434
+
435
+ archive_path = workdir / archive_name
436
+ with tarfile.open(archive_path, "w:gz") as tar:
437
+ for f in workdir.glob("**/*"):
438
+ if f.is_file() and f != archive_path:
439
+ tar.add(f, arcname=f.relative_to(workdir))
440
+
441
+ print("Uploading...")
442
+ with open(archive_path, 'rb') as f:
443
+ response = requests.post(
444
+ f"{base_url}/v1/deploy",
445
+ data={"function_name": self.name, "port": port},
446
+ files={'source_archive': (archive_name, f, 'application/gzip')},
447
+ headers={"X-API-Key": self.api_key},
448
+ timeout=9000,
449
+ stream=True,
450
+ )
451
+
452
+ if not response.ok:
453
+ print(f"Deploy failed: {response.status_code}")
454
+ try:
455
+ print(f" {response.json()['detail']}")
456
+ except (json.JSONDecodeError, KeyError):
457
+ print(f" {response.text}")
458
+ return None
459
+
460
+ # Parse NDJSON stream
461
+ url = None
462
+ for line in response.iter_lines(decode_unicode=True):
463
+ if line:
464
+ event = json.loads(line)
465
+ status = event.get("status", "")
466
+ msg = event.get("message", "")
467
+ print(f" [{status}] {msg}")
468
+ if status == "DONE":
469
+ url = event.get("url")
470
+ print(f"Deployed: {url}")
471
+ elif status == "ERROR":
472
+ return None
473
+ return url
474
+
399
475
  def __del__(self):
400
476
  self._cleanup_container()
401
477