llama-deploy-core 0.3.0a9__py3-none-any.whl → 0.3.0a11__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.
@@ -0,0 +1,219 @@
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
+
18
+
19
+ class ClientError(Exception):
20
+ """Base class for client errors."""
21
+
22
+ def __init__(self, message: str) -> None:
23
+ super().__init__(message)
24
+
25
+
26
+ class BaseClient:
27
+ def __init__(self, base_url: str) -> None:
28
+ self.base_url = base_url.rstrip("/")
29
+ self.client = httpx.Client(
30
+ base_url=self.base_url,
31
+ event_hooks={"response": [self._handle_response]},
32
+ )
33
+ self.hookless_client = httpx.Client(base_url=self.base_url)
34
+
35
+ def _handle_response(self, response: httpx.Response) -> None:
36
+ try:
37
+ response.raise_for_status()
38
+ except httpx.HTTPStatusError as e:
39
+ try:
40
+ response.read()
41
+ error_data = e.response.json()
42
+ if isinstance(error_data, dict) and "detail" in error_data:
43
+ error_message = error_data["detail"]
44
+ else:
45
+ error_message = str(error_data)
46
+ except (ValueError, KeyError):
47
+ error_message = e.response.text
48
+ raise ClientError(f"HTTP {e.response.status_code}: {error_message}") from e
49
+ except httpx.RequestError as e:
50
+ raise ClientError(f"Request failed: {e}") from e
51
+
52
+
53
+ class ControlPlaneClient(BaseClient):
54
+ """Unscoped client for non-project endpoints."""
55
+
56
+ def health_check(self) -> dict:
57
+ response = self.client.get("/health")
58
+ return response.json()
59
+
60
+ def server_version(self) -> dict:
61
+ response = self.client.get("/version")
62
+ return response.json()
63
+
64
+ def list_projects(self) -> List[ProjectSummary]:
65
+ response = self.client.get("/api/v1beta1/deployments/list-projects")
66
+ projects_response = ProjectsListResponse.model_validate(response.json())
67
+ return [project for project in projects_response.projects]
68
+
69
+
70
+ class ProjectClient(BaseClient):
71
+ """Project-scoped client for deployment operations."""
72
+
73
+ def __init__(
74
+ self,
75
+ base_url: str,
76
+ project_id: str,
77
+ ) -> None:
78
+ super().__init__(base_url)
79
+ self.project_id = project_id
80
+
81
+ def list_deployments(self) -> List[DeploymentResponse]:
82
+ response = self.client.get(
83
+ "/api/v1beta1/deployments",
84
+ params={"project_id": self.project_id},
85
+ )
86
+ deployments_response = DeploymentsListResponse.model_validate(response.json())
87
+ return [deployment for deployment in deployments_response.deployments]
88
+
89
+ def get_deployment(
90
+ self, deployment_id: str, include_events: bool = False
91
+ ) -> DeploymentResponse:
92
+ response = self.client.get(
93
+ f"/api/v1beta1/deployments/{deployment_id}",
94
+ params={"project_id": self.project_id, "include_events": include_events},
95
+ )
96
+ return DeploymentResponse.model_validate(response.json())
97
+
98
+ def create_deployment(
99
+ self, deployment_data: DeploymentCreate
100
+ ) -> DeploymentResponse:
101
+ response = self.client.post(
102
+ "/api/v1beta1/deployments",
103
+ params={"project_id": self.project_id},
104
+ json=deployment_data.model_dump(exclude_none=True),
105
+ )
106
+ return DeploymentResponse.model_validate(response.json())
107
+
108
+ def delete_deployment(self, deployment_id: str) -> None:
109
+ self.client.delete(
110
+ f"/api/v1beta1/deployments/{deployment_id}",
111
+ params={"project_id": self.project_id},
112
+ )
113
+
114
+ def update_deployment(
115
+ self,
116
+ deployment_id: str,
117
+ update_data: DeploymentUpdate,
118
+ ) -> DeploymentResponse:
119
+ response = self.client.patch(
120
+ f"/api/v1beta1/deployments/{deployment_id}",
121
+ params={"project_id": self.project_id},
122
+ json=update_data.model_dump(),
123
+ )
124
+ return DeploymentResponse.model_validate(response.json())
125
+
126
+ def validate_repository(
127
+ self,
128
+ repo_url: str,
129
+ deployment_id: str | None = None,
130
+ pat: str | None = None,
131
+ ) -> RepositoryValidationResponse:
132
+ response = self.client.post(
133
+ "/api/v1beta1/deployments/validate-repository",
134
+ params={"project_id": self.project_id},
135
+ json=RepositoryValidationRequest(
136
+ repository_url=repo_url,
137
+ deployment_id=deployment_id,
138
+ pat=pat,
139
+ ).model_dump(),
140
+ )
141
+ return RepositoryValidationResponse.model_validate(response.json())
142
+
143
+ def stream_deployment_logs(
144
+ self,
145
+ deployment_id: str,
146
+ *,
147
+ include_init_containers: bool = False,
148
+ since_seconds: int | None = None,
149
+ tail_lines: int | None = None,
150
+ ) -> tuple["Closer", Iterator[LogEvent]]:
151
+ """Stream logs as LogEvent items from the control plane using SSE.
152
+
153
+ This yields `LogEvent` models until the stream ends (e.g. rollout).
154
+ """
155
+ # Use a separate client without response hooks so we don't consume the stream
156
+
157
+ params = {
158
+ "project_id": self.project_id,
159
+ "include_init_containers": include_init_containers,
160
+ }
161
+ if since_seconds is not None:
162
+ params["since_seconds"] = since_seconds
163
+ if tail_lines is not None:
164
+ params["tail_lines"] = tail_lines
165
+
166
+ url = f"/api/v1beta1/deployments/{deployment_id}/logs"
167
+ headers = {"Accept": "text/event-stream"}
168
+
169
+ stack = contextlib.ExitStack()
170
+ response = stack.enter_context(
171
+ self.hookless_client.stream(
172
+ "GET", url, params=params, headers=headers, timeout=None
173
+ )
174
+ )
175
+ try:
176
+ response.raise_for_status()
177
+ except Exception:
178
+ stack.close()
179
+ raise
180
+
181
+ return stack.close, _iterate_log_stream(response, stack.close)
182
+
183
+
184
+ def _iterate_log_stream(
185
+ response: httpx.Response, closer: "Closer"
186
+ ) -> Iterator[LogEvent]:
187
+ event_name: str | None = None
188
+ data_lines: list[str] = []
189
+
190
+ try:
191
+ for line in response.iter_lines():
192
+ if line is None:
193
+ continue
194
+ line = line.decode() if isinstance(line, (bytes, bytearray)) else line
195
+ print("got line", line)
196
+ if line.startswith("event:"):
197
+ event_name = line[len("event:") :].strip()
198
+ elif line.startswith("data:"):
199
+ data_lines.append(line[len("data:") :].lstrip())
200
+ elif line.strip() == "":
201
+ if event_name == "log" and data_lines:
202
+ data_str = "\n".join(data_lines)
203
+ try:
204
+ yield LogEvent.model_validate_json(data_str)
205
+ print("yielded log event", data_str)
206
+ except Exception:
207
+ # If parsing fails, skip malformed event
208
+ pass
209
+ # reset for next event
210
+ event_name = None
211
+ data_lines = []
212
+ finally:
213
+ try:
214
+ closer()
215
+ except Exception:
216
+ pass
217
+
218
+
219
+ type Closer = callable[tuple[()], None]
File without changes
@@ -0,0 +1,10 @@
1
+ from ._abstract_deployments_service import AbstractDeploymentsService
2
+ from ._create_deployments_router import create_v1beta1_deployments_router
3
+ from ._exceptions import DeploymentNotFoundError, ReplicaSetNotFoundError
4
+
5
+ __all__ = [
6
+ "AbstractDeploymentsService",
7
+ "create_v1beta1_deployments_router",
8
+ "DeploymentNotFoundError",
9
+ "ReplicaSetNotFoundError",
10
+ ]
@@ -0,0 +1,149 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import AsyncGenerator, cast
3
+
4
+ from llama_deploy.core import schema
5
+ from llama_deploy.core.schema.base import LogEvent
6
+ from llama_deploy.core.schema.deployments import DeploymentResponse
7
+
8
+
9
+ class AbstractDeploymentsService(ABC):
10
+ @abstractmethod
11
+ async def validate_repository(
12
+ self,
13
+ project_id: str,
14
+ request: schema.RepositoryValidationRequest,
15
+ ) -> schema.RepositoryValidationResponse:
16
+ """
17
+ Validate repository access and return unified response.
18
+ """
19
+ ...
20
+
21
+ @abstractmethod
22
+ async def create_deployment(
23
+ self,
24
+ project_id: str,
25
+ deployment_data: schema.DeploymentCreate,
26
+ ) -> DeploymentResponse:
27
+ """
28
+ Create a new deployment
29
+
30
+ Args:
31
+ project_id: The ID of the project to create the deployment in
32
+ deployment_data: The data for the deployment
33
+
34
+ Returns:
35
+ The created deployment
36
+ Raises:
37
+ DeploymentNotFoundError: If the deployment ID is not found
38
+ """
39
+ ...
40
+
41
+ @abstractmethod
42
+ async def get_deployments(
43
+ self,
44
+ project_id: str,
45
+ ) -> schema.DeploymentsListResponse:
46
+ """
47
+ Get a list of deployments for a project
48
+
49
+ Args:
50
+ project_id: The ID of the project to get the deployments for
51
+
52
+ Returns:
53
+ A list of deployments
54
+ """
55
+ ...
56
+
57
+ @abstractmethod
58
+ async def get_deployment(
59
+ self,
60
+ project_id: str,
61
+ deployment_id: str,
62
+ include_events: bool = False,
63
+ ) -> schema.DeploymentResponse | None:
64
+ """
65
+ Get a deployment by ID
66
+
67
+ Args:
68
+ project_id: The ID of the project to get the deployment for
69
+ deployment_id: The ID of the deployment to get
70
+ include_events: Whether to include events in the response
71
+
72
+ Returns:
73
+ The deployment
74
+ Raises:
75
+ DeploymentNotFoundError: If the deployment ID is not found
76
+ """
77
+ ...
78
+
79
+ @abstractmethod
80
+ async def delete_deployment(
81
+ self,
82
+ project_id: str,
83
+ deployment_id: str,
84
+ ) -> None:
85
+ """
86
+ Delete a deployment
87
+
88
+ Args:
89
+ project_id: The ID of the project to delete the deployment from
90
+ deployment_id: The ID of the deployment to delete
91
+
92
+ Returns:
93
+ None
94
+ Raises:
95
+ DeploymentNotFoundError: If the deployment ID is not found
96
+ """
97
+ ...
98
+
99
+ @abstractmethod
100
+ async def update_deployment(
101
+ self,
102
+ project_id: str,
103
+ deployment_id: str,
104
+ update_data: schema.DeploymentUpdate,
105
+ ) -> DeploymentResponse:
106
+ """
107
+ Update a deployment
108
+
109
+ Args:
110
+ project_id: The ID of the project to update the deployment in
111
+ deployment_id: The ID of the deployment to update
112
+ update_data: The data to update the deployment with
113
+
114
+ Returns:
115
+ The updated deployment
116
+ Raises:
117
+ DeploymentNotFoundError: If the deployment ID is not found
118
+ """
119
+ ...
120
+
121
+ @abstractmethod
122
+ async def stream_deployment_logs(
123
+ self,
124
+ project_id: str,
125
+ deployment_id: str,
126
+ include_init_containers: bool = False,
127
+ since_seconds: int | None = None,
128
+ tail_lines: int | None = None,
129
+ ) -> AsyncGenerator[LogEvent, None]:
130
+ """
131
+ Stream the logs for a deployment
132
+
133
+ Args:
134
+ project_id: The ID of the project to stream the logs for
135
+ deployment_id: The ID of the deployment to stream the logs for
136
+ include_init_containers: Whether to include init containers in the logs
137
+ since_seconds: The number of seconds to stream the logs for
138
+ tail_lines: The number of lines to stream the logs for
139
+
140
+ Returns:
141
+ A generator of log events
142
+ Raises:
143
+ DeploymentNotFoundError: If the deployment ID is not found
144
+ """
145
+ # This method is abstract. The following unreachable code ensures type
146
+ # checkers treat it as an async generator, so call sites can `async for`.
147
+ raise NotImplementedError
148
+ if False:
149
+ yield cast(LogEvent, None)
@@ -0,0 +1,167 @@
1
+ import logging
2
+ from typing import Awaitable, Callable
3
+
4
+ from fastapi import APIRouter, Depends, HTTPException, Request, Response
5
+ from fastapi.params import Query
6
+ from fastapi.responses import StreamingResponse
7
+ from llama_deploy.control_plane import k8s_client
8
+ from llama_deploy.core import schema
9
+ from typing_extensions import Annotated
10
+
11
+ from ._abstract_deployments_service import AbstractDeploymentsService
12
+ from ._exceptions import DeploymentNotFoundError, ReplicaSetNotFoundError
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ async def get_project_id(project_id: Annotated[str, Query()]) -> str:
18
+ return project_id
19
+
20
+
21
+ def create_v1beta1_deployments_router(
22
+ deployments_service: AbstractDeploymentsService,
23
+ get_project_id: Callable[[str], Awaitable[str]] = get_project_id,
24
+ ) -> APIRouter:
25
+ router = APIRouter(prefix="/api/v1beta1/deployments", tags=["v1beta1-deployments"])
26
+
27
+ @router.get("/list-projects")
28
+ async def get_projects() -> schema.ProjectsListResponse:
29
+ """Get all unique projects with their deployment counts"""
30
+ projects_data = await k8s_client.get_projects_with_deployment_count()
31
+ return schema.ProjectsListResponse(projects=projects_data)
32
+
33
+ @router.post("/validate-repository")
34
+ async def validate_repository(
35
+ project_id: Annotated[str, Depends(get_project_id)],
36
+ request: schema.RepositoryValidationRequest,
37
+ ) -> schema.RepositoryValidationResponse:
38
+ """Validate repository access and return unified response."""
39
+ return await deployments_service.validate_repository(
40
+ project_id=project_id,
41
+ request=request,
42
+ )
43
+
44
+ @router.post("", response_model=schema.DeploymentResponse)
45
+ async def create_deployment(
46
+ project_id: Annotated[str, Depends(get_project_id)],
47
+ deployment_data: schema.DeploymentCreate,
48
+ ) -> Response:
49
+ deployment_response = await deployments_service.create_deployment(
50
+ project_id=project_id,
51
+ deployment_data=deployment_data,
52
+ )
53
+ # Return deployment response with warning header if there are git issues
54
+
55
+ response = Response(
56
+ content=deployment_response.model_dump_json(),
57
+ status_code=201,
58
+ media_type="application/json",
59
+ )
60
+ return response
61
+
62
+ @router.get("")
63
+ async def get_deployments(
64
+ project_id: Annotated[str, Depends(get_project_id)],
65
+ ) -> schema.DeploymentsListResponse:
66
+ return await deployments_service.get_deployments(project_id=project_id)
67
+
68
+ @router.get("/{deployment_id}")
69
+ async def get_deployment(
70
+ project_id: Annotated[str, Depends(get_project_id)],
71
+ deployment_id: str,
72
+ include_events: Annotated[bool, Query()] = False,
73
+ ) -> schema.DeploymentResponse:
74
+ deployment = await deployments_service.get_deployment(
75
+ project_id=project_id,
76
+ deployment_id=deployment_id,
77
+ include_events=include_events,
78
+ )
79
+ if deployment is None:
80
+ raise HTTPException(
81
+ status_code=404,
82
+ detail=f"Deployment with id {deployment_id} not found",
83
+ )
84
+
85
+ return deployment
86
+
87
+ @router.delete("/{deployment_id}")
88
+ async def delete_deployment(
89
+ project_id: Annotated[str, Depends(get_project_id)],
90
+ deployment_id: str,
91
+ ) -> None:
92
+ await deployments_service.delete_deployment(
93
+ project_id=project_id, deployment_id=deployment_id
94
+ )
95
+
96
+ @router.patch("/{deployment_id}", response_model=schema.DeploymentResponse)
97
+ async def update_deployment(
98
+ project_id: Annotated[str, Depends(get_project_id)],
99
+ deployment_id: str,
100
+ update_data: schema.DeploymentUpdate,
101
+ ) -> Response:
102
+ """Update an existing deployment with patch-style changes
103
+
104
+ Args:
105
+ project_id: The project ID
106
+ deployment_id: The deployment ID to update
107
+ update_data: The patch-style update data
108
+ """
109
+
110
+ deployment_response = await deployments_service.update_deployment(
111
+ project_id=project_id,
112
+ deployment_id=deployment_id,
113
+ update_data=update_data,
114
+ )
115
+
116
+ response = Response(
117
+ content=deployment_response.model_dump_json(),
118
+ status_code=200,
119
+ media_type="application/json",
120
+ )
121
+ return response
122
+
123
+ @router.get("/{deployment_id}/logs")
124
+ async def stream_deployment_logs(
125
+ request: Request,
126
+ project_id: Annotated[str, Depends(get_project_id)],
127
+ deployment_id: str,
128
+ include_init_containers: Annotated[bool, Query()] = False,
129
+ since_seconds: Annotated[int | None, Query()] = None,
130
+ tail_lines: Annotated[int | None, Query()] = None,
131
+ ):
132
+ """Stream logs for the latest ReplicaSet of a deployment.
133
+
134
+ The stream ends when the latest ReplicaSet changes (e.g., a new rollout occurs).
135
+ """
136
+
137
+ try:
138
+ inner = deployments_service.stream_deployment_logs(
139
+ project_id=project_id,
140
+ deployment_id=deployment_id,
141
+ include_init_containers=include_init_containers,
142
+ since_seconds=since_seconds,
143
+ tail_lines=tail_lines,
144
+ )
145
+
146
+ async def sse_lines():
147
+ async for data in inner:
148
+ yield "event: log\n"
149
+ yield f"data: {data.model_dump_json()}\n\n"
150
+
151
+ return StreamingResponse(
152
+ sse_lines(),
153
+ media_type="text/event-stream",
154
+ headers={
155
+ "Cache-Control": "no-cache",
156
+ "Connection": "keep-alive",
157
+ "X-Accel-Buffering": "no",
158
+ },
159
+ )
160
+
161
+ except DeploymentNotFoundError as e:
162
+ raise HTTPException(status_code=404, detail=str(e))
163
+ except ReplicaSetNotFoundError as e:
164
+ # Deployment exists but hasn't created a ReplicaSet yet
165
+ raise HTTPException(status_code=409, detail=str(e))
166
+
167
+ return router
@@ -0,0 +1,14 @@
1
+ class DeploymentNotFoundError(Exception):
2
+ """
3
+ Raised when a deployment is not found
4
+ """
5
+
6
+ pass
7
+
8
+
9
+ class ReplicaSetNotFoundError(Exception):
10
+ """
11
+ May be raised if a deployment does not set have a replica set
12
+ """
13
+
14
+ pass
@@ -1,11 +1,17 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: llama-deploy-core
3
- Version: 0.3.0a9
3
+ Version: 0.3.0a11
4
4
  Summary: Core models and schemas for LlamaDeploy
