latitude-sdk 3.0.2__tar.gz → 4.0.0b1__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 (50) hide show
  1. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/PKG-INFO +8 -6
  2. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/README.md +7 -5
  3. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/pyproject.toml +1 -1
  4. latitude_sdk-4.0.0b1/src/latitude_sdk/client/client.py +276 -0
  5. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/client/payloads.py +16 -0
  6. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/client/router.py +10 -1
  7. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/sdk/latitude.py +3 -0
  8. latitude_sdk-4.0.0b1/src/latitude_sdk/sdk/projects.py +41 -0
  9. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/sdk/prompts.py +86 -113
  10. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/sdk/types.py +17 -15
  11. latitude_sdk-4.0.0b1/tests/projects/create_test.py +101 -0
  12. latitude_sdk-4.0.0b1/tests/projects/get_all_test.py +137 -0
  13. latitude_sdk-4.0.0b1/tests/prompts/__init__.py +0 -0
  14. latitude_sdk-4.0.0b1/tests/prompts/chat_test.py +300 -0
  15. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/prompts/run_test.py +22 -354
  16. latitude_sdk-4.0.0b1/tests/test_acceptance.py +314 -0
  17. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/utils/fixtures.py +61 -188
  18. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/uv.lock +176 -175
  19. latitude_sdk-3.0.2/src/latitude_sdk/client/client.py +0 -140
  20. latitude_sdk-3.0.2/tests/prompts/chat_test.py +0 -575
  21. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/.gitignore +0 -0
  22. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/.python-version +0 -0
  23. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/LICENSE.md +0 -0
  24. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/scripts/format.py +0 -0
  25. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/scripts/lint.py +0 -0
  26. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/scripts/test.py +0 -0
  27. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/__init__.py +0 -0
  28. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/client/__init__.py +0 -0
  29. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/env/__init__.py +0 -0
  30. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/env/env.py +0 -0
  31. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/py.typed +0 -0
  32. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/sdk/__init__.py +0 -0
  33. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/sdk/errors.py +0 -0
  34. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/sdk/evaluations.py +0 -0
  35. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/sdk/logs.py +0 -0
  36. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/util/__init__.py +0 -0
  37. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/src/latitude_sdk/util/utils.py +0 -0
  38. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/__init__.py +0 -0
  39. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/evaluations/__init__.py +0 -0
  40. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/evaluations/annotate_test.py +0 -0
  41. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/logs/__init__.py +0 -0
  42. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/logs/create_test.py +0 -0
  43. {latitude_sdk-3.0.2/tests/prompts → latitude_sdk-4.0.0b1/tests/projects}/__init__.py +0 -0
  44. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/prompts/get_all_test.py +0 -0
  45. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/prompts/get_or_create_test.py +0 -0
  46. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/prompts/get_test.py +0 -0
  47. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/prompts/render_chain_test.py +0 -0
  48. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/prompts/render_test.py +0 -0
  49. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/utils/__init__.py +0 -0
  50. {latitude_sdk-3.0.2 → latitude_sdk-4.0.0b1}/tests/utils/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: latitude-sdk
3
- Version: 3.0.2
3
+ Version: 4.0.0b1
4
4
  Summary: Latitude SDK for Python
5
5
  Project-URL: repository, https://github.com/latitude-dev/latitude-llm/tree/main/packages/sdks/python
6
6
  Project-URL: homepage, https://github.com/latitude-dev/latitude-llm/tree/main/packages/sdks/python#readme
@@ -46,7 +46,7 @@ await sdk.prompts.run("joke-teller", RunPromptOptions(
46
46
  ))
