notte-sdk 1.4.0__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.
- notte_sdk/__init__.py +7 -0
- notte_sdk/client.py +36 -0
- notte_sdk/endpoints/__init__.py +0 -0
- notte_sdk/endpoints/agents.py +323 -0
- notte_sdk/endpoints/base.py +226 -0
- notte_sdk/endpoints/env.py +250 -0
- notte_sdk/endpoints/persona.py +285 -0
- notte_sdk/endpoints/sessions.py +372 -0
- notte_sdk/endpoints/vault.py +83 -0
- notte_sdk/errors.py +40 -0
- notte_sdk/py.typed +0 -0
- notte_sdk/types.py +812 -0
- notte_sdk/vault.py +73 -0
- notte_sdk-1.4.0.dist-info/METADATA +10 -0
- notte_sdk-1.4.0.dist-info/RECORD +16 -0
- notte_sdk-1.4.0.dist-info/WHEEL +4 -0
notte_sdk/__init__.py
ADDED
notte_sdk/client.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing_extensions import final
|
|
2
|
+
|
|
3
|
+
from notte_sdk.endpoints.agents import AgentsClient
|
|
4
|
+
from notte_sdk.endpoints.env import EnvClient
|
|
5
|
+
from notte_sdk.endpoints.persona import PersonaClient
|
|
6
|
+
from notte_sdk.endpoints.sessions import SessionsClient
|
|
7
|
+
from notte_sdk.endpoints.vault import VaultClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@final
|
|
11
|
+
class NotteClient:
|
|
12
|
+
"""
|
|
13
|
+
Client for the Notte API.
|
|
14
|
+
|
|
15
|
+
Note: this client is only able to handle one session at a time.
|
|
16
|
+
If you need to handle multiple sessions, you need to create a new client for each session.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
api_key: str | None = None,
|
|
22
|
+
verbose: bool = False,
|
|
23
|
+
):
|
|
24
|
+
"""Initialize a NotteClient instance.
|
|
25
|
+
|
|
26
|
+
Initializes the NotteClient with the specified API key and server URL, creating instances
|
|
27
|
+
of SessionsClient, AgentsClient, and EnvClient.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
api_key: Optional API key for authentication.
|
|
31
|
+
"""
|
|
32
|
+
self.sessions: SessionsClient = SessionsClient(api_key=api_key, verbose=verbose)
|
|
33
|
+
self.agents: AgentsClient = AgentsClient(api_key=api_key, verbose=verbose)
|
|
34
|
+
self.env: EnvClient = EnvClient(api_key=api_key, verbose=verbose)
|
|
35
|
+
self.persona: PersonaClient = PersonaClient(api_key=api_key, verbose=verbose)
|
|
36
|
+
self.vault: VaultClient = VaultClient(api_key=api_key, persona_client=self.persona, verbose=verbose)
|
|
File without changes
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import Unpack
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from loguru import logger
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from typing_extensions import final, override
|
|
9
|
+
|
|
10
|
+
from notte_sdk.endpoints.base import BaseClient, NotteEndpoint
|
|
11
|
+
from notte_sdk.types import (
|
|
12
|
+
AgentListRequest,
|
|
13
|
+
AgentResponse,
|
|
14
|
+
AgentRunRequest,
|
|
15
|
+
AgentRunRequestDict,
|
|
16
|
+
AgentStatus,
|
|
17
|
+
AgentStatusRequest,
|
|
18
|
+
AgentStatusRequestDict,
|
|
19
|
+
ListRequestDict,
|
|
20
|
+
)
|
|
21
|
+
from notte_sdk.types import AgentStatusResponse as _AgentStatusResponse
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# proxy for: StepAgentOutput
|
|
25
|
+
class _AgentResponse(BaseModel):
|
|
26
|
+
state: BaseModel
|
|
27
|
+
actions: list[BaseModel]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
AgentStatusResponse = _AgentStatusResponse[_AgentResponse]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@final
|
|
34
|
+
class AgentsClient(BaseClient):
|
|
35
|
+
"""
|
|
36
|
+
Client for the Notte API.
|
|
37
|
+
|
|
38
|
+
Note: this client is only able to handle one session at a time.
|
|
39
|
+
If you need to handle multiple sessions, you need to create a new client for each session.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# Session
|
|
43
|
+
AGENT_RUN = "run"
|
|
44
|
+
AGENT_STOP = "{agent_id}/stop"
|
|
45
|
+
AGENT_STATUS = "{agent_id}"
|
|
46
|
+
AGENT_LIST = ""
|
|
47
|
+
# The following endpoints downloads a .webp file
|
|
48
|
+
AGENT_REPLAY = "{agent_id}/replay"
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
api_key: str | None = None,
|
|
53
|
+
verbose: bool = False,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Initialize an AgentsClient instance.
|
|
57
|
+
|
|
58
|
+
Configures the client to use the "agents" endpoint path and sets optional API key and server URL for authentication and server configuration. The initial state has no recorded agent response.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
api_key: Optional API key for authenticating requests.
|
|
62
|
+
"""
|
|
63
|
+
super().__init__(base_endpoint_path="agents", api_key=api_key, verbose=verbose)
|
|
64
|
+
self._last_agent_response: AgentResponse | None = None
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def agent_run_endpoint() -> NotteEndpoint[AgentResponse]:
|
|
68
|
+
"""
|
|
69
|
+
Returns an endpoint for running an agent.
|
|
70
|
+
|
|
71
|
+
Creates a NotteEndpoint configured with the AGENT_RUN path, a POST method, and an expected AgentResponse.
|
|
72
|
+
"""
|
|
73
|
+
return NotteEndpoint(path=AgentsClient.AGENT_RUN, response=AgentResponse, method="POST")
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def agent_stop_endpoint(agent_id: str | None = None) -> NotteEndpoint[AgentResponse]:
|
|
77
|
+
"""
|
|
78
|
+
Constructs a DELETE endpoint for stopping an agent.
|
|
79
|
+
|
|
80
|
+
If an agent ID is provided, it is inserted into the endpoint URL. The returned
|
|
81
|
+
endpoint is configured with the DELETE HTTP method and expects an AgentStatusResponse.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
agent_id (str, optional): The identifier of the agent to stop. If omitted,
|
|
85
|
+
the URL template will remain unformatted.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
NotteEndpoint[AgentResponse]: The endpoint object for stopping the agent.
|
|
89
|
+
"""
|
|
90
|
+
path = AgentsClient.AGENT_STOP
|
|
91
|
+
if agent_id is not None:
|
|
92
|
+
path = path.format(agent_id=agent_id)
|
|
93
|
+
return NotteEndpoint(path=path, response=AgentStatusResponse, method="DELETE")
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def agent_status_endpoint(agent_id: str | None = None) -> NotteEndpoint[AgentStatusResponse]:
|
|
97
|
+
"""
|
|
98
|
+
Creates an endpoint for retrieving an agent's status.
|
|
99
|
+
|
|
100
|
+
If an agent ID is provided, formats the endpoint path to target that specific agent.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
agent_id: Optional identifier of the agent; if specified, the endpoint path will include this ID.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
NotteEndpoint configured with the GET method and AgentStatusResponse as the expected response.
|
|
107
|
+
"""
|
|
108
|
+
path = AgentsClient.AGENT_STATUS
|
|
109
|
+
if agent_id is not None:
|
|
110
|
+
path = path.format(agent_id=agent_id)
|
|
111
|
+
return NotteEndpoint(path=path, response=AgentStatusResponse, method="GET")
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def agent_replay_endpoint(agent_id: str | None = None) -> NotteEndpoint[BaseModel]:
|
|
115
|
+
"""
|
|
116
|
+
Creates an endpoint for downloading an agent's replay.
|
|
117
|
+
"""
|
|
118
|
+
path = AgentsClient.AGENT_REPLAY
|
|
119
|
+
if agent_id is not None:
|
|
120
|
+
path = path.format(agent_id=agent_id)
|
|
121
|
+
return NotteEndpoint(path=path, response=BaseModel, method="GET")
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def agent_list_endpoint(params: AgentListRequest | None = None) -> NotteEndpoint[AgentResponse]:
|
|
125
|
+
"""
|
|
126
|
+
Creates a NotteEndpoint for listing agents.
|
|
127
|
+
|
|
128
|
+
Returns an endpoint configured with the agent listing path and a GET method.
|
|
129
|
+
The optional params argument provides filtering or pagination details for the request.
|
|
130
|
+
"""
|
|
131
|
+
return NotteEndpoint(
|
|
132
|
+
path=AgentsClient.AGENT_LIST,
|
|
133
|
+
response=AgentResponse,
|
|
134
|
+
method="GET",
|
|
135
|
+
request=None,
|
|
136
|
+
params=params,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@override
|
|
140
|
+
@staticmethod
|
|
141
|
+
def endpoints() -> Sequence[NotteEndpoint[BaseModel]]:
|
|
142
|
+
"""
|
|
143
|
+
Returns a list of endpoints for agent operations.
|
|
144
|
+
|
|
145
|
+
Aggregates endpoints for running, stopping, checking status, and listing agents.
|
|
146
|
+
"""
|
|
147
|
+
return [
|
|
148
|
+
AgentsClient.agent_run_endpoint(),
|
|
149
|
+
AgentsClient.agent_stop_endpoint(),
|
|
150
|
+
AgentsClient.agent_status_endpoint(),
|
|
151
|
+
AgentsClient.agent_list_endpoint(),
|
|
152
|
+
AgentsClient.agent_replay_endpoint(),
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def agent_id(self) -> str | None:
|
|
157
|
+
"""
|
|
158
|
+
Returns the agent ID from the last agent response, or None if no response exists.
|
|
159
|
+
|
|
160
|
+
This property retrieves the identifier from the most recent agent operation response.
|
|
161
|
+
If no agent has been run or if the response is missing, it returns None.
|
|
162
|
+
"""
|
|
163
|
+
return self._last_agent_response.agent_id if self._last_agent_response is not None else None
|
|
164
|
+
|
|
165
|
+
def get_agent_id(self, agent_id: str | None = None) -> str:
|
|
166
|
+
"""
|
|
167
|
+
Retrieves the agent ID to be used for agent operations.
|
|
168
|
+
|
|
169
|
+
If an `agent_id` is provided, it is returned directly. Otherwise, the method attempts to obtain the agent ID from the client's last agent response. Raises a ValueError if no agent ID is available.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
agent_id (Optional[str]): An agent identifier. If omitted, the ID from the last agent response is used.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
ValueError: If no agent ID is provided and the client has no recorded agent response.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
str: The determined agent identifier.
|
|
179
|
+
"""
|
|
180
|
+
if agent_id is None:
|
|
181
|
+
if self._last_agent_response is None:
|
|
182
|
+
raise ValueError("No agent to get agent id from")
|
|
183
|
+
agent_id = self._last_agent_response.agent_id
|
|
184
|
+
return agent_id
|
|
185
|
+
|
|
186
|
+
def run(self, **data: Unpack[AgentRunRequestDict]) -> AgentResponse:
|
|
187
|
+
"""
|
|
188
|
+
Run an agent with the specified request parameters.
|
|
189
|
+
|
|
190
|
+
Validates the provided data using the AgentRunRequest model, sends a run request through the
|
|
191
|
+
designated endpoint, updates the last agent response, and returns the resulting AgentResponse.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
**data: Keyword arguments representing the fields of an AgentRunRequest.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
AgentResponse: The response obtained from the agent run request.
|
|
198
|
+
"""
|
|
199
|
+
request = AgentRunRequest.model_validate(data)
|
|
200
|
+
response = self.request(AgentsClient.agent_run_endpoint().with_request(request))
|
|
201
|
+
self._last_agent_response = response
|
|
202
|
+
return response
|
|
203
|
+
|
|
204
|
+
def wait_for_completion(
|
|
205
|
+
self,
|
|
206
|
+
agent_id: str | None = None,
|
|
207
|
+
polling_interval_seconds: int = 10,
|
|
208
|
+
max_attempts: int = 30,
|
|
209
|
+
) -> AgentStatusResponse:
|
|
210
|
+
"""
|
|
211
|
+
Waits for the specified agent to complete.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
agent_id: The identifier of the agent to wait for.
|
|
215
|
+
polling_interval_seconds: The interval between status checks.
|
|
216
|
+
max_attempts: The maximum number of attempts to check the agent's status.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
AgentStatusResponse: The response from the agent status check.
|
|
220
|
+
"""
|
|
221
|
+
agent_id = self.get_agent_id(agent_id)
|
|
222
|
+
last_step = 0
|
|
223
|
+
for _ in range(max_attempts):
|
|
224
|
+
response = self.status(agent_id=agent_id, replay=False)
|
|
225
|
+
if response.status == AgentStatus.closed:
|
|
226
|
+
return response
|
|
227
|
+
if len(response.steps) >= last_step:
|
|
228
|
+
for step in response.steps[last_step:]:
|
|
229
|
+
for action in step.actions:
|
|
230
|
+
logger.info(action.to_action().execution_message()) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType, reportAttributeAccessIssue]
|
|
231
|
+
last_step = len(response.steps)
|
|
232
|
+
logger.info(
|
|
233
|
+
f"Waiting {polling_interval_seconds} seconds for agent to complete (current step: {last_step})..."
|
|
234
|
+
)
|
|
235
|
+
time.sleep(polling_interval_seconds)
|
|
236
|
+
raise TimeoutError("Agent did not complete in time")
|
|
237
|
+
|
|
238
|
+
def close(self, agent_id: str) -> AgentResponse:
|
|
239
|
+
"""
|
|
240
|
+
Stops the specified agent and clears the last agent response.
|
|
241
|
+
|
|
242
|
+
Retrieves a valid agent identifier using the provided value or the last stored
|
|
243
|
+
response, sends a stop request to the API, resets the internal agent response,
|
|
244
|
+
and returns the resulting AgentResponse.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
agent_id: The identifier of the agent to stop.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
AgentResponse: The response from the stop operation.
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
ValueError: If a valid agent identifier cannot be determined.
|
|
254
|
+
"""
|
|
255
|
+
agent_id = self.get_agent_id(agent_id)
|
|
256
|
+
endpoint = AgentsClient.agent_stop_endpoint(agent_id=agent_id)
|
|
257
|
+
response = self.request(endpoint)
|
|
258
|
+
self._last_agent_response = None
|
|
259
|
+
return response
|
|
260
|
+
|
|
261
|
+
def status(self, **data: Unpack[AgentStatusRequestDict]) -> AgentStatusResponse:
|
|
262
|
+
"""
|
|
263
|
+
Retrieves the status of the specified agent.
|
|
264
|
+
|
|
265
|
+
Queries the API for the current status of an agent using a validated agent ID.
|
|
266
|
+
The provided ID is confirmed (or obtained from the last response if needed), and the
|
|
267
|
+
resulting status is stored internally before being returned.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
agent_id: Unique identifier of the agent to check.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
AgentResponse: The current status information of the specified agent.
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
ValueError: If no valid agent ID can be determined.
|
|
277
|
+
"""
|
|
278
|
+
agent_id = self.get_agent_id(data["agent_id"])
|
|
279
|
+
request = AgentStatusRequest.model_validate(data)
|
|
280
|
+
endpoint = AgentsClient.agent_status_endpoint(agent_id=agent_id).with_params(request)
|
|
281
|
+
response = self.request(endpoint)
|
|
282
|
+
self._last_agent_response = response
|
|
283
|
+
return response
|
|
284
|
+
|
|
285
|
+
def list(self, **data: Unpack[ListRequestDict]) -> Sequence[AgentResponse]:
|
|
286
|
+
"""
|
|
287
|
+
Lists agents matching specified criteria.
|
|
288
|
+
|
|
289
|
+
Validates the keyword arguments using the AgentListRequest model, constructs
|
|
290
|
+
the corresponding endpoint for listing agents, and returns a sequence of agent
|
|
291
|
+
responses.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
data: Arbitrary keyword arguments representing filter criteria for agents.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
A sequence of AgentResponse objects.
|
|
298
|
+
"""
|
|
299
|
+
params = AgentListRequest.model_validate(data)
|
|
300
|
+
endpoint = AgentsClient.agent_list_endpoint(params=params)
|
|
301
|
+
return self.request_list(endpoint)
|
|
302
|
+
|
|
303
|
+
def replay(
|
|
304
|
+
self,
|
|
305
|
+
agent_id: str | None = None,
|
|
306
|
+
output_file: str | None = None,
|
|
307
|
+
) -> bytes:
|
|
308
|
+
"""
|
|
309
|
+
Downloads the replay for the specified agent in webp format.
|
|
310
|
+
"""
|
|
311
|
+
agent_id = self.get_agent_id(agent_id)
|
|
312
|
+
endpoint = self.request_path(AgentsClient.agent_replay_endpoint(agent_id=agent_id))
|
|
313
|
+
response = requests.get(
|
|
314
|
+
url=endpoint,
|
|
315
|
+
headers=self.headers(),
|
|
316
|
+
timeout=self.DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
|
317
|
+
)
|
|
318
|
+
if b"not found" in response.content:
|
|
319
|
+
raise ValueError(f"Replay for agent {agent_id} is not available.")
|
|
320
|
+
if output_file is not None:
|
|
321
|
+
with open(output_file, "wb") as f:
|
|
322
|
+
_ = f.write(response.content)
|
|
323
|
+
return response.content
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Any, ClassVar, Generic, Literal, Self, TypeVar
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from notte_sdk.errors import AuthenticationError, NotteAPIError
|
|
11
|
+
|
|
12
|
+
TResponse = TypeVar("TResponse", bound=BaseModel, covariant=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NotteEndpoint(BaseModel, Generic[TResponse]):
|
|
16
|
+
path: str
|
|
17
|
+
response: type[TResponse]
|
|
18
|
+
request: BaseModel | None = None
|
|
19
|
+
method: Literal["GET", "POST", "DELETE"]
|
|
20
|
+
params: BaseModel | None = None
|
|
21
|
+
|
|
22
|
+
def with_request(self, request: BaseModel) -> Self:
|
|
23
|
+
# return deep copy of self with the request set
|
|
24
|
+
"""
|
|
25
|
+
Return a deep copy of the endpoint with the specified request.
|
|
26
|
+
|
|
27
|
+
Creates a new instance of the endpoint with its request attribute updated to the provided model.
|
|
28
|
+
The original instance remains unmodified.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
request: A Pydantic model instance carrying the request data.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A new endpoint instance with the updated request.
|
|
35
|
+
"""
|
|
36
|
+
return self.model_copy(update={"request": request})
|
|
37
|
+
|
|
38
|
+
def with_params(self, params: BaseModel) -> Self:
|
|
39
|
+
# return deep copy of self with the params set
|
|
40
|
+
"""
|
|
41
|
+
Return a new endpoint instance with updated parameters.
|
|
42
|
+
|
|
43
|
+
Creates a copy of the current endpoint with its "params" attribute set to the provided
|
|
44
|
+
Pydantic model.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
params: A Pydantic model instance containing the new parameters.
|
|
48
|
+
"""
|
|
49
|
+
return self.model_copy(update={"params": params})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BaseClient(ABC):
|
|
53
|
+
DEFAULT_NOTTE_API_URL: ClassVar[str] = "https://staging.notte.cc"
|
|
54
|
+
DEFAULT_REQUEST_TIMEOUT_SECONDS: ClassVar[int] = 60
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
base_endpoint_path: str | None,
|
|
59
|
+
api_key: str | None = None,
|
|
60
|
+
verbose: bool = False,
|
|
61
|
+
):
|
|
62
|
+
"""
|
|
63
|
+
Initialize a new API client instance.
|
|
64
|
+
|
|
65
|
+
Sets up the client by resolving an API key from the provided parameter or the
|
|
66
|
+
NOTTE_API_KEY environment variable. Selects the server URL (defaulting to a
|
|
67
|
+
preconfigured server if none is provided), initializes a mapping of endpoints
|
|
68
|
+
using the implemented 'endpoints' method, and stores an optional base endpoint
|
|
69
|
+
path for constructing request URLs.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
base_endpoint_path: Optional base path to be prefixed to endpoint URLs.
|
|
73
|
+
api_key: Optional API key for authentication; if not supplied, retrieved from
|
|
74
|
+
the NOTTE_API_KEY environment variable.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
AuthenticationError: If an API key is neither provided nor available in the environment.
|
|
78
|
+
"""
|
|
79
|
+
token = api_key or os.getenv("NOTTE_API_KEY")
|
|
80
|
+
if token is None:
|
|
81
|
+
raise AuthenticationError("NOTTE_API_KEY needs to be provided")
|
|
82
|
+
self.token: str = token
|
|
83
|
+
self.server_url: str = os.getenv("NOTTE_API_URL") or self.DEFAULT_NOTTE_API_URL
|
|
84
|
+
self._endpoints: dict[str, NotteEndpoint[BaseModel]] = {
|
|
85
|
+
endpoint.path: endpoint for endpoint in self.endpoints()
|
|
86
|
+
}
|
|
87
|
+
self.base_endpoint_path: str | None = base_endpoint_path
|
|
88
|
+
self.verbose: bool = verbose
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def endpoints() -> Sequence[NotteEndpoint[BaseModel]]:
|
|
93
|
+
"""
|
|
94
|
+
Return API endpoints for the client.
|
|
95
|
+
|
|
96
|
+
This abstract method should be implemented by subclasses to supply the list of available
|
|
97
|
+
NotteEndpoint instances for the client.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Sequence[NotteEndpoint[BaseModel]]: A list of endpoints for the client.
|
|
101
|
+
"""
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
def headers(self) -> dict[str, str]:
|
|
105
|
+
"""
|
|
106
|
+
Return HTTP headers for authenticated API requests.
|
|
107
|
+
|
|
108
|
+
Constructs and returns a dictionary containing the 'Authorization' header,
|
|
109
|
+
which is formatted as a Bearer token using the API key stored in self.token.
|
|
110
|
+
"""
|
|
111
|
+
return {"Authorization": f"Bearer {self.token}"}
|
|
112
|
+
|
|
113
|
+
def request_path(self, endpoint: NotteEndpoint[TResponse]) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Constructs the full request URL for the given API endpoint.
|
|
116
|
+
|
|
117
|
+
If a base endpoint path is defined, the URL is formed by concatenating the server URL,
|
|
118
|
+
the base endpoint path, and the endpoint's path. Otherwise, the endpoint's path is appended
|
|
119
|
+
directly to the server URL.
|
|
120
|
+
"""
|
|
121
|
+
if self.base_endpoint_path is None:
|
|
122
|
+
return f"{self.server_url}/{endpoint.path}"
|
|
123
|
+
return f"{self.server_url}/{self.base_endpoint_path}/{endpoint.path}"
|
|
124
|
+
|
|
125
|
+
def _request(self, endpoint: NotteEndpoint[TResponse]) -> requests.Response:
|
|
126
|
+
"""
|
|
127
|
+
Executes an HTTP request for the given API endpoint.
|
|
128
|
+
|
|
129
|
+
Constructs the full URL and headers from the endpoint's configuration and issues an HTTP
|
|
130
|
+
request using the specified method (GET, POST, or DELETE). For POST requests, a request model
|
|
131
|
+
must be provided; otherwise, a ValueError is raised. If the response status code is not 200 or
|
|
132
|
+
the JSON response contains an error detail, a NotteAPIError is raised.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
endpoint: An API endpoint instance containing the HTTP method, path, optional request model,
|
|
136
|
+
and query parameters.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The JSON-decoded response from the API.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
ValueError: If a POST request is attempted without a request model.
|
|
143
|
+
NotteAPIError: If the API response indicates a failure.
|
|
144
|
+
"""
|
|
145
|
+
headers = self.headers()
|
|
146
|
+
url = self.request_path(endpoint)
|
|
147
|
+
params = endpoint.params.model_dump() if endpoint.params is not None else None
|
|
148
|
+
if self.verbose:
|
|
149
|
+
logger.info(f"Making `{endpoint.method}` request to `{endpoint.path} (i.e `{url}`) with params `{params}`.")
|
|
150
|
+
match endpoint.method:
|
|
151
|
+
case "GET":
|
|
152
|
+
response = requests.get(
|
|
153
|
+
url=url,
|
|
154
|
+
headers=headers,
|
|
155
|
+
params=params,
|
|
156
|
+
timeout=self.DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
|
157
|
+
)
|
|
158
|
+
case "POST":
|
|
159
|
+
if endpoint.request is None:
|
|
160
|
+
raise ValueError("Request model is required for POST requests")
|
|
161
|
+
response = requests.post(
|
|
162
|
+
url=url,
|
|
163
|
+
headers=headers,
|
|
164
|
+
json=endpoint.request.model_dump(),
|
|
165
|
+
params=params,
|
|
166
|
+
timeout=self.DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
|
167
|
+
)
|
|
168
|
+
case "DELETE":
|
|
169
|
+
response = requests.delete(
|
|
170
|
+
url=url,
|
|
171
|
+
headers=headers,
|
|
172
|
+
params=params,
|
|
173
|
+
timeout=self.DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
|
174
|
+
)
|
|
175
|
+
if response.status_code != 200:
|
|
176
|
+
raise NotteAPIError(path=endpoint.path, response=response)
|
|
177
|
+
response_dict: Any = response.json()
|
|
178
|
+
if "detail" in response_dict:
|
|
179
|
+
raise NotteAPIError(path=endpoint.path, response=response)
|
|
180
|
+
return response_dict
|
|
181
|
+
|
|
182
|
+
def request(self, endpoint: NotteEndpoint[TResponse]) -> TResponse:
|
|
183
|
+
"""
|
|
184
|
+
Requests the specified API endpoint and returns the validated response.
|
|
185
|
+
|
|
186
|
+
This method sends an HTTP request according to the endpoint configuration and
|
|
187
|
+
validates that the response is a dictionary. It then parses the response using the
|
|
188
|
+
endpoint's associated response model. If the response is not a dictionary, a
|
|
189
|
+
NotteAPIError is raised.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
endpoint: The API endpoint configuration containing request details and the
|
|
193
|
+
expected response model.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
The validated response parsed using the endpoint's response model.
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
NotteAPIError: If the API response is not a dictionary.
|
|
200
|
+
"""
|
|
201
|
+
response: Any = self._request(endpoint)
|
|
202
|
+
if not isinstance(response, dict):
|
|
203
|
+
raise NotteAPIError(path=endpoint.path, response=response)
|
|
204
|
+
return endpoint.response.model_validate(response)
|
|
205
|
+
|
|
206
|
+
def request_list(self, endpoint: NotteEndpoint[TResponse]) -> Sequence[TResponse]:
|
|
207
|
+
# Handle the case where TResponse is a list of BaseModel
|
|
208
|
+
"""
|
|
209
|
+
Retrieves and validates a list of responses from the API.
|
|
210
|
+
|
|
211
|
+
This method sends a request using the provided endpoint and expects the response to be a list. Each item is validated
|
|
212
|
+
against the model defined in the endpoint. A NotteAPIError is raised if the response is not a list.
|
|
213
|
+
|
|
214
|
+
Parameters:
|
|
215
|
+
endpoint: The API endpoint containing the path and the expected response model.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
A list of validated response items.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
NotteAPIError: If the response is not a list.
|
|
222
|
+
"""
|
|
223
|
+
response_list: Any = self._request(endpoint)
|
|
224
|
+
if not isinstance(response_list, list):
|
|
225
|
+
raise NotteAPIError(path=endpoint.path, response=response_list)
|
|
226
|
+
return [endpoint.response.model_validate(item) for item in response_list] # pyright: ignore[reportUnknownVariableType]
|