Skip to content

Commit 35f693c

Browse files
chore: tighten typing across plus_api client
Adds typed containers for wire payloads, literal aliases for HTTP method and log type, and Ffnal markers on resource constants. Updates upstream returns in project_utils.py and deploy/main.py to match the new contracts.
1 parent da15554 commit 35f693c

4 files changed

Lines changed: 232 additions & 73 deletions

File tree

lib/cli/src/crewai_cli/deploy/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Any
22

3+
from crewai_core.plus_api import CreateCrewPayload
34
from rich.console import Console
45

56
from crewai_cli import git
@@ -161,7 +162,7 @@ def _create_payload(
161162
self,
162163
env_vars: dict[str, str],
163164
remote_repo_url: str,
164-
) -> dict[str, Any]:
165+
) -> CreateCrewPayload:
165166
"""
166167
Create the payload for crew creation.
167168
@@ -172,6 +173,8 @@ def _create_payload(
172173
Returns:
173174
Dict[str, Any]: The payload for crew creation.
174175
"""
176+
if not self.project_name:
177+
raise ValueError("project_name is required to create a deployment payload")
175178
return {
176179
"deploy": {
177180
"name": self.project_name,

lib/crewai-core/src/crewai_core/plus_api.py

Lines changed: 174 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,161 @@
33
from __future__ import annotations
44

55
import os
6-
from typing import Any
6+
from typing import Any, Final, Literal, TypedDict, cast
77
from urllib.parse import urljoin
88

99
import httpx
10+
from typing_extensions import NotRequired
1011

1112
from crewai_core.constants import DEFAULT_CREWAI_ENTERPRISE_URL
1213
from crewai_core.settings import Settings
1314
from crewai_core.version import get_crewai_version
1415

1516

17+
HttpMethod = Literal["GET", "POST", "PATCH", "DELETE"]
18+
19+
20+
class AvailableExport(TypedDict):
21+
name: str
22+
23+
24+
class EnvVarEntry(TypedDict):
25+
name: str
26+
description: str
27+
required: bool
28+
default: str | None
29+
30+
31+
class ToolMetadata(TypedDict):
32+
name: str
33+
module: str
34+
humanized_name: str
35+
description: str
36+
run_params_schema: dict[str, Any]
37+
init_params_schema: dict[str, Any]
38+
env_vars: list[EnvVarEntry]
39+
40+
41+
class ToolsMetadataPayload(TypedDict):
42+
package: str
43+
tools: list[ToolMetadata] | None
44+
45+
46+
class PublishToolPayload(TypedDict):
47+
handle: str
48+
public: bool
49+
version: str
50+
file: str
51+
description: str | None
52+
available_exports: list[AvailableExport] | None
53+
tools_metadata: ToolsMetadataPayload | None
54+
55+
56+
class CrewDeploymentSpec(TypedDict):
57+
name: str
58+
repo_clone_url: str
59+
env: dict[str, str]
60+
61+
62+
class CreateCrewPayload(TypedDict):
63+
deploy: CrewDeploymentSpec
64+
65+
66+
class _WithUserIdentifier(TypedDict):
67+
user_identifier: NotRequired[str]
68+
69+
70+
class LoginPayload(_WithUserIdentifier):
71+
pass
72+
73+
74+
class TraceExecutionContext(TypedDict):
75+
crew_fingerprint: str | None
76+
crew_name: str | None
77+
flow_name: str | None
78+
crewai_version: str
79+
privacy_level: str
80+
81+
82+
class TraceExecutionMetadata(TypedDict):
83+
expected_duration_estimate: int
84+
agent_count: int
85+
task_count: int
86+
flow_method_count: int
87+
execution_started_at: str
88+
89+
90+
class TraceBatchInitPayload(_WithUserIdentifier):
91+
trace_id: str
92+
execution_type: str
93+
execution_context: TraceExecutionContext
94+
execution_metadata: TraceExecutionMetadata
95+
ephemeral_trace_id: NotRequired[str]
96+
97+
98+
class TraceBatchMetadata(TypedDict):
99+
events_count: int
100+
batch_sequence: int
101+
is_final_batch: bool
102+
103+
104+
class TraceEventsPayload(TypedDict):
105+
events: list[dict[str, Any]]
106+
batch_metadata: TraceBatchMetadata
107+
108+
109+
class TraceFinalizePayload(TypedDict):
110+
status: Literal["completed"]
111+
duration_ms: float | None
112+
final_event_count: int
113+
114+
115+
class TraceFailedPayload(TypedDict):
116+
status: Literal["failed"]
117+
failure_reason: str
118+
119+
120+
Headers = TypedDict(
121+
"Headers",
122+
{
123+
"Content-Type": str,
124+
"User-Agent": str,
125+
"X-Crewai-Version": str,
126+
"Authorization": NotRequired[str],
127+
"X-Crewai-Organization-Id": NotRequired[str],
128+
},
129+
)
130+
131+
132+
class RequestKwargs(TypedDict):
133+
headers: dict[str, str]
134+
json: NotRequired[Any]
135+
params: NotRequired[dict[str, str]]
136+
timeout: NotRequired[float]
137+
138+
16139
class PlusAPI:
17140
"""Client for working with the CrewAI+ API."""
18141

19-
TOOLS_RESOURCE = "/crewai_plus/api/v1/tools"
20-
ORGANIZATIONS_RESOURCE = "/crewai_plus/api/v1/me/organizations"
21-
CREWS_RESOURCE = "/crewai_plus/api/v1/crews"
22-
AGENTS_RESOURCE = "/crewai_plus/api/v1/agents"
23-
TRACING_RESOURCE = "/crewai_plus/api/v1/tracing"
24-
EPHEMERAL_TRACING_RESOURCE = "/crewai_plus/api/v1/tracing/ephemeral"
25-
INTEGRATIONS_RESOURCE = "/crewai_plus/api/v1/integrations"
142+
TOOLS_RESOURCE: Final = "/crewai_plus/api/v1/tools"
143+
ORGANIZATIONS_RESOURCE: Final = "/crewai_plus/api/v1/me/organizations"
144+
CREWS_RESOURCE: Final = "/crewai_plus/api/v1/crews"
145+
AGENTS_RESOURCE: Final = "/crewai_plus/api/v1/agents"
146+
TRACING_RESOURCE: Final = "/crewai_plus/api/v1/tracing"
147+
EPHEMERAL_TRACING_RESOURCE: Final = "/crewai_plus/api/v1/tracing/ephemeral"
148+
INTEGRATIONS_RESOURCE: Final = "/crewai_plus/api/v1/integrations"
26149

27150
def __init__(self, api_key: str | None = None) -> None:
151+
version = get_crewai_version()
28152
self.api_key = api_key
29-
self.headers = {
153+
self.headers: Headers = {
30154
"Content-Type": "application/json",
31-
"User-Agent": f"CrewAI-CLI/{get_crewai_version()}",
32-
"X-Crewai-Version": get_crewai_version(),
155+
"User-Agent": f"CrewAI-CLI/{version}",
156+
"X-Crewai-Version": version,
33157
}
34158
if api_key:
35159
self.headers["Authorization"] = f"Bearer {api_key}"
160+
36161
settings = Settings()
37162
if settings.org_uuid:
38163
self.headers["X-Crewai-Organization-Id"] = settings.org_uuid
@@ -44,17 +169,30 @@ def __init__(self, api_key: str | None = None) -> None:
44169
)
45170

46171
def _make_request(
47-
self, method: str, endpoint: str, **kwargs: Any
172+
self,
173+
method: HttpMethod,
174+
endpoint: str,
175+
*,
176+
json: Any = None,
177+
params: dict[str, str] | None = None,
178+
timeout: float | None = None,
179+
verify: bool = True,
48180
) -> httpx.Response:
49181
url = urljoin(self.base_url, endpoint)
50-
verify = kwargs.pop("verify", True)
182+
request_kwargs: RequestKwargs = {"headers": cast(dict[str, str], self.headers)}
183+
if json is not None:
184+
request_kwargs["json"] = json
185+
if params is not None:
186+
request_kwargs["params"] = params
187+
if timeout is not None:
188+
request_kwargs["timeout"] = timeout
51189
with httpx.Client(trust_env=False, verify=verify) as client:
52-
return client.request(method, url, headers=self.headers, **kwargs)
190+
return client.request(method, url, **request_kwargs)
53191

54192
def login_to_tool_repository(
55193
self, user_identifier: str | None = None
56194
) -> httpx.Response:
57-
payload = {}
195+
payload: LoginPayload = {}
58196
if user_identifier:
59197
payload["user_identifier"] = user_identifier
60198
return self._make_request("POST", f"{self.TOOLS_RESOURCE}/login", json=payload)
@@ -65,7 +203,7 @@ def get_tool(self, handle: str) -> httpx.Response:
65203
async def get_agent(self, handle: str) -> httpx.Response:
66204
url = urljoin(self.base_url, f"{self.AGENTS_RESOURCE}/{handle}")
67205
async with httpx.AsyncClient() as client:
68-
return await client.get(url, headers=self.headers)
206+
return await client.get(url, headers=cast(dict[str, str], self.headers))
69207

70208
def publish_tool(
71209
self,
@@ -74,10 +212,10 @@ def publish_tool(
74212
version: str,
75213
description: str | None,
76214
encoded_file: str,
77-
available_exports: list[dict[str, Any]] | None = None,
78-
tools_metadata: list[dict[str, Any]] | None = None,
215+
available_exports: list[AvailableExport] | None = None,
216+
tools_metadata: list[ToolMetadata] | None = None,
79217
) -> httpx.Response:
80-
params = {
218+
params: PublishToolPayload = {
81219
"handle": handle,
82220
"public": is_public,
83221
"version": version,
@@ -129,13 +267,13 @@ def delete_crew_by_uuid(self, uuid: str) -> httpx.Response:
129267
def list_crews(self) -> httpx.Response:
130268
return self._make_request("GET", self.CREWS_RESOURCE)
131269

132-
def create_crew(self, payload: dict[str, Any]) -> httpx.Response:
270+
def create_crew(self, payload: CreateCrewPayload) -> httpx.Response:
133271
return self._make_request("POST", self.CREWS_RESOURCE, json=payload)
134272

135273
def get_organizations(self) -> httpx.Response:
136274
return self._make_request("GET", self.ORGANIZATIONS_RESOURCE)
137275

138-
def initialize_trace_batch(self, payload: dict[str, Any]) -> httpx.Response:
276+
def initialize_trace_batch(self, payload: TraceBatchInitPayload) -> httpx.Response:
139277
return self._make_request(
140278
"POST",
141279
f"{self.TRACING_RESOURCE}/batches",
@@ -144,7 +282,7 @@ def initialize_trace_batch(self, payload: dict[str, Any]) -> httpx.Response:
144282
)
145283

146284
def initialize_ephemeral_trace_batch(
147-
self, payload: dict[str, Any]
285+
self, payload: TraceBatchInitPayload
148286
) -> httpx.Response:
149287
return self._make_request(
150288
"POST",
@@ -153,7 +291,7 @@ def initialize_ephemeral_trace_batch(
153291
)
154292

155293
def send_trace_events(
156-
self, trace_batch_id: str, payload: dict[str, Any]
294+
self, trace_batch_id: str, payload: TraceEventsPayload
157295
) -> httpx.Response:
158296
return self._make_request(
159297
"POST",
@@ -163,7 +301,7 @@ def send_trace_events(
163301
)
164302

165303
def send_ephemeral_trace_events(
166-
self, trace_batch_id: str, payload: dict[str, Any]
304+
self, trace_batch_id: str, payload: TraceEventsPayload
167305
) -> httpx.Response:
168306
return self._make_request(
169307
"POST",
@@ -173,7 +311,7 @@ def send_ephemeral_trace_events(
173311
)
174312

175313
def finalize_trace_batch(
176-
self, trace_batch_id: str, payload: dict[str, Any]
314+
self, trace_batch_id: str, payload: TraceFinalizePayload
177315
) -> httpx.Response:
178316
return self._make_request(
179317
"PATCH",
@@ -183,7 +321,7 @@ def finalize_trace_batch(
183321
)
184322

185323
def finalize_ephemeral_trace_batch(
186-
self, trace_batch_id: str, payload: dict[str, Any]
324+
self, trace_batch_id: str, payload: TraceFinalizePayload
187325
) -> httpx.Response:
188326
return self._make_request(
189327
"PATCH",
@@ -195,20 +333,28 @@ def finalize_ephemeral_trace_batch(
195333
def mark_trace_batch_as_failed(
196334
self, trace_batch_id: str, error_message: str
197335
) -> httpx.Response:
336+
payload: TraceFailedPayload = {
337+
"status": "failed",
338+
"failure_reason": error_message,
339+
}
198340
return self._make_request(
199341
"PATCH",
200342
f"{self.TRACING_RESOURCE}/batches/{trace_batch_id}",
201-
json={"status": "failed", "failure_reason": error_message},
343+
json=payload,
202344
timeout=30,
203345
)
204346

205347
def mark_ephemeral_trace_batch_as_failed(
206348
self, trace_batch_id: str, error_message: str
207349
) -> httpx.Response:
350+
payload: TraceFailedPayload = {
351+
"status": "failed",
352+
"failure_reason": error_message,
353+
}
208354
return self._make_request(
209355
"PATCH",
210356
f"{self.EPHEMERAL_TRACING_RESOURCE}/batches/{trace_batch_id}",
211-
json={"status": "failed", "failure_reason": error_message},
357+
json=payload,
212358
timeout=30,
213359
)
214360

0 commit comments

Comments
 (0)