cycls 0.0.2.34__tar.gz → 0.0.2.36__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.34
3
+ Version: 0.0.2.36
4
4
  Summary: Cycls SDK
5
5
  Author: Mohammed J. AlRujayi
6
6
  Author-email: mj@cycls.com
@@ -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,51 @@ 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
+ # docker system prune -af
122
+ def _perform_auto_cleanup(self):
123
+ """Performs a simple, automatic cleanup of old Docker resources."""
124
+ try:
125
+ for container in self.docker_client.containers.list(all=True, filters={"label": self.managed_label}):
126
+ container.remove(force=True)
127
+
128
+ cleaned_images = 0
129
+ for image in self.docker_client.images.list(all=True, filters={"label": self.managed_label}):
130
+ is_current = self.tag in image.tags
131
+ is_deployable = any(t.startswith(f"{self.image_prefix}:deploy-") for t in image.tags)
132
+
133
+ if not is_current and not is_deployable:
134
+ self.docker_client.images.remove(image.id, force=True)
135
+ cleaned_images += 1
136
+
137
+ if cleaned_images > 0:
138
+ print(f"🧹 Cleaned up {cleaned_images} old image version(s).")
139
+
140
+ self.docker_client.images.prune(filters={'label': self.managed_label})
141
+
142
+ except Exception as e:
143
+ print(f"⚠️ An error occurred during cleanup: {e}")
107
144
 
108
145
  def _generate_base_tag(self) -> str:
109
146
  """Creates a unique tag for the base Docker image based on its dependencies."""
@@ -132,11 +169,7 @@ class Runtime:
132
169
  if self.apt_packages else ""
133
170
  )
134
171
  run_shell_commands = "\n".join([f"RUN {cmd}" for cmd in self.run_commands]) if self.run_commands else ""
135
-
136
- # fixes absolute path copy, but causes collision
137
- copy_lines = "\n".join([f"COPY {Path(src).name} {dst}" for src, dst in self.copy.items()])
138
- # copy_lines = "\n".join([f"COPY {src} {dst}" for src, dst in self.copy.items()])
139
-
172
+ copy_lines = "\n".join([f"COPY context_files/{dst} {dst}" for dst in self.copy.values()])
140
173
  expose_line = f"EXPOSE {port}" if port else ""
141
174
 
142
175
  return f"""
@@ -162,6 +195,16 @@ COPY {self.payload_file} {self.io_dir}/
162
195
  """Prepares a complete build context in the given directory."""
163
196
  port = kwargs.get('port') if kwargs else None
164
197
 
198
+ # Create a dedicated subdirectory for all user-copied files
199
+ context_files_dir = workdir / "context_files"
200
+ context_files_dir.mkdir()
201
+
202
+ if self.copy:
203
+ for src, dst in self.copy.items():
204
+ src_path = Path(src).resolve() # Resolve to an absolute path
205
+ dest_in_context = context_files_dir / dst
206
+ _copy_path(src_path, dest_in_context)
207
+
165
208
  (workdir / "Dockerfile").write_text(self._generate_dockerfile(port=port))
166
209
  (workdir / self.runner_filename).write_text(self.runner_script)
167
210
 
@@ -169,20 +212,10 @@ COPY {self.payload_file} {self.io_dir}/
169
212
  payload_bytes = cloudpickle.dumps((self.func, args or [], kwargs or {}))
170
213
  (workdir / self.payload_file).write_bytes(payload_bytes)
171
214
 
172
- if self.copy:
173
- # for src in self.copy.keys():
174
- # _copy_path(Path(src), workdir / src)
175
-
176
- # fixes absolute path copy
177
- for src in self.copy.keys():
178
- src_path = Path(src)
179
- dest_path = workdir / src_path.name if src_path.is_absolute() else workdir / src
180
- _copy_path(src_path, dest_path)
181
-
182
215
  def _build_image_if_needed(self):
183
216
  """Checks if the base Docker image exists locally and builds it if not."""
184
217
  try:
185
- docker_client.images.get(self.tag)
218
+ self.docker_client.images.get(self.tag)
186
219
  print(f"✅ Found cached base image: {self.tag}")
187
220
  return
188
221
  except docker.errors.ImageNotFound:
@@ -194,12 +227,13 @@ COPY {self.payload_file} {self.io_dir}/
194
227
  self._prepare_build_context(tmpdir)
195
228
 
196
229
  print("--- 🐳 Docker Build Logs (Base Image) ---")
197
- response_generator = docker_client.api.build(
230
+ response_generator = self.docker_client.api.build(
198
231
  path=str(tmpdir),
199
232
  tag=self.tag,
200
233
  forcerm=True,
201
234
  decode=True,
202
- target='base' # Only build the 'base' stage
235
+ target='base', # Only build the 'base' stage
236
+ labels={self.managed_label: "true"}, # image label
203
237
  )
204
238
  try:
205
239
  for chunk in response_generator:
@@ -215,6 +249,7 @@ COPY {self.payload_file} {self.io_dir}/
215
249
  def runner(self, *args, **kwargs):
216
250
  """Context manager to set up, run, and tear down the container for local execution."""
217
251
  port = kwargs.get('port', None)
252
+ self._perform_auto_cleanup()
218
253
  self._build_image_if_needed()
219
254
  container = None
220
255
  ports_mapping = {f'{port}/tcp': port} if port else None
@@ -228,10 +263,11 @@ COPY {self.payload_file} {self.io_dir}/
228
263
  cloudpickle.dump((self.func, args, kwargs), f)
229
264
 
230
265
  try:
231
- container = docker_client.containers.create(
266
+ container = self.docker_client.containers.create(
232
267
  image=self.tag,
233
268
  volumes={str(tmpdir): {'bind': self.io_dir, 'mode': 'rw'}},
234
- ports=ports_mapping
269
+ ports=ports_mapping,
270
+ labels={self.managed_label: "true"} # container label
235
271
  )
236
272
  container.start()
237
273
  yield container, result_path
@@ -279,7 +315,7 @@ COPY {self.payload_file} {self.io_dir}/
279
315
  final_tag = f"{self.image_prefix}:deploy-{payload_hash}"
280
316
 
281
317
  try:
282
- docker_client.images.get(final_tag)
318
+ self.docker_client.images.get(final_tag)
283
319
  print(f"✅ Found cached deployable image: {final_tag}")
284
320
  return final_tag
285
321
  except docker.errors.ImageNotFound:
@@ -290,7 +326,7 @@ COPY {self.payload_file} {self.io_dir}/
290
326
  self._prepare_build_context(tmpdir, include_payload=True, args=args, kwargs=kwargs)
291
327
 
292
328
  print("--- 🐳 Docker Build Logs (Final Image) ---")
293
- response_generator = docker_client.api.build(
329
+ response_generator = self.docker_client.api.build(
294
330
  path=str(tmpdir), tag=final_tag, forcerm=True, decode=True
295
331
  )
296
332
  try:
@@ -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)
@@ -97,6 +102,7 @@ class Agent:
97
102
  with modal.enable_output(), run_app(app=self.app, client=self.client):
98
103
  while True: time.sleep(10)
99
104
 
105
+ # docker system prune -af
100
106
  # poetry config pypi-token.pypi <your-token>
101
- # poetry run python agent.py
107
+ # poetry run python agent-cycls.py
102
108
  # poetry publish --build
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "cycls"
3
- version = "0.0.2.34"
3
+ version = "0.0.2.36"
4
4
 
5
5
  packages = [{ include = "cycls" }]
6
6
  include = ["cycls/theme/**/*"]
File without changes
File without changes
File without changes