47
47
  ```
48
48
 
49
- Find more [examples](https://github.com/latitude-dev/latitude-llm/tree/main/examples/sdks/python).
49
+ Find more [examples](https://docs.latitude.so/examples/sdk).
50
50
 
51
51
  ## Development
52
52
 
@@ -60,7 +60,9 @@ Requires uv `0.5.10` or higher.
60
60
  - Build package: `uv build`
61
61
  - Publish package: `uv publish`
62
62
 
63
- ## Run only one test
63
+ ### Running only a specific test
64
+
65
+ Mark the test with an `only` marker:
64
66
 
65
67
  ```python
66
68
  import pytest
@@ -70,13 +72,13 @@ async def my_test(self):
70
72
  # ... your code
71
73
  ```
72
74
 
73
- And then run the tests with the marker `only`:
75
+ ...and then run the tests with the marker `only`:
74
76
 
75
77
  ```sh
76
78
  uv run scripts/test.py -m only
77
79
  ```
78
80
 
79
- Other way is all in line:
81
+ Another way is to specify the test in line:
80
82
 
81
83
  ```python
82
84
  uv run scripts/test.py <test_path>::<test_case>::<test_name>
@@ -84,4 +86,4 @@ uv run scripts/test.py <test_path>::<test_case>::<test_name>
84
86
 
85
87
  ## License
86
88
 
