cycls 0.0.2.68__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:
@@ -325,6 +328,110 @@ COPY {self.payload_file} {self.io_dir}/
325
328
  print(f"\n🛑 Operation stopped: {e}")
326
329
  return None
327
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
+
328
435
  def build(self, *args, **kwargs):
329
436
  """Builds a self-contained, deployable Docker image locally."""
330
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.68
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=oAaqYbY58mIbXzQbWoNpbHyOA9py-7ZDnrEoPLXxHfE,18847
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.68.dist-info/METADATA,sha256=l8vds3aonBPjG6Q0oO8BkvDynM-3MEC9Yh4hFbb5I4M,7943
11
- cycls-0.0.2.68.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
12
- cycls-0.0.2.68.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,,