cycls 0.0.2.34__py3-none-any.whl → 0.0.2.35__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,14 +9,6 @@ from pathlib import Path
9
9
  from contextlib import contextmanager
10
10
  import tarfile
11
11
 
12
- # --- Docker Client Initialization ---
13
- try:
14
- docker_client = docker.from_env()
15
- except docker.errors.DockerException:
16
- print("❌ Error: Docker is not running or not installed.")
17
- print("Please start the Docker daemon and try again.")
18
- sys.exit(1)
19
-
20
12
  # --- Top-Level Helper Functions ---
21
13
 
22
14
  def _bootstrap_script(payload_file: str, result_file: str) -> str:
@@ -84,7 +76,7 @@ class Runtime:
84
76
  """
85
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):
86
78
  self.func = func
87
- self.python_version = python_version or "3.12"
79
+ self.python_version = python_version or f"{sys.version_info.major}.{sys.version_info.minor}"
88
80
  self.pip_packages = sorted(pip_packages or [])
89
81
  self.apt_packages = sorted(apt_packages or [])
90
82
  self.run_commands = sorted(run_commands or [])
@@ -104,6 +96,50 @@ class Runtime:
104
96
  self.tag = self._generate_base_tag()
105
97
 
106
98
  self.api_key = api_key
99
+ self._docker_client = None
100
+ self.managed_label = f"cycls.runtime"
101
+
102
+ @property
103
+ def docker_client(self):
104
+ """
105
+ Lazily initializes and returns a Docker client.
106
+ This ensures Docker is only required for methods that actually use it.
107
+ """
108
+ if self._docker_client is None:
109
+ try:
110
+ print("🐳 Initializing Docker client...")
111
+ client = docker.from_env()
112
+ client.ping()
113
+ self._docker_client = client
114
+ except docker.errors.DockerException:
115
+ print("\n❌ Error: Docker is not running or is not installed.")
116
+ print(" This is required for local 'run' and 'build' operations.")
117
+ print(" Please start the Docker daemon and try again.")
118
+ sys.exit(1)
119
+ return self._docker_client
120
+
121
+ def _perform_auto_cleanup(self):
122
+ """Performs a simple, automatic cleanup of old Docker resources."""
123
+ try:
124
+ for container in self.docker_client.containers.list(all=True, filters={"label": self.managed_label}):
125
+ container.remove(force=True)
126
+
127
+ cleaned_images = 0
128
+ for image in self.docker_client.images.list(name=self.image_prefix):
129
+ is_current = self.tag in image.tags
130
+ is_deployable = any(t.startswith(f"{self.image_prefix}:deploy-") for t in image.tags)
131
+
132
+ if not is_current and not is_deployable:
133
+ self.docker_client.images.remove(image.id, force=True)
134
+ cleaned_images += 1
135
+
136
+ if cleaned_images > 0:
137
+ print(f"🧹 Cleaned up {cleaned_images} old image version(s).")
138
+
139
+ self.docker_client.images.prune(filters={'label': self.managed_label})
140
+
141
+ except Exception as e:
142
+ print(f"⚠️ An error occurred during cleanup: {e}")
107
143
 
108
144
  def _generate_base_tag(self) -> str:
109
145
  """Creates a unique tag for the base Docker image based on its dependencies."""
@@ -182,7 +218,7 @@ COPY {self.payload_file} {self.io_dir}/
182
218
  def _build_image_if_needed(self):
183
219
  """Checks if the base Docker image exists locally and builds it if not."""
184
220
  try:
185
- docker_client.images.get(self.tag)
221
+ self.docker_client.images.get(self.tag)
186
222
  print(f"✅ Found cached base image: {self.tag}")
187
223
  return
188
224
  except docker.errors.ImageNotFound:
@@ -194,7 +230,7 @@ COPY {self.payload_file} {self.io_dir}/
194
230
  self._prepare_build_context(tmpdir)
195
231
 
196
232
  print("--- 🐳 Docker Build Logs (Base Image) ---")
197
- response_generator = docker_client.api.build(
233
+ response_generator = self.docker_client.api.build(
198
234
  path=str(tmpdir),
199
235
  tag=self.tag,
200
236
  forcerm=True,
@@ -215,6 +251,7 @@ COPY {self.payload_file} {self.io_dir}/
215
251
  def runner(self, *args, **kwargs):
216
252
  """Context manager to set up, run, and tear down the container for local execution."""
217
253
  port = kwargs.get('port', None)
254
+ self._perform_auto_cleanup()
218
255
  self._build_image_if_needed()
219
256
  container = None
220
257
  ports_mapping = {f'{port}/tcp': port} if port else None
@@ -228,10 +265,11 @@ COPY {self.payload_file} {self.io_dir}/
228
265
  cloudpickle.dump((self.func, args, kwargs), f)
229
266
 
230
267
  try:
231
- container = docker_client.containers.create(
268
+ container = self.docker_client.containers.create(
232
269
  image=self.tag,
233
270
  volumes={str(tmpdir): {'bind': self.io_dir, 'mode': 'rw'}},
234
- ports=ports_mapping
271
+ ports=ports_mapping,
272
+ labels={self.managed_label: "true"}
235
273
  )
236
274
  container.start()
237
275
  yield container, result_path
@@ -279,7 +317,7 @@ COPY {self.payload_file} {self.io_dir}/
279
317
  final_tag = f"{self.image_prefix}:deploy-{payload_hash}"
280
318
 
281
319
  try:
282
- docker_client.images.get(final_tag)
320
+ self.docker_client.images.get(final_tag)
283
321
  print(f"✅ Found cached deployable image: {final_tag}")
284
322
  return final_tag
285
323
  except docker.errors.ImageNotFound:
@@ -290,7 +328,7 @@ COPY {self.payload_file} {self.io_dir}/
290
328
  self._prepare_build_context(tmpdir, include_payload=True, args=args, kwargs=kwargs)
291
329
 
292
330
  print("--- 🐳 Docker Build Logs (Final Image) ---")
293
- response_generator = docker_client.api.build(
331
+ response_generator = self.docker_client.api.build(
294
332
  path=str(tmpdir), tag=final_tag, forcerm=True, decode=True
295
333
  )
296
334
  try:
cycls/sdk.py CHANGED
@@ -16,12 +16,13 @@ class Agent:
16
16
 
17
17
  self.registered_functions = []
18
18
 
19
- def __call__(self, name="", header="", intro="", domain=None, auth=False):
19
+ def __call__(self, name=None, header="", intro="", domain=None, auth=False):
20
20
  def decorator(f):
21
21
  self.registered_functions.append({
22
22
  "func": f,
23
23
  "config": ["public", False, self.org, self.api_token, header, intro, auth],
24
- "name": name,
24
+ # "name": name,
25
+ "name": name or (f.__name__).replace('_', '-'),
25
26
  "domain": domain or f"{name}.cycls.ai",
26
27
  })
27
28
  return f
@@ -40,7 +41,7 @@ class Agent:
40
41
  uvicorn.run(web(i["func"], *i["config"]), host="0.0.0.0", port=port)
41
42
  return
42
43
 
43
- def deploy(self, prod=False, port=8080):
44
+ def cycls(self, prod=False, port=8080):
44
45
  if not self.registered_functions:
45
46
  print("Error: No @agent decorated function found.")
46
47
  return
@@ -54,11 +55,15 @@ class Agent:
54
55
 
55
56
  i["config"][6] = False
56
57
 
58
+ copy={str(cycls_path.joinpath('theme')):"public", str(cycls_path)+"/web.py":"app/web.py"}
59
+ copy.update({i:i for i in self.copy})
60
+
57
61
  new = Runtime(
58
62
  func=lambda port: __import__("uvicorn").run(__import__("web").web(i["func"], *i["config"]), host="0.0.0.0", port=port),
59
- name="web-agent",
63
+ name=i["name"],
64
+ apt_packages=self.apt,
60
65
  pip_packages=["fastapi[standard]", "pyjwt", "cryptography", "uvicorn", *self.pip],
61
- copy={str(cycls_path.joinpath('theme')):"public", str(cycls_path)+"/web.py":"app/web.py"},
66
+ copy=copy,
62
67
  api_key=self.api_key
63
68
  )
64
69
  new.deploy(port=port) if prod else new.run(port=port)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycls
3
- Version: 0.0.2.34
3
+ Version: 0.0.2.35
4
4
  Summary: Cycls SDK
5
5
  Author: Mohammed J. AlRujayi
6
6
  Author-email: mj@cycls.com
@@ -1,9 +1,9 @@
1
1
  cycls/__init__.py,sha256=ysfA4R_r_INokmpZGK2S8wf2ypLCqwyj0WdfX-hx0bs,51
2
- cycls/runtime.py,sha256=rUQnaweV9y5fPcKxTmOqrQO2nT_A5wqgSElonY6GNmw,16366
3
- cycls/sdk.py,sha256=tzTWNzWVdQPh-I-AmhkkgQ0M3xJyMy-x-vFAlq1XihU,4408
2
+ cycls/runtime.py,sha256=U0dMUs9dxgjMTbtv9dzK3MQ57g8kun0ICqA30Cx9cBA,18187
3
+ cycls/sdk.py,sha256=gEdxYDtYE6FOWwLS33k0h5wuzwdcB9kvTMrkEBmz2iE,4573
4
4
  cycls/theme/assets/index-D0-uI8sw.js,sha256=aUsqm9HZtEJz38o-0MW12ZVeOlSeKigwc_fYJBntiyI,1068551
5
5
  cycls/theme/index.html,sha256=epB4cgSjC7xJOXpVuCwt9r7ivoGvLiXSrxsoOgINw58,895
6
6
  cycls/web.py,sha256=nSEJMUQoPaz8qjgVmsC1JiDRv9Y1UKVzTH4_pRHp_VE,4260
7
- cycls-0.0.2.34.dist-info/METADATA,sha256=fRwJekFv5Rzy2izqhMtf6IhMmHd3GBdZRnwEO66FxbY,5666
8
- cycls-0.0.2.34.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
9
- cycls-0.0.2.34.dist-info/RECORD,,
7
+ cycls-0.0.2.35.dist-info/METADATA,sha256=DztQ_n4XO8rvuwGq-bN7Y028yINUNjcH3XYbjF-OKsI,5666
8
+ cycls-0.0.2.35.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
9
+ cycls-0.0.2.35.dist-info/RECORD,,