llamactl 0.3.0a10__tar.gz → 0.3.0a12__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.
Files changed (27) hide show
  1. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/PKG-INFO +4 -4
  2. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/pyproject.toml +5 -4
  3. llamactl-0.3.0a12/src/llama_deploy/cli/client.py +34 -0
  4. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/textual/deployment_monitor.py +1 -1
  5. llamactl-0.3.0a10/src/llama_deploy/cli/client.py +0 -275
  6. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/README.md +0 -0
  7. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/__init__.py +0 -0
  8. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/app.py +0 -0
  9. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/commands/aliased_group.py +0 -0
  10. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/commands/deployment.py +0 -0
  11. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/commands/init.py +0 -0
  12. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/commands/profile.py +0 -0
  13. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/commands/serve.py +0 -0
  14. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/config.py +0 -0
  15. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/debug.py +0 -0
  16. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/env.py +0 -0
  17. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/interactive_prompts/utils.py +0 -0
  18. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/options.py +0 -0
  19. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/py.typed +0 -0
  20. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/textual/deployment_form.py +0 -0
  21. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/textual/deployment_help.py +0 -0
  22. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/textual/git_validation.py +0 -0
  23. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/textual/github_callback_server.py +0 -0
  24. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/textual/llama_loader.py +0 -0
  25. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/textual/profile_form.py +0 -0
  26. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/textual/secrets_form.py +0 -0
  27. {llamactl-0.3.0a10 → llamactl-0.3.0a12}/src/llama_deploy/cli/textual/styles.tcss +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: llamactl
3
- Version: 0.3.0a10
3
+ Version: 0.3.0a12
4
4
  Summary: A command-line interface for managing LlamaDeploy projects and deployments
5
5
  Author: Adrian Lyjak
6
6
  Author-email: Adrian Lyjak <adrianlyjak@gmail.com>
7
7
  License: MIT
8
- Requires-Dist: llama-deploy-core>=0.3.0a10,<0.4.0
9
- Requires-Dist: llama-deploy-appserver>=0.3.0a10,<0.4.0
8
+ Requires-Dist: llama-deploy-core[client]>=0.3.0a12,<0.4.0
9
+ Requires-Dist: llama-deploy-appserver>=0.3.0a12,<0.4.0
10
10
  Requires-Dist: httpx>=0.24.0
11
11
  Requires-Dist: rich>=13.0.0
12
12
  Requires-Dist: questionary>=2.0.0
@@ -16,7 +16,7 @@ Requires-Dist: tenacity>=9.1.2
16
16
  Requires-Dist: textual>=5.3.0
17
17
  Requires-Dist: aiohttp>=3.12.14
18
18
  Requires-Dist: copier>=9.9.0
19
- Requires-Python: >=3.12, <4
19
+ Requires-Python: >=3.11, <4
20
20
  Description-Content-Type: text/markdown
21
21
 
22
22
  # llamactl
@@ -1,16 +1,16 @@
1
1
  [project]
2
2
  name = "llamactl"
3
- version = "0.3.0a10"
3
+ version = "0.3.0a12"
4
4
  description = "A command-line interface for managing LlamaDeploy projects and deployments"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
7
7
  authors = [
8
8
  { name = "Adrian Lyjak", email = "adrianlyjak@gmail.com" }
9
9
  ]
