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.
- {cycls-0.0.2.34 → cycls-0.0.2.36}/PKG-INFO +1 -1
- {cycls-0.0.2.34 → cycls-0.0.2.36}/cycls/runtime.py +67 -31
- {cycls-0.0.2.34 → cycls-0.0.2.36}/cycls/sdk.py +12 -6
- {cycls-0.0.2.34 → cycls-0.0.2.36}/pyproject.toml +1 -1
- {cycls-0.0.2.34 → cycls-0.0.2.36}/README.md +0 -0
- {cycls-0.0.2.34 → cycls-0.0.2.36}/cycls/__init__.py +0 -0
- {cycls-0.0.2.34 → cycls-0.0.2.36}/cycls/theme/assets/index-D0-uI8sw.js +0 -0
- {cycls-0.0.2.34 → cycls-0.0.2.36}/cycls/theme/index.html +0 -0
- {cycls-0.0.2.34 → cycls-0.0.2.36}/cycls/web.py +0 -0
|
@@ -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 "
|
|
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=
|
|
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
|
|
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="
|
|
63
|
+
name=i["name"],
|
|
64
|
+
apt_packages=self.apt,
|
|
60
65
|
pip_packages=["fastapi[standard]", "pyjwt", "cryptography", "uvicorn", *self.pip],
|
|
61
|
-
copy=
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|