cycls 0.0.2.70__py3-none-any.whl → 0.0.2.72__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/cli.py ADDED
@@ -0,0 +1,217 @@
1
+ import sys
2
+ import json
3
+ import time
4
+ import threading
5
+ import httpx
6
+
7
+ # ANSI codes
8
+ DIM = "\033[2m"
9
+ BOLD = "\033[1m"
10
+ RESET = "\033[0m"
11
+ CLEAR_LINE = "\r\033[K"
12
+ GREEN = "\033[32m"
13
+ YELLOW = "\033[33m"
14
+ BLUE = "\033[34m"
15
+ RED = "\033[31m"
16
+ CYAN = "\033[36m"
17
+ MAGENTA = "\033[35m"
18
+
19
+ CALLOUT_STYLES = {
20
+ "success": ("✓", GREEN),
21
+ "warning": ("⚠", YELLOW),
22
+ "info": ("ℹ", BLUE),
23
+ "error": ("✗", RED),
24
+ }
25
+
26
+ def format_time(seconds):
27
+ if seconds < 60:
28
+ return f"{seconds:.1f}s"
29
+ return f"{int(seconds // 60)}m {int(seconds % 60)}s"
30
+
31
+ def render_table(headers, rows):
32
+ if not headers:
33
+ return
34
+ widths = [len(str(h)) for h in headers]
35
+ for row in rows:
36
+ for i, cell in enumerate(row):
37
+ if i < len(widths):
38
+ widths[i] = max(widths[i], len(str(cell)))
39
+
40
+ top = "┌" + "┬".join("─" * (w + 2) for w in widths) + "┐"
41
+ sep = "├" + "┼".join("─" * (w + 2) for w in widths) + "┤"
42
+ bot = "└" + "┴".join("─" * (w + 2) for w in widths) + "┘"
43
+
44
+ def fmt_row(cells, bold=False):
45
+ parts = []
46
+ for i, w in enumerate(widths):
47
+ cell = str(cells[i]) if i < len(cells) else ""
48
+ if bold:
49
+ parts.append(f" {BOLD}{cell.ljust(w)}{RESET} ")
50
+ else:
51
+ parts.append(f" {cell.ljust(w)} ")
52
+ return "│" + "│".join(parts) + "│"
53
+
54
+ print(top)
55
+ print(fmt_row(headers, bold=True))
56
+ print(sep)
57
+ for row in rows:
58
+ print(fmt_row(row))
59
+ print(bot)
60
+
61
+ class Spinner:
62
+ def __init__(self):
63
+ self.active = False
64
+ self.thread = None
65
+ self.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
66
+
67
+ def start(self):
68
+ self.active = True
69
+ def spin():
70
+ i = 0
71
+ while self.active:
72
+ if not self.active:
73
+ break
74
+ print(f"\r{MAGENTA}{self.frames[i % len(self.frames)]}{RESET} ", end="", flush=True)
75
+ for _ in range(8): # Check active more frequently
76
+ if not self.active:
77
+ break
78
+ time.sleep(0.01)
79
+ i += 1
80
+ self.thread = threading.Thread(target=spin, daemon=True)
81
+ self.thread.start()
82
+
83
+ def stop(self):
84
+ self.active = False
85
+ if self.thread:
86
+ self.thread.join(timeout=0.2)
87
+ print(f"{CLEAR_LINE}", end="", flush=True)
88
+
89
+ def chat(url):
90
+ messages = []
91
+ endpoint = f"{url.rstrip('/')}/chat/cycls"
92
+
93
+ print(f"\n{MAGENTA}●{RESET} {BOLD}{url}{RESET}\n")
94
+
95
+ while True:
96
+ try:
97
+ user_input = input(f"{CYAN}❯{RESET} ")
98
+ if not user_input.strip():
99
+ continue
100
+
101
+ messages.append({"role": "user", "content": user_input})
102
+ print()
103
+
104
+ start_time = time.time()
105
+ in_thinking = False
106
+ table_headers = []
107
+ table_rows = []
108
+
109
+ with httpx.stream("POST", endpoint, json={"messages": messages}, timeout=None) as r:
110
+ for line in r.iter_lines():
111
+ if line.startswith("data: ") and line != "data: [DONE]":
112
+
113
+ data = json.loads(line[6:])
114
+ msg_type = data.get("type")
115
+
116
+ # Handle plain string or missing type
117
+ if msg_type is None:
118
+ # Could be OpenAI format or plain text
119
+ if isinstance(data, str):
120
+ print(data, end="", flush=True)
121
+ continue
122
+ elif "choices" in data:
123
+ # OpenAI format
124
+ content = data.get("choices", [{}])[0].get("delta", {}).get("content", "")
125
+ if content:
126
+ print(content, end="", flush=True)
127
+ continue
128
+ elif "text" in data:
129
+ print(data.get("text", ""), end="", flush=True)
130
+ continue
131
+ elif "content" in data:
132
+ print(data.get("content", ""), end="", flush=True)
133
+ continue
134
+ else:
135
+ # Debug: print raw data
136
+ print(f"[debug: {data}]", end="", flush=True)
137
+ continue
138
+
139
+ # Close thinking if switching
140
+ if msg_type != "thinking" and in_thinking:
141
+ print(f"</thinking>{RESET}\n", end="", flush=True)
142
+ in_thinking = False
143
+
144
+ # Flush table if switching
145
+ if msg_type != "table" and table_headers:
146
+ render_table(table_headers, table_rows)
147
+ table_headers = []
148
+ table_rows = []
149
+
150
+ if msg_type == "thinking":
151
+ if not in_thinking:
152
+ print(f"{DIM}<thinking>", end="", flush=True)
153
+ in_thinking = True
154
+ print(data.get("thinking", ""), end="", flush=True)
155
+
156
+ elif msg_type == "text":
157
+ print(data.get("text", ""), end="", flush=True)
158
+
159
+ elif msg_type == "code":
160
+ lang = data.get("language", "")
161
+ print(f"\n```{lang}\n{data.get('code', '')}\n```\n", end="", flush=True)
162
+
163
+ elif msg_type == "status":
164
+ print(f"{DIM}[{data.get('status', '')}]{RESET} ", end="", flush=True)
165
+
166
+ elif msg_type == "table":
167
+ if "headers" in data:
168
+ if table_headers:
169
+ render_table(table_headers, table_rows)
170
+ table_headers = data["headers"]
171
+ table_rows = []
172
+ elif "row" in data:
173
+ table_rows.append(data["row"])
174
+
175
+ elif msg_type == "callout":
176
+ style = data.get("style", "info")
177
+ icon, color = CALLOUT_STYLES.get(style, ("•", RESET))
178
+ title = data.get("title", "")
179
+ text = data.get("callout", "")
180
+ if title:
181
+ print(f"\n{color}{icon} {BOLD}{title}{RESET}")
182
+ print(f"{color} {text}{RESET}\n", end="", flush=True)
183
+ else:
184
+ print(f"\n{color}{icon} {text}{RESET}\n", end="", flush=True)
185
+
186
+ elif msg_type == "image":
187
+ print(f"{DIM}[image: {data.get('src', '')}]{RESET}", end="", flush=True)
188
+
189
+ # Flush remaining
190
+ if table_headers:
191
+ render_table(table_headers, table_rows)
192
+ if in_thinking:
193
+ print(f"</thinking>{RESET}", end="", flush=True)
194
+
195
+ elapsed = time.time() - start_time
196
+ print(f"\n\n{DIM}✦ {format_time(elapsed)}{RESET}\n")
197
+
198
+ except KeyboardInterrupt:
199
+ continue
200
+ except EOFError:
201
+ print(f"{RESET}\n👋")
202
+ break
203
+ except (httpx.ReadError, httpx.ConnectError):
204
+ print(f"{RESET}🔄 Reconnecting...", end="", flush=True)
205
+ time.sleep(1)
206
+ print(CLEAR_LINE, end="")
207
+ if messages:
208
+ messages.pop()
209
+
210
+ def main():
211
+ if len(sys.argv) < 3 or sys.argv[1] != "chat":
212
+ print("Usage: cycls chat <url>")
213
+ sys.exit(1)
214
+ chat(sys.argv[2])
215
+
216
+ if __name__ == "__main__":
217
+ main()
cycls/runtime.py CHANGED
@@ -178,9 +178,11 @@ class Runtime:
178
178
 