5
5
  License: MIT
6
+ Requires-Dist: fastapi>=0.115.0
6
7
  Requires-Dist: pydantic>=2.0.0
7
8
  Requires-Dist: pyyaml>=6.0.2
9
+ Requires-Dist: types-pyyaml>=6.0.12.20250822
10
+ Requires-Dist: httpx>=0.27.0 ; extra == 'client'
11
+ Requires-Dist: fastapi>=0.115.0 ; extra == 'server'
8
12
  Requires-Python: >=3.12, <4
13
+ Provides-Extra: client
14
+ Provides-Extra: server
9
15
  Description-Content-Type: text/markdown
10
16
 
11
17
  > [!WARNING]
@@ -1,14 +1,20 @@
1
1
  llama_deploy/core/__init__.py,sha256=112612bf2e928c2e0310d6556bb13fc28c00db70297b90a8527486cd2562e408,43
2
+ llama_deploy/core/client/manage_client.py,sha256=c098a12def4ece9897d1d5d70c69bf2bd7140de5ec970cae30a5927901e2a3da,7499
2
3
  llama_deploy/core/config.py,sha256=69bb0ea8ac169eaa4e808cd60a098b616bddd3145d26c6c35e56db38496b0e6a,35
3
4
  llama_deploy/core/deployment_config.py,sha256=ff10cc96f2c64abc4761eb83c5372fd22f3770159b45503818264723b578de4e,15092
