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.
- {cycls-0.0.2.93 → cycls-0.0.2.95}/PKG-INFO +1 -1
- {cycls-0.0.2.93 → cycls-0.0.2.95}/cycls/app.py +7 -0
- {cycls-0.0.2.93 → cycls-0.0.2.95}/cycls/function.py +76 -0
- cycls-0.0.2.93/cycls/themes/default/assets/index-Xh0IeurI.js → cycls-0.0.2.95/cycls/themes/default/assets/index-CTZe1T7l.js +66 -66
- {cycls-0.0.2.93 → cycls-0.0.2.95}/cycls/themes/default/index.html +1 -1
- {cycls-0.0.2.93 → cycls-0.0.2.95}/pyproject.toml +1 -1
- {cycls-0.0.2.93 → cycls-0.0.2.95}/.gitignore +0 -0
- {cycls-0.0.2.93 → cycls-0.0.2.95}/README.md +0 -0
- {cycls-0.0.2.93 → cycls-0.0.2.95}/cycls/__init__.py +0 -0
- {cycls-0.0.2.93 → cycls-0.0.2.95}/cycls/auth.py +0 -0
- {cycls-0.0.2.93 → cycls-0.0.2.95}/cycls/cli.py +0 -0
- {cycls-0.0.2.93 → cycls-0.0.2.95}/cycls/state.py +0 -0
- {cycls-0.0.2.93 → cycls-0.0.2.95}/cycls/themes/default/assets/index-oGkkm3Z8.css +0 -0
- {cycls-0.0.2.93 → cycls-0.0.2.95}/cycls/themes/dev/index.html +0 -0
- {cycls-0.0.2.93 → cycls-0.0.2.95}/cycls/web.py +0 -0
|
@@ -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
|
|