cycls 0.0.2.67__py3-none-any.whl → 0.0.2.69__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.
cycls/runtime.py CHANGED
@@ -9,6 +9,9 @@ from pathlib import Path
9
9
  from contextlib import contextmanager
10
10
  import tarfile
11
11
 
12
+ # Enable BuildKit for faster builds with better caching
13
+ os.environ["DOCKER_BUILDKIT"] = "1"
14
+
12
15
  # --- Top-Level Helper Functions ---
13
16
 
14
17
  def _bootstrap_script(payload_file: str, result_file: str) -> str:
@@ -68,16 +71,22 @@ def _copy_path(src_path: Path, dest_path: Path):
68
71
  dest_path.parent.mkdir(parents=True, exist_ok=True)
69
72
  shutil.copy(src_path, dest_path)
70
73
 
74
+ # Pre-built base image with common dependencies
75
+ BASE_IMAGE = "ghcr.io/cycls/base:python3.12"
76
+ BASE_PACKAGES = {
77
+ "cloudpickle", "cryptography", "fastapi", "fastapi[standard]",
78
+ "pydantic", "pyjwt", "uvicorn", "uvicorn[standard]", "httpx"
79
+ }
80
+
71
81
  # --- Main Runtime Class ---
72
82
 
73
83
  class Runtime:
74
84
  """
75
85
  Handles building a Docker image and executing a function within a container.
76
86
  """
77
- def __init__(self, func, name, python_version=None, pip_packages=None, apt_packages=None, run_commands=None, copy=None, base_url=None, api_key=None):
87
+ def __init__(self, func, name, python_version=None, pip_packages=None, apt_packages=None, run_commands=None, copy=None, base_url=None, api_key=None, base_image=None):
78
88
  self.func = func
79
89
  self.python_version = python_version or f"{sys.version_info.major}.{sys.version_info.minor}"
80
- self.pip_packages = sorted(pip_packages or [])
81
90
  self.apt_packages = sorted(apt_packages or [])
82
91
  self.run_commands = sorted(run_commands or [])
83
92
  self.copy = copy or {}
@@ -85,6 +94,11 @@ class Runtime:
85
94
  self.base_url = base_url or "https://service-core-280879789566.me-central1.run.app"
86
95
  self.image_prefix = f"cycls/{name}"
87
96
 
97
+ # Use pre-built base image by default, filter out already-installed packages
98
+ self.base_image = base_image or BASE_IMAGE
99
+ all_pip = set(pip_packages or [])
100
+ self.pip_packages = sorted(all_pip - BASE_PACKAGES) if self.base_image == BASE_IMAGE else sorted(all_pip)
101
+
88
102
  # Standard paths and filenames used inside the container
89
103
  self.io_dir = "/app/io"
90
104
  self.runner_filename = "runner.py"
@@ -145,6 +159,7 @@ class Runtime:
145
159
  def _generate_base_tag(self) -> str:
146
160
  """Creates a unique tag for the base Docker image based on its dependencies."""