179
179
  def _generate_dockerfile(self, port=None) -> str:
180
180
  """Generates a multi-stage Dockerfile string."""
181
- # Only install extra packages not in base image
181
+ using_base = self.base_image == BASE_IMAGE
182
+
183
+ # Only install extra packages not in base image (use uv if available in base)
182
184
  run_pip_install = (
183
- f"RUN pip install --no-cache-dir {' '.join(self.pip_packages)}"
185
+ f"RUN uv pip install --system --no-cache {' '.join(self.pip_packages)}"
184
186
  if self.pip_packages else ""
185
187
  )
186
188
  run_apt_install = (
@@ -191,16 +193,19 @@ class Runtime:
191
193
  copy_lines = "\n".join([f"COPY context_files/{dst} {dst}" for dst in self.copy.values()])
192
194
  expose_line = f"EXPOSE {port}" if port else ""
193
195
 
196
+ # Skip env/mkdir/workdir if using pre-built base (already configured)
197
+ env_lines = "" if using_base else f"""ENV PIP_ROOT_USER_ACTION=ignore \\
198
+ PYTHONUNBUFFERED=1
199
+ RUN mkdir -p {self.io_dir}
200
+ WORKDIR /app"""
201
+
194
202
  return f"""
195
203
  # STAGE 1: Base image with all dependencies
196
204
  FROM {self.base_image} as base
197
- ENV PIP_ROOT_USER_ACTION=ignore
198
- ENV PYTHONUNBUFFERED=1
199
- RUN mkdir -p {self.io_dir}
205
+ {env_lines}
200
206
  {run_apt_install}
201
207
  {run_pip_install}
202
208
  {run_shell_commands}
203
- WORKDIR /app
204
209
  {copy_lines}
205
210
  COPY {self.runner_filename} {self.runner_path}
206
211
  ENTRYPOINT ["python", "{self.runner_path}", "{self.io_dir}"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycls
3
- Version: 0.0.2.70
3
+ Version: 0.0.2.72
4
4
  Summary: Distribute Intelligence
5
5
  Author: Mohammed J. AlRujayi
6
6
  Author-email: mj@cycls.com
@@ -1,12 +1,14 @@
1
1
  cycls/__init__.py,sha256=bVT0dYTXLdSC3ZURgtm-DEOj-VO6RUM6zGsJB0zuj6Y,61
2
2
  cycls/auth.py,sha256=xkndHZyCfnlertMMEKerCJjf23N3fVcTRVTTSXTTuzg,247
3
+ cycls/cli.py,sha256=AKf0z7ZLau3GvBVR_IhB7agmq4nVaHkcuUafNyvv2_A,7978
3
4
  cycls/default-theme/assets/index-B0ZKcm_V.css,sha256=wK9-NhEB8xPcN9Zv69zpOcfGTlFbMwyC9WqTmSKUaKw,6546
4
5
  cycls/default-theme/assets/index-D5EDcI4J.js,sha256=sN4qRcAXa7DBd9JzmVcCoCwH4l8cNCM-U9QGUjBvWSo,1346506
5
6
  cycls/default-theme/index.html,sha256=bM-yW_g0cGrV40Q5yY3ccY0fM4zI1Wuu5I8EtGFJIxs,828
6
7
  cycls/dev-theme/index.html,sha256=QJBHkdNuMMiwQU7o8dN8__8YQeQB45D37D-NCXIWB2Q,11585
7
- cycls/runtime.py,sha256=LgOrQ6Arh-GWSJqckfra-CvjeSekTvGvjHOBxu7JTQQ,21408
8
+ cycls/runtime.py,sha256=lg7XKHd9fLV_bYksHv2LHf3Lq7HPAC3K5Tr8pNgQ7sM,21641
8
9
  cycls/sdk.py,sha256=6oRKP44TJN9HKdNw9OYzDlZFDUMUhoCMt8TEwyu26dI,7368
9
10
  cycls/web.py,sha256=3M3qaWTNY3dpgd7Vq5aXREp-cIFsHrDqBQ1YkGrOaUk,4659
10
- cycls-0.0.2.70.dist-info/METADATA,sha256=CNYz8lGGGn3lIutuVV4oIy_6e0q4eg8ghkNIErNt5p4,8008
11
- cycls-0.0.2.70.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
12
- cycls-0.0.2.70.dist-info/RECORD,,
11
+ cycls-0.0.2.72.dist-info/METADATA,sha256=8co1QLXI-88tAFHSuaWeZchypHvODZVC1Qjlo9A_WDE,8008
12
+ cycls-0.0.2.72.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
13
+ cycls-0.0.2.72.dist-info/entry_points.txt,sha256=vEhqUxFhhuzCKWtq02LbMnT3wpUqdfgcM3Yh-jjXom8,40
14
+ cycls-0.0.2.72.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ cycls=cycls.cli:main
3
+