4
5
  llama_deploy/core/git/git_util.py,sha256=c581c1da13871b4e89eda58f56ddb074139454c06ae9b04c0b396fdb2b9a5176,9193
5
6
  llama_deploy/core/path_util.py,sha256=14d50c0c337c8450ed46cafc88436027056b365a48370a69cdb76c88d7c26fd1,798
7
+ llama_deploy/core/py.typed,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
6
8
  llama_deploy/core/schema/__init__.py,sha256=cc60a6fb54983d7ca13e2cc86d414a0d006a79c20e44344701f9fbe3b1d21577,739
7
9
  llama_deploy/core/schema/base.py,sha256=c02e33e35e7e4540b3065a82267febeb6da169222210a1d1c2479f6a7f1c6a4b,802
8
10
  llama_deploy/core/schema/deployments.py,sha256=1e310548f6847ee000b06d655b3fec006148bd2994b4c6b7d073582a7c312ec1,6392
9
11
  llama_deploy/core/schema/git_validation.py,sha256=27b306aa6ecabe58cab6381d92551545f263fe7550c58b3087115410bc71fd21,1915
10
12
  llama_deploy/core/schema/projects.py,sha256=c97eda38207d80354c2ee3a237cba9c3f6838148197cfa2d97b9a18d3da1a38b,294