87
- The SDK is licensed under the [LGPL-3.0 License](https://opensource.org/licenses/LGPL-3.0) - read the [LICENSE](/LICENSE) file for details.
89
+ The SDK is licensed under the [MIT License](https://opensource.org/licenses/MIT) - read the [LICENSE](/LICENSE) file for details.
@@ -27,7 +27,7 @@ await sdk.prompts.run("joke-teller", RunPromptOptions(
27
27
  ))
28
28
  ```
29
29
 
30
- Find more [examples](https://github.com/latitude-dev/latitude-llm/tree/main/examples/sdks/python).
30
+ Find more [examples](https://docs.latitude.so/examples/sdk).
31
31
 
32
32
  ## Development
33
33
 
@@ -41,7 +41,9 @@ Requires uv `0.5.10` or higher.
41
41
  - Build package: `uv build`
42
42
  - Publish package: `uv publish`
43
43
 
44
- ## Run only one test
44
+ ### Running only a specific test
45
+
46
+ Mark the test with an `only` marker:
45
47
 
46
48
  ```python
47
49
  import pytest
@@ -51,13 +53,13 @@ async def my_test(self):
51
53
  # ... your code
52
54
  ```
53
55
 
54
- And then run the tests with the marker `only`:
56
+ ...and then run the tests with the marker `only`:
55
57
 
56
58
  ```sh
57
59
  uv run scripts/test.py -m only
58
60
  ```
59
61
 
60
- Other way is all in line:
62
+ Another way is to specify the test in line:
61
63
 
62
64
  ```python
63
65
  uv run scripts/test.py <test_path>::<test_case>::<test_name>
@@ -65,4 +67,4 @@ uv run scripts/test.py <test_path>::<test_case>::<test_name>
65
67
 
66
68
  ## License
67
69
 
68
- The SDK is licensed under the [LGPL-3.0 License](https://opensource.org/licenses/LGPL-3.0) - read the [LICENSE](/LICENSE) file for details.
70
+ The SDK is licensed under the [MIT License](https://opensource.org/licenses/MIT) - read the [LICENSE](/LICENSE) file for details.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "latitude-sdk"
3
- version = "3.0.2"
3
+ version = "4.0.0-beta.1"
4
4
  description = "Latitude SDK for Python"
5
5
  authors = [{ name = "Latitude Data SL", email = "hello@latitude.so" }]
6
6
  maintainers = [{ name = "Latitude Data SL", email = "hello@latitude.so" }]
@@ -0,0 +1,276 @@
1
+ import asyncio
2
+ import json
3
+ from contextlib import asynccontextmanager
4
+ from typing import Any, AsyncGenerator, Awaitable, Callable, Optional
5
+
6
+ import httpx
7
+ import httpx_sse
8
+
9
+ from latitude_sdk.client.payloads import (
10
+ ErrorResponse,
11
+ RequestBody,
12
+ RequestHandler,
13
+ RequestParams,
14
+ )
15
+ from latitude_sdk.client.router import Router, RouterOptions
16
+ from latitude_sdk.sdk.errors import (
17
+ ApiError,
18
+ ApiErrorCodes,
19
+ ApiErrorDbRef,
20
+ )
21
+ from latitude_sdk.sdk.types import LogSources
22
+ from latitude_sdk.util import Model
23
+
24
+ RETRIABLE_STATUSES = [408, 409, 429, 500, 502, 503, 504]
25
+
26
+ ClientEvent = httpx_sse.ServerSentEvent
27
+
28
+
29
+ class ClientResponse(httpx.Response):
30
+ async def sse(self: httpx.Response) -> AsyncGenerator[ClientEvent, Any]:
31
+ source = httpx_sse.EventSource(self)
32
+
33
+ async for event in source.aiter_sse():
34
+ yield event
35
+
36
+
37
+ httpx.Response.sse = ClientResponse.sse # pyright: ignore [reportAttributeAccessIssue]
38
+
39
+
40
+ class ClientOptions(Model):
41
+ api_key: str
42
+ retries: int
43
+ delay: float
44
+ timeout: Optional[float]
45
+ source: LogSources
46
+ router: RouterOptions
47
+
48
+
49
+ class Client:
50
+ options: ClientOptions
51
+ router: Router
52
+
53
+ def __init__(self, options: ClientOptions):
54
+ self.options = options
55
+ self.router = Router(options.router)
56
+
57
+ @asynccontextmanager
58
+ async def request(
59
+ self,
60
+ handler: RequestHandler,
61
+ params: Optional[RequestParams] = None,
62
+ body: Optional[RequestBody] = None,
63
+ ) -> AsyncGenerator[ClientResponse, Any]:
64
+ """
65
+ Main request handler that delegates to streaming or non-streaming methods.
66
+ """
67
+ is_streaming = self._is_streaming_request(body)
68
+
69
+ if is_streaming:
70
+ async with self._streaming_request(handler, params, body) as response:
71
+ yield response
72
+ else:
73
+ async with self._non_streaming_request(handler, params, body) as response:
74
+ yield response
75
+
76
+ def _is_streaming_request(self, body: Optional[RequestBody]) -> bool:
77
+ """Check if this is a streaming request based on the body."""
78
+ return body is not None and hasattr(body, "stream") and body.stream # type: ignore
79
+
80
+ def _prepare_headers(self, is_streaming: bool) -> dict[str, str]:
81
+ """Prepare headers for the request."""
82
+ headers = {
83
+ "Authorization": f"Bearer {self.options.api_key}",
84
+ "Content-Type": "application/json",
85
+ }
86
+
87
+ if is_streaming:
88
+ headers["Accept"] = "text/event-stream"
89
+
90
+ return headers
91
+
92
+ def _prepare_content(self, body: Optional[RequestBody]) -> Optional[str]:
93
+ """Prepare request content from body."""
94
+ if not body:
95
+ return None
96
+
97
+ return json.dumps(
98
+ {
99
+ **json.loads(body.model_dump_json()),
100
+ "__internal": {"source": self.options.source},
101
+ }
102
+ )
103
+
104
+ @asynccontextmanager
105
+ async def _streaming_request(
106
+ self,
107
+ handler: RequestHandler,
108
+ params: Optional[RequestParams] = None,
109
+ body: Optional[RequestBody] = None,
110
+ ) -> AsyncGenerator[ClientResponse, Any]:
111
+ """Handle streaming requests with proper resource management."""
112
+ headers = self._prepare_headers(is_streaming=True)
113
+ content = self._prepare_content(body)
114
+ method, url = self.router.resolve(handler, params)
115
+
116
+ async with httpx.AsyncClient(
117
+ headers=headers,
118
+ timeout=self.options.timeout,
119
+ follow_redirects=False,
120
+ max_redirects=0,
121
+ ) as client:
122
+ response = await self._execute_with_retry(
123
+ lambda: client.stream(method=method, url=url, content=content), # type: ignore
124
+ is_streaming=True,
125
+ )
126
+
127
+ # For streaming responses, yield the response directly
128
+ # The caller is responsible for consuming the stream
129
+ yield response
130
+
131
+ @asynccontextmanager
132
+ async def _non_streaming_request(
133
+ self,
134
+ handler: RequestHandler,
135
+ params: Optional[RequestParams] = None,
136
+ body: Optional[RequestBody] = None,
137
+ ) -> AsyncGenerator[ClientResponse, Any]:
138
+ """Handle non-streaming requests with proper resource management."""
139
+ headers = self._prepare_headers(is_streaming=False)
140
+ content = self._prepare_content(body)
141
+ method, url = self.router.resolve(handler, params)
142
+
143
+ async with httpx.AsyncClient(
144
+ headers=headers,
145
+ timeout=self.options.timeout,
146
+ follow_redirects=False,
147
+ max_redirects=0,
148
+ ) as client:
149
+ response = await self._execute_with_retry(
150
+ lambda: client.request(method=method, url=url, content=content), # type: ignore
151
+ is_streaming=False,
152
+ )
153
+
154
+ try:
155
+ # Pre-read the response text for non-streaming responses
156
+ # This ensures the response is fully loaded and validates it
157
+ _ = response.text
158
+ yield response
159
+ finally:
160
+ await response.aclose()
161
+
162
+ async def _execute_with_retry(
163
+ self,
164
+ request_func: Callable[[], Awaitable[ClientResponse]],
165
+ is_streaming: bool = False,
166
+ ) -> ClientResponse:
167
+ """Execute a request with retry logic."""
168
+ last_exception = None
169
+ last_response = None
170
+
171
+ for attempt in range(1, self.options.retries + 1):
172
+ response_cm = None
173
+ response = None
174
+
175
+ try:
176
+ if is_streaming:
177
+ # For streaming, request_func returns a context manager
178
+ response_cm = request_func()
179
+ response = await response_cm.__aenter__() # type: ignore
180
+ # Store the context manager for proper cleanup
181
+ response._stream_cm = response_cm # pyright: ignore [reportAttributeAccessIssue]
182
+ else:
183
+ response = await request_func()
184
+
185
+ response.raise_for_status() # type: ignore
186
+ return response # type: ignore
187
+
188
+ except Exception as exception:
189
+ last_exception = exception
190
+
191
+ # For HTTP errors, get the response from the exception
192
+ if isinstance(exception, httpx.HTTPStatusError):
193
+ last_response = exception.response
194
+ else:
195
+ last_response = getattr(exception, "response", None)
196
+
197
+ # Don't retry ApiErrors - they're business logic errors
198
+ if isinstance(exception, ApiError):
199
+ raise exception
200
+
201
+ # If this is the last attempt, break to raise the exception
202
+ if attempt >= self.options.retries:
203
+ break
204
+
205
+ # Check if we should retry based on status code
206
+ if (
207
+ isinstance(exception, httpx.HTTPStatusError)
208
+ and exception.response.status_code in RETRIABLE_STATUSES
209
+ ):
210
+ await asyncio.sleep(self._calculate_delay(attempt))
211
+ continue
212
+
213
+ # For non-retriable errors, don't retry
214
+ break
215
+
216
+ # If we get here, all retries failed
217
+ raise await self._exception(last_exception, last_response) from last_exception # type: ignore
218
+
219
+ def _calculate_delay(self, attempt: int) -> float:
220
+ """Calculate exponential backoff delay."""
221
+ return self.options.delay * (2 ** (attempt - 1))
222
+
223
+ async def _exception(self, exception: Exception, response: Optional[httpx.Response] = None) -> ApiError:
224
+ if not response:
225
+ return ApiError(
226
+ status=500,
227
+ code=ApiErrorCodes.InternalServerError,
228
+ message=str(exception),
229
+ response=str(exception),
230
+ )
231
+
232
+ # Try to safely get response text and content, handling streaming responses
233
+ response_text = ""
234
+ response_content = b""
235
+
236
+ try:
237
+ # Check if this is a streaming response
238
+ content_type = response.headers.get("content-type", "")
239
+ is_streaming = "text/event-stream" in content_type
240
+
241
+ # For streaming responses, try to read content but don't access text
242
+ if is_streaming:
243
+ # For streaming responses, we can access content but not text
244
+ response_content = response.content
245
+ response_text = ""
246
+ else:
247
+ # For non-streaming responses, we can safely access both
248
+ response_content = response.content
249
+ response_text = response.text
250
+ except Exception:
251
+ # If we can't read the response (e.g., streaming response that hasn't been read),
252
+ # try to get what we can
253
+ try:
254
+ response_content = response.content
255
+ except Exception:
256
+ response_content = b""
257
+ response_text = ""
258
+
259
+ try:
260
+ error = ErrorResponse.model_validate_json(response_content)
261
+
262
+ return ApiError(
263
+ status=response.status_code,
264
+ code=error.code,
265
+ message=error.message,
266
+ response=response_text,
267
+ db_ref=ApiErrorDbRef(**dict(error.db_ref)) if error.db_ref else None,
268
+ )
269
+
270
+ except Exception:
271
+ return ApiError(
272
+ status=response.status_code,
273
+ code=ApiErrorCodes.InternalServerError,
274
+ message=str(exception),
275
+ response=response_text,
276
+ )
@@ -45,6 +45,7 @@ class RunPromptRequestBody(Model):
45
45
  parameters: Optional[Dict[str, Any]] = None
46
46
  custom_identifier: Optional[str] = Field(default=None, alias=str("customIdentifier"))
47
47
  stream: Optional[bool] = None
48
+ tools: Optional[List[str]] = None
48
49
 
49
50
 
50
51
  class ChatPromptRequestParams(Model):
@@ -88,6 +89,16 @@ class AnnotateEvaluationRequestBody(Model):
88
89
  metadata: Optional[Metadata] = None
89
90
 
90
91
 
92
+ class ToolResultsRequestBody(Model):
93
+ tool_call_id: str = Field(alias=str("toolCallId"))
94
+ result: Any
95
+ is_error: Optional[bool] = Field(default=None, alias=str("isError"))
96
+
97
+
98
+ class CreateProjectRequestBody(Model):
99
+ name: str
100
+
101
+
91
102
  RequestParams = Union[
92
103
  GetPromptRequestParams,
93
104
  GetAllPromptRequestParams,
@@ -105,6 +116,8 @@ RequestBody = Union[
105
116
  ChatPromptRequestBody,
106
117
  CreateLogRequestBody,
107
118
  AnnotateEvaluationRequestBody,
119
+ ToolResultsRequestBody,
120
+ CreateProjectRequestBody,
108
121
  ]
109
122
 
110
123
 
@@ -116,3 +129,6 @@ class RequestHandler(StrEnum):
116
129
  ChatPrompt = "CHAT_PROMPT"
117
130
  CreateLog = "CREATE_LOG"
118
131
  AnnotateEvaluation = "ANNOTATE_EVALUATION"
132
+ ToolResults = "TOOL_RESULTS"
133
+ GetAllProjects = "GET_ALL_PROJECTS"
134
+ CreateProject = "CREATE_PROJECT"
@@ -27,7 +27,7 @@ class Router:
27
27
  def __init__(self, options: RouterOptions):
28
28
  self.options = options
29
29
 
30
- def resolve(self, handler: RequestHandler, params: RequestParams) -> Tuple[str, str]:
30
+ def resolve(self, handler: RequestHandler, params: Optional[RequestParams] = None) -> Tuple[str, str]:
31
31
  if handler == RequestHandler.GetPrompt:
32
32
  assert isinstance(params, GetPromptRequestParams)
33
33
 
@@ -90,6 +90,15 @@ class Router:
90
90
 
91
91
  return "POST", self.conversations().annotate(params.conversation_uuid, params.evaluation_uuid)
92
92
 
93
+ elif handler == RequestHandler.ToolResults:
94
+ return "POST", f"{self.options.gateway.base_url}/tools/results"
95
+
96
+ elif handler == RequestHandler.GetAllProjects:
97
+ return "GET", f"{self.options.gateway.base_url}/projects"
98
+
99
+ elif handler == RequestHandler.CreateProject:
100
+ return "POST", f"{self.options.gateway.base_url}/projects"
101
+
93
102
  raise TypeError(f"Unknown handler: {handler}")
94
103
 
95
104
  class Conversations(Model):
@@ -6,6 +6,7 @@ from latitude_sdk.client import Client, ClientOptions, RouterOptions
6
6
  from latitude_sdk.env import env
7
7
  from latitude_sdk.sdk.evaluations import Evaluations
8
8
  from latitude_sdk.sdk.logs import Logs
9
+ from latitude_sdk.sdk.projects import Projects
9
10
  from latitude_sdk.sdk.prompts import Prompts
10
11
  from latitude_sdk.sdk.types import GatewayOptions, LogSources, SdkOptions
11
12
  from latitude_sdk.util import Model
@@ -49,6 +50,7 @@ class Latitude:
49
50
 
50
51
  promptl: Promptl
51
52
 
53
+ projects: Projects
52
54
  prompts: Prompts
53
55
  logs: Logs
54
56
  evaluations: Evaluations
@@ -76,6 +78,7 @@ class Latitude:
76
78
  )
77
79
 
78
80
  self.promptl = Promptl(self._options.promptl)
81
+ self.projects = Projects(self._client, self._options)
79
82
  self.prompts = Prompts(self._client, self.promptl, self._options)
80
83
  self.logs = Logs(self._client, self._options)
81
84
  self.evaluations = Evaluations(self._client, self._options)
@@ -0,0 +1,41 @@
1
+ import json
2
+ from typing import List
3
+
4
+ from latitude_sdk.client import Client
5
+ from latitude_sdk.client.payloads import CreateProjectRequestBody, RequestHandler
6
+ from latitude_sdk.sdk.types import Project, SdkOptions, Version
7
+
8
+
9
+ class CreateProjectResponse:
10
+ def __init__(self, project: Project, version: Version):
11
+ self.project = project
12
+ self.version = version
13
+
14
+
15
+ class Projects:
16
+ _client: Client
17
+ _options: SdkOptions
18
+
19
+ def __init__(self, client: Client, options: SdkOptions):
20
+ self._client = client
21
+ self._options = options
22
+
23
+ async def get_all(self) -> List[Project]:
24
+ async with self._client.request(
25
+ handler=RequestHandler.GetAllProjects,
26
+ params=None,
27
+ ) as response:
28
+ projects_data = json.loads(response.content)
29
+ return [Project.model_validate_json(json.dumps(project)) for project in projects_data]
30
+
31
+ async def create(self, name: str) -> CreateProjectResponse:
32
+ async with self._client.request(
33
+ handler=RequestHandler.CreateProject,
34
+ params=None,
35
+ body=CreateProjectRequestBody(name=name),
36
+ ) as response:
37
+ response_data = json.loads(response.content)
38
+ return CreateProjectResponse(
39
+ project=Project.model_validate_json(json.dumps(response_data["project"])),
40
+ version=Version.model_validate_json(json.dumps(response_data["version"])),
41
+ )