10
- requires-python = ">=3.12, <4"
10
+ requires-python = ">=3.11, <4"
11
11
  dependencies = [
12
- "llama-deploy-core>=0.3.0a10,<0.4.0",
13
- "llama-deploy-appserver>=0.3.0a10,<0.4.0",
12
+ "llama-deploy-core[client]>=0.3.0a12,<0.4.0",
13
+ "llama-deploy-appserver>=0.3.0a12,<0.4.0",
14
14
  "httpx>=0.24.0",
15
15
  "rich>=13.0.0",
16
16
  "questionary>=2.0.0",
@@ -41,3 +41,4 @@ module-name = "llama_deploy.cli"
41
41
 
42
42
  [tool.uv.sources]
43
43
  llama-deploy-appserver = { workspace = true }
44
+ llama-deploy-core = { workspace = true }
@@ -0,0 +1,34 @@
1
+ from llama_deploy.cli.config import config_manager
2
+ from llama_deploy.core.client.manage_client import ControlPlaneClient, ProjectClient
3
+ from rich import print as rprint
4
+
5
+
6
+ def get_control_plane_client(base_url: str | None = None) -> ControlPlaneClient:
7
+ profile = config_manager.get_current_profile()
8
+ if not profile and not base_url:
9
+ rprint("\n[bold red]No profile configured![/bold red]")
10
+ rprint("\nTo get started, create a profile with:")
11
+ rprint("[cyan]llamactl profile create[/cyan]")
12
+ raise SystemExit(1)
13
+ resolved_base_url = (base_url or (profile.api_url if profile else "")).rstrip("/")
14
+ if not resolved_base_url:
15
+ raise ValueError("API URL is required")
16
+ return ControlPlaneClient(resolved_base_url)
17
+
18
+
19
+ def get_project_client(
20
+ base_url: str | None = None, project_id: str | None = None
21
+ ) -> ProjectClient:
22
+ profile = config_manager.get_current_profile()
23
+ if not profile:
24
+ rprint("\n[bold red]No profile configured![/bold red]")
25
+ rprint("\nTo get started, create a profile with:")
26
+ rprint("[cyan]llamactl profile create[/cyan]")
27
+ raise SystemExit(1)
28
+ resolved_base_url = (base_url or profile.api_url or "").rstrip("/")
29
+ if not resolved_base_url:
30
+ raise ValueError("API URL is required")
31
+ resolved_project_id = project_id or profile.active_project_id
32
+ if not resolved_project_id:
33
+ raise ValueError("Project ID is required")
34
+ return ProjectClient(resolved_base_url, resolved_project_id)
@@ -9,8 +9,8 @@ import time
9
9
  from pathlib import Path
10
10
  from typing import Iterator
11
11
 
12
- from llama_deploy.cli.client import Closer
13
12
  from llama_deploy.cli.client import get_project_client as get_client
13
+ from llama_deploy.core.client.manage_client import Closer
14
14
  from llama_deploy.core.schema.base import LogEvent
15
15
  from llama_deploy.core.schema.deployments import DeploymentResponse
16
16
  from rich.text import Text
@@ -1,275 +0,0 @@
1
- import contextlib
2
- from typing import Iterator, List
3
-
4
- import httpx
5
- from llama_deploy.core.schema.base import LogEvent
6
- from llama_deploy.core.schema.deployments import (
7
- DeploymentCreate,
8
- DeploymentResponse,
9
- DeploymentsListResponse,
10
- DeploymentUpdate,
11
- )
12
- from llama_deploy.core.schema.git_validation import (
13
- RepositoryValidationRequest,
14
- RepositoryValidationResponse,
15
- )
16
- from llama_deploy.core.schema.projects import ProjectsListResponse, ProjectSummary
17
- from rich.console import Console
18
-
19
- from .config import config_manager
20
-
21
-
22
- class ClientError(Exception):
23
- """Base class for client errors."""
24
-
25
- def __init__(self, message: str) -> None:
26
- super().__init__(message)
27
-
28
-
29
- class BaseClient:
30
- def __init__(self, base_url: str, console: Console) -> None:
31
- self.base_url = base_url.rstrip("/")
32
- self.console = console
33
- self.client = httpx.Client(
34
- base_url=self.base_url,
35
- event_hooks={"response": [self._handle_response]},
36
- )
37
- self.hookless_client = httpx.Client(base_url=self.base_url)
38
-
39
- def _handle_response(self, response: httpx.Response) -> None:
40
- try:
41
- response.raise_for_status()
42
- except httpx.HTTPStatusError as e:
43
- try:
44
- response.read()
45
- error_data = e.response.json()
46
- if isinstance(error_data, dict) and "detail" in error_data:
47
- error_message = error_data["detail"]
48
- else:
49
- error_message = str(error_data)
50
- except (ValueError, KeyError):
51
- error_message = e.response.text
52
- raise ClientError(f"HTTP {e.response.status_code}: {error_message}") from e
53
- except httpx.RequestError as e:
54
- raise ClientError(f"Request failed: {e}") from e
55
-
56
-
57
- class ControlPlaneClient(BaseClient):
58
- """Unscoped client for non-project endpoints."""
59
-
60
- def health_check(self) -> dict:
61
- response = self.client.get("/health")
62
- return response.json()
63
-
64
- def server_version(self) -> dict:
65
- response = self.client.get("/version")
66
- return response.json()
67
-
68
- def list_projects(self) -> List[ProjectSummary]:
69
- response = self.client.get("/api/v1beta1/deployments/list-projects")
70
- projects_response = ProjectsListResponse.model_validate(response.json())
71
- return [project for project in projects_response.projects]
72
-
73
-
74
- class ProjectClient(BaseClient):
75
- """Project-scoped client for deployment operations."""
76
-
77
- def __init__(
78
- self,
79
- base_url: str | None = None,
80
- project_id: str | None = None,
81
- console: Console | None = None,
82
- ) -> None:
83
- # Allow default construction using active profile (for tests and convenience)
84
- if base_url is None or project_id is None:
85
- profile = config_manager.get_current_profile()
86
- if not profile:
87
- # Match previous behavior for missing profiles
88
- (console or Console()).print(
89
- "\n[bold red]No profile configured![/bold red]"
90
- )
91
- (console or Console()).print("\nTo get started, create a profile with:")
92
- (console or Console()).print("[cyan]llamactl profile create[/cyan]")
93
- raise SystemExit(1)
94
- base_url = base_url or profile.api_url or ""
95
- project_id = project_id or profile.active_project_id
96
- if not base_url:
97
- raise ValueError("API URL is required")
98
- if not project_id:
99
- raise ValueError("Project ID is required")
100
- resolved_console = console or Console()
101
- super().__init__(base_url, resolved_console)
102
- self.project_id = project_id
103
-
104
- def list_deployments(self) -> List[DeploymentResponse]:
105
- response = self.client.get(
106
- "/api/v1beta1/deployments",
107
- params={"project_id": self.project_id},
108
- )
109
- deployments_response = DeploymentsListResponse.model_validate(response.json())
110
- return [deployment for deployment in deployments_response.deployments]
111
-
112
- def get_deployment(
113
- self, deployment_id: str, include_events: bool = False
114
- ) -> DeploymentResponse:
115
- response = self.client.get(
116
- f"/api/v1beta1/deployments/{deployment_id}",
117
- params={"project_id": self.project_id, "include_events": include_events},
118
- )
119
- return DeploymentResponse.model_validate(response.json())
120
-
121
- def create_deployment(
122
- self, deployment_data: DeploymentCreate
123
- ) -> DeploymentResponse:
124
- response = self.client.post(
125
- "/api/v1beta1/deployments",
126
- params={"project_id": self.project_id},
127
- json=deployment_data.model_dump(exclude_none=True),
128
- )
129
- return DeploymentResponse.model_validate(response.json())
130
-
131
- def delete_deployment(self, deployment_id: str) -> None:
132
- self.client.delete(
133
- f"/api/v1beta1/deployments/{deployment_id}",
134
- params={"project_id": self.project_id},
135
- )
136
-
137
- def update_deployment(
138
- self,
139
- deployment_id: str,
140
- update_data: DeploymentUpdate,
141
- ) -> DeploymentResponse:
142
- response = self.client.patch(
143
- f"/api/v1beta1/deployments/{deployment_id}",
144
- params={"project_id": self.project_id},
145
- json=update_data.model_dump(),
146
- )
147
- return DeploymentResponse.model_validate(response.json())
148
-
149
- def validate_repository(
150
- self,
151
- repo_url: str,
152
- deployment_id: str | None = None,
153
- pat: str | None = None,
154
- ) -> RepositoryValidationResponse:
155
- response = self.client.post(
156
- "/api/v1beta1/deployments/validate-repository",
157
- params={"project_id": self.project_id},
158
- json=RepositoryValidationRequest(
159
- repository_url=repo_url,
160
- deployment_id=deployment_id,
161
- pat=pat,
162
- ).model_dump(),
163
- )
164
- return RepositoryValidationResponse.model_validate(response.json())
165
-
166
- def stream_deployment_logs(
167
- self,
168
- deployment_id: str,
169
- *,
170
- include_init_containers: bool = False,
171
- since_seconds: int | None = None,
172
- tail_lines: int | None = None,
173
- ) -> tuple["Closer", Iterator[LogEvent]]:
174
- """Stream logs as LogEvent items from the control plane using SSE.
175
-
176
- This yields `LogEvent` models until the stream ends (e.g. rollout).
177
- """
178
- # Use a separate client without response hooks so we don't consume the stream
179
-
180
- params = {
181
- "project_id": self.project_id,
182
- "include_init_containers": include_init_containers,
183
- }
184
- if since_seconds is not None:
185
- params["since_seconds"] = since_seconds
186
- if tail_lines is not None:
187
- params["tail_lines"] = tail_lines
188
-
189
- url = f"/api/v1beta1/deployments/{deployment_id}/logs"
190
- headers = {"Accept": "text/event-stream"}
191
-
192
- stack = contextlib.ExitStack()
193
- response = stack.enter_context(
194
- self.hookless_client.stream(
195
- "GET", url, params=params, headers=headers, timeout=None
196
- )
197
- )
198
- try:
199
- response.raise_for_status()
200
- except Exception:
201
- stack.close()
202
- raise
203
-
204
- return stack.close, _iterate_log_stream(response, stack.close)
205
-
206
-
207
- def _iterate_log_stream(
208
- response: httpx.Response, closer: "Closer"
209
- ) -> Iterator[LogEvent]:
210
- event_name: str | None = None
211
- data_lines: list[str] = []
212
-
213
- try:
214
- for line in response.iter_lines():
215
- if line is None:
216
- continue
217
- line = line.decode() if isinstance(line, (bytes, bytearray)) else line
218
- print("got line", line)
219
- if line.startswith("event:"):
220
- event_name = line[len("event:") :].strip()
221
- elif line.startswith("data:"):
222
- data_lines.append(line[len("data:") :].lstrip())
223
- elif line.strip() == "":
224
- if event_name == "log" and data_lines:
225
- data_str = "\n".join(data_lines)
226
- try:
227
- yield LogEvent.model_validate_json(data_str)
228
- print("yielded log event", data_str)
229
- except Exception:
230
- # If parsing fails, skip malformed event
231
- pass
232
- # reset for next event
233
- event_name = None
234
- data_lines = []
235
- finally:
236
- try:
237
- closer()
238
- except Exception:
239
- pass
240
-
241
-
242
- def get_control_plane_client(base_url: str | None = None) -> ControlPlaneClient:
243
- console = Console()
244
- profile = config_manager.get_current_profile()
245
- if not profile and not base_url:
246
- console.print("\n[bold red]No profile configured![/bold red]")
247
- console.print("\nTo get started, create a profile with:")
248
- console.print("[cyan]llamactl profile create[/cyan]")
249
- raise SystemExit(1)
250
- resolved_base_url = (base_url or (profile.api_url if profile else "")).rstrip("/")
251
- if not resolved_base_url:
252
- raise ValueError("API URL is required")
253
- return ControlPlaneClient(resolved_base_url, console)
254
-
255
-
256
- def get_project_client(
257
- base_url: str | None = None, project_id: str | None = None
258
- ) -> ProjectClient:
259
- console = Console()
260
- profile = config_manager.get_current_profile()
261
- if not profile:
262
- console.print("\n[bold red]No profile configured![/bold red]")
263
- console.print("\nTo get started, create a profile with:")
264
- console.print("[cyan]llamactl profile create[/cyan]")
265
- raise SystemExit(1)
266
- resolved_base_url = (base_url or profile.api_url or "").rstrip("/")
267
- if not resolved_base_url:
268
- raise ValueError("API URL is required")
269
- resolved_project_id = project_id or profile.active_project_id
270
- if not resolved_project_id:
271
- raise ValueError("Project ID is required")
272
- return ProjectClient(resolved_base_url, resolved_project_id, console)
273
-
274
-
275
- type Closer = callable[tuple[()], None]
File without changes