147
161
  signature_parts = [
162
+ self.base_image,
148
163
  "".join(self.python_version),
149
164
  "".join(self.pip_packages),
150
165
  "".join(self.apt_packages),
@@ -163,7 +178,11 @@ class Runtime:
163
178
 
164
179
  def _generate_dockerfile(self, port=None) -> str:
165
180
  """Generates a multi-stage Dockerfile string."""
166
- run_pip_install = f"RUN pip install --no-cache-dir cloudpickle {' '.join(self.pip_packages)}"
181
+ # Only install extra packages not in base image
182
+ run_pip_install = (
183
+ f"RUN pip install --no-cache-dir {' '.join(self.pip_packages)}"
184
+ if self.pip_packages else ""
185
+ )
167
186
  run_apt_install = (
168
187
  f"RUN apt-get update && apt-get install -y --no-install-recommends {' '.join(self.apt_packages)}"
169
188
  if self.apt_packages else ""
@@ -174,14 +193,14 @@ class Runtime:
174
193
 
175
194
  return f"""
176
195
  # STAGE 1: Base image with all dependencies
177
- FROM python:{self.python_version}-slim as base
196
+ FROM {self.base_image} as base
178
197
  ENV PIP_ROOT_USER_ACTION=ignore
179
198
  ENV PYTHONUNBUFFERED=1
180
199
  RUN mkdir -p {self.io_dir}
181
200
  {run_apt_install}
182
201
  {run_pip_install}
183
202
  {run_shell_commands}
184
- WORKDIR app
203
+ WORKDIR /app
185
204
  {copy_lines}
186
205
  COPY {self.runner_filename} {self.runner_path}
187
206
  ENTRYPOINT ["python", "{self.runner_path}", "{self.io_dir}"]
@@ -309,6 +328,110 @@ COPY {self.payload_file} {self.io_dir}/
309
328
  print(f"\n🛑 Operation stopped: {e}")
310
329
  return None
311
330
 
331
+ def watch(self, *args, **kwargs):
332
+ """Runs the container with file watching - restarts script on changes."""
333
+ try:
334
+ from watchfiles import watch as watchfiles_watch
335
+ except ImportError:
336
+ print("❌ watchfiles not installed. Run: pip install watchfiles")
337
+ return
338
+
339
+ import inspect
340
+
341
+ # Get the main script (the outermost .py file in the stack)
342
+ main_script = None
343
+ for frame_info in inspect.stack():
344
+ filename = frame_info.filename
345
+ if filename.endswith('.py') and not filename.startswith('<'):
346
+ main_script = Path(filename).resolve()
347
+ # main_script is now the outermost/first script in the call chain
348
+
349
+ # Build watch paths: main script + copy sources
350
+ watch_paths = []
351
+ if main_script and main_script.exists():
352
+ watch_paths.append(main_script)
353
+ watch_paths.extend([Path(src).resolve() for src in self.copy.keys() if Path(src).exists()])
354
+
355
+ if not watch_paths:
356
+ print("⚠️ No files to watch. Running without watch mode.")
357
+ return self.run(*args, **kwargs)
358
+
359
+ print(f"👀 Watching for changes:")
360
+ for p in watch_paths:
361
+ print(f" {p}")
362
+ print()
363
+
364
+ while True:
365
+ # Run the container in a subprocess-like manner
366
+ print(f"🚀 Running function '{self.name}' in container...")
367
+ self._perform_auto_cleanup()
368
+ self._build_image_if_needed()
369
+
370
+ port = kwargs.get('port', None)
371
+ ports_mapping = {f'{port}/tcp': port} if port else None
372
+
373
+ with tempfile.TemporaryDirectory() as tmpdir_str:
374
+ tmpdir = Path(tmpdir_str)
375
+ payload_path = tmpdir / self.payload_file
376
+
377
+ with payload_path.open('wb') as f:
378
+ cloudpickle.dump((self.func, args, kwargs), f)
379
+
380
+ container = self.docker_client.containers.create(
381
+ image=self.tag,
382
+ volumes={str(tmpdir): {'bind': self.io_dir, 'mode': 'rw'}},
383
+ ports=ports_mapping,
384
+ labels={self.managed_label: "true"}
385
+ )
386
+ container.start()
387
+ print(f"✅ Container running on port {port}")
388
+ print("👀 Waiting for file changes... (Ctrl+C to stop)\n")
389
+
390
+ try:
391
+ # Watch for changes in a separate thread
392
+ import threading
393
+ import time
394
+ change_detected = threading.Event()
395
+
396
+ def watch_thread():
397
+ for changes in watchfiles_watch(*watch_paths):
398
+ changed_files = [str(c[1]) for c in changes]
399
+ print(f"\n🔄 Changes detected:")
400
+ for f in changed_files:
401
+ print(f" {f}")
402
+ change_detected.set()
403
+ break
404
+
405
+ watcher = threading.Thread(target=watch_thread, daemon=True)
406
+ watcher.start()
407
+
408
+ # Stream container logs in separate thread
409
+ def log_thread():
410
+ for chunk in container.logs(stream=True, follow=True):
411
+ if change_detected.is_set():
412
+ break
413
+ print(chunk.decode('utf-8').strip())
414
+
415
+ logger = threading.Thread(target=log_thread, daemon=True)
416
+ logger.start()
417
+
418
+ # Wait for change or interrupt
419
+ while not change_detected.is_set():
420
+ time.sleep(0.5)
421
+
422
+ print("\n🔄 Restarting...")
423
+ container.stop(timeout=2)
424
+ container.remove()
425
+ # Re-exec the main script
426
+ print(f"🔄 Re-executing {main_script.name}...\n")
427
+ os.execv(sys.executable, [sys.executable, str(main_script)])
428
+
429
+ except KeyboardInterrupt:
430
+ print("\n🛑 Stopping...")
431
+ container.stop(timeout=2)
432
+ container.remove()
433
+ return
434
+
312
435
  def build(self, *args, **kwargs):
313
436
  """Builds a self-contained, deployable Docker image locally."""
314
437
  print("📦 Building self-contained image for deployment...")
cycls/sdk.py CHANGED
@@ -88,13 +88,16 @@ class Agent:
88
88
  uvicorn.run(web(agent.func, agent.config), host="0.0.0.0", port=port)
89
89
  return
90
90
 
91
- def deploy(self, prod=False, port=8080):
91
+ def deploy(self, prod=False, port=8080, watch=False):
92
92
  if not self.registered_functions:
93
93
  print("Error: No @agent decorated function found.")
94
94
  return
95
95
  if (self.key is None) and prod:
96
96
  print("🛑 Error: Please add your Cycls API key")
97
97
  return
98
+ if prod and watch:
99
+ print("⚠️ Warning: watch=True ignored in production mode.")
100
+ watch = False
98
101
 
99
102
  agent = self.registered_functions[0]
100
103
  if len(self.registered_functions) > 1:
@@ -118,7 +121,12 @@ class Agent:
118
121
  base_url=self.base_url,
119
122
  api_key=self.key
120
123
  )
121
- new.deploy(port=port) if prod else new.run(port=port)
124
+ if prod:
125
+ new.deploy(port=port)
126
+ elif watch:
127
+ new.watch(port=port)
128
+ else:
129
+ new.run(port=port)
122
130
  return
123
131
 
124
132
  def modal(self, prod=False):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycls
3
- Version: 0.0.2.67
3
+ Version: 0.0.2.69
4
4
  Summary: Distribute Intelligence
5
5
  Author: Mohammed J. AlRujayi
6
6
  Author-email: mj@cycls.com
@@ -4,9 +4,9 @@ cycls/default-theme/assets/index-B0ZKcm_V.css,sha256=wK9-NhEB8xPcN9Zv69zpOcfGTlF
4
4
  cycls/default-theme/assets/index-D5EDcI4J.js,sha256=sN4qRcAXa7DBd9JzmVcCoCwH4l8cNCM-U9QGUjBvWSo,1346506
5
5
  cycls/default-theme/index.html,sha256=bM-yW_g0cGrV40Q5yY3ccY0fM4zI1Wuu5I8EtGFJIxs,828
6
6
  cycls/dev-theme/index.html,sha256=QJBHkdNuMMiwQU7o8dN8__8YQeQB45D37D-NCXIWB2Q,11585
7
- cycls/runtime.py,sha256=hLBtwtGz0FCW1-EPCJy6kMdF2fB3i6Df_H8-bm7qeK0,18223
8
- cycls/sdk.py,sha256=PqHIJdq1pvOTKm1c5vaG6DTMUsjdMzIjzcxrqw3aN9E,6744
7
+ cycls/runtime.py,sha256=BaP5sLG3NLPQZwuKwoYueC3R5WeuNg2-YzOQes6paUE,23221
8
+ cycls/sdk.py,sha256=iOSFFPSN2iGCTIL2ZBOpb2X5jjUEm7gQbAuanYHF4qg,6974
9
9
  cycls/web.py,sha256=3M3qaWTNY3dpgd7Vq5aXREp-cIFsHrDqBQ1YkGrOaUk,4659
10
- cycls-0.0.2.67.dist-info/METADATA,sha256=fVNjmTlSIa9GAQiLvWsrEZO3v7izpZrYj6KTRngEPD4,7943
11
- cycls-0.0.2.67.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
12
- cycls-0.0.2.67.dist-info/RECORD,,
10
+ cycls-0.0.2.69.dist-info/METADATA,sha256=3nG949VOjQ1K-sftwDGsAQheGAt-GCKWGUmDgUBgOP4,7943
11
+ cycls-0.0.2.69.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
12
+ cycls-0.0.2.69.dist-info/RECORD,,