13
+ llama_deploy/core/server/manage_api/__init__.py,sha256=ed814d76fcade150d43205631b12bcae0b06bc2b8456a81fee24d6cf867adbc8,370
14
+ llama_deploy/core/server/manage_api/_abstract_deployments_service.py,sha256=1bb1fbe904f84f892f092fb82d931cb2c2ca6a7563a64fb6ab52be21c02d75e5,4290
15
+ llama_deploy/core/server/manage_api/_create_deployments_router.py,sha256=9b3fd2eeae027e3095da4db1785cdcc971b49023de615efe13b0a9e5f649982d,6081
16
+ llama_deploy/core/server/manage_api/_exceptions.py,sha256=ee71cd9c2354a665e6905cd9cc752d2d65f71f0b936d33fec3c1c5229c38accf,246
11
17
  llama_deploy/core/ui_build.py,sha256=290dafa951918e5593b9035570fa4c66791d7e5ea785bd372ad11e99e8283857,1514
12
- llama_deploy_core-0.3.0a9.dist-info/WHEEL,sha256=66530aef82d5020ef5af27ae0123c71abb9261377c5bc519376c671346b12918,79
13
- llama_deploy_core-0.3.0a9.dist-info/METADATA,sha256=97185524565321b30c322e7f5b3346489ee77bd659f8c19427d5cee21ce058ab,402
14
- llama_deploy_core-0.3.0a9.dist-info/RECORD,,
18
+ llama_deploy_core-0.3.0a11.dist-info/WHEEL,sha256=66530aef82d5020ef5af27ae0123c71abb9261377c5bc519376c671346b12918,79
19
+ llama_deploy_core-0.3.0a11.dist-info/METADATA,sha256=c8e44fcfa8c82bb7d0a3c2fa07f6fd8381e30f3876f01deb929dc199d9b5260d,627
20
+ llama_deploy_core-0.3.0a11.dist-info/RECORD,,