aidial-client 0.11.0.dev4__tar.gz → 0.12.0.dev1__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 (69) hide show
  1. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/PKG-INFO +59 -2
  2. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/README.md +58 -1
  3. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_client.py +29 -0
  4. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_client_pool.py +30 -0
  5. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_http_client/_async.py +31 -11
  6. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_http_client/_base.py +19 -0
  7. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_http_client/_sync.py +4 -11
  8. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/helpers/storage_resource.py +24 -0
  9. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/resources/files.py +21 -31
  10. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/file.py +8 -0
  11. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/pyproject.toml +1 -1
  12. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/LICENSE +0 -0
  13. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/__init__.py +0 -0
  14. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_auth.py +0 -0
  15. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_compatibility/__init__.py +0 -0
  16. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_compatibility/openai.py +0 -0
  17. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_compatibility/pydantic.py +0 -0
  18. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_compatibility/pydantic_v1.py +0 -0
  19. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_constants.py +0 -0
  20. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_exception.py +0 -0
  21. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_http_client/__init__.py +0 -0
  22. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_http_client/_sse.py +0 -0
  23. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_internal_types/__init__.py +0 -0
  24. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_internal_types/_defaults.py +0 -0
  25. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_internal_types/_generic.py +0 -0
  26. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_internal_types/_http_request.py +0 -0
  27. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_internal_types/_json_rpc.py +0 -0
  28. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_internal_types/_model.py +0 -0
  29. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_log.py +0 -0
  30. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_utils/__init__.py +0 -0
  31. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_utils/_alias.py +0 -0
  32. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_utils/_dict.py +0 -0
  33. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_utils/_openai.py +0 -0
  34. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_utils/_response_processing.py +0 -0
  35. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/_utils/_type_guard.py +0 -0
  36. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/helpers/__init__.py +0 -0
  37. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/helpers/_url.py +0 -0
  38. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/py.typed +0 -0
  39. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/resources/__init__.py +0 -0
  40. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/resources/application.py +0 -0
  41. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/resources/base.py +0 -0
  42. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/resources/bucket.py +0 -0
  43. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/resources/chat/__init__.py +0 -0
  44. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/resources/chat/completions.py +0 -0
  45. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/resources/client_channel.py +0 -0
  46. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/resources/deployments.py +0 -0
  47. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/resources/metadata.py +0 -0
  48. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/resources/model.py +0 -0
  49. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/resources/prompts.py +0 -0
  50. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/resources/resource_permissions.py +0 -0
  51. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/resources/toolset.py +0 -0
  52. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/__init__.py +0 -0
  53. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/application.py +0 -0
  54. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/bucket.py +0 -0
  55. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/chat/__init__.py +0 -0
  56. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/chat/function.py +0 -0
  57. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/chat/legacy/__init__.py +0 -0
  58. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/chat/legacy/application_request.py +0 -0
  59. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/chat/legacy/chat_completion.py +0 -0
  60. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/chat/request.py +0 -0
  61. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/chat/request_param.py +0 -0
  62. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/chat/response.py +0 -0
  63. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/chat/tool.py +0 -0
  64. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/client_channel.py +0 -0
  65. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/deployment.py +0 -0
  66. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/metadata.py +0 -0
  67. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/model.py +0 -0
  68. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/prompt.py +0 -0
  69. {aidial_client-0.11.0.dev4 → aidial_client-0.12.0.dev1}/aidial_client/types/toolset.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aidial-client
3
- Version: 0.11.0.dev4
3
+ Version: 0.12.0.dev1
4
4
  Summary: A Python client library for the AI DIAL API
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -39,6 +39,7 @@ Description-Content-Type: text/markdown
39
39
  - [Authentication](#authentication)
40
40
  - [API Keys](#api-keys)
41
41
  - [Bearer Token](#bearer-token)
42
+ - [Lifecycle Management](#lifecycle-management)
42
43
  - [Deployments](#deployments)
43
44
  - [List Deployments](#list-deployments)
44
45
  - [Get Deployment by Id](#get-deployment-by-id)
@@ -143,6 +144,45 @@ async_client = AsyncDial(
143
144
  )
144
145
  ```
145
146
 
147
+ ### Lifecycle Management
148
+
149
+ For deterministic shutdown of underlying HTTP clients, both client types and
150
+ client pools expose lifecycle APIs.
151
+
152
+ ```python
153
+ from aidial_client import AsyncDial, AsyncDialClientPool, Dial, DialClientPool
154
+
155
+ # Sync client
156
+ with Dial(api_key="your_api_key", base_url="https://your-dial-instance.com") as client:
157
+ ...
158
+
159
+ client = Dial(api_key="your_api_key", base_url="https://your-dial-instance.com")
160
+ client.close()
161
+
162
+ # Async client
163
+ async with AsyncDial(
164
+ api_key="your_api_key", base_url="https://your-dial-instance.com"
165
+ ) as async_client:
166
+ ...
167
+
168
+ async_client = AsyncDial(
169
+ api_key="your_api_key", base_url="https://your-dial-instance.com"
170
+ )
171
+ await async_client.aclose()
172
+
173
+ # Sync pool
174
+ with DialClientPool() as pool:
175
+ pooled_client = pool.create_client(
176
+ base_url="https://your-dial-instance.com", api_key="your-api-key"
177
+ )
178
+
179
+ # Async pool
180
+ async with AsyncDialClientPool() as async_pool:
181
+ pooled_async_client = async_pool.create_client(
182
+ base_url="https://your-dial-instance.com", api_key="your-api-key"
183
+ )
184
+ ```
185
+
146
186
  You can also pass `bearer_token` as a function without parameters, that returns a `string`:
147
187
 
148
188
  ```python
@@ -514,6 +554,16 @@ result = await async_client.files.download(
514
554
  )
515
555
  ```
516
556
 
557
+ For large async downloads, use `stream_download()` to process bytes as they arrive without buffering the full response in memory:
558
+
559
+ ```python
560
+ async with async_client.files.stream_download(
561
+ url=await async_client.my_files_home() / "relative_folder/my-file.txt"
562
+ ) as result:
563
+ async for bytes_chunk in result:
564
+ ...
565
+ ```
566
+
517
567
  As a result, you will receive an object of type `FileDownloadResponse`, that you can iterate by byte chunks:
518
568
 
519
569
  ```python
@@ -530,6 +580,13 @@ all_content = result.get_content()
530
580
  all_content = await result.aget_content()
531
581
  ```
532
582
 
583
+ or access response metadata:
584
+
585
+ ```python
586
+ headers = result.headers
587
+ content_type = result.content_type
588
+ ```
589
+
533
590
  or write it to the file:
534
591
 
535
592
  ```python
@@ -950,7 +1007,7 @@ second_client = client_pool.create_client(
950
1007
  #### Asynchronous Client Pool
951
1008
 
952
1009
  ```python
953
- from dial_client import (
1010
+ from aidial_client import (
954
1011
  AsyncDialClientPool,
955
1012
  )
956
1013
 
@@ -17,6 +17,7 @@
17
17
  - [Authentication](#authentication)
18
18
  - [API Keys](#api-keys)
19
19
  - [Bearer Token](#bearer-token)
20
+ - [Lifecycle Management](#lifecycle-management)
20
21
  - [Deployments](#deployments)
21
22
  - [List Deployments](#list-deployments)
22
23
  - [Get Deployment by Id](#get-deployment-by-id)
@@ -121,6 +122,45 @@ async_client = AsyncDial(
121
122
  )
122
123
  ```
123
124
 
125
+ ### Lifecycle Management
126
+
127
+ For deterministic shutdown of underlying HTTP clients, both client types and
128
+ client pools expose lifecycle APIs.
129
+
130
+ ```python
131
+ from aidial_client import AsyncDial, AsyncDialClientPool, Dial, DialClientPool
132
+
133
+ # Sync client
134
+ with Dial(api_key="your_api_key", base_url="https://your-dial-instance.com") as client:
135
+ ...
136
+
137
+ client = Dial(api_key="your_api_key", base_url="https://your-dial-instance.com")
138
+ client.close()
139
+
140
+ # Async client
141
+ async with AsyncDial(
142
+ api_key="your_api_key", base_url="https://your-dial-instance.com"
143
+ ) as async_client:
144
+ ...
145
+
146
+ async_client = AsyncDial(
147
+ api_key="your_api_key", base_url="https://your-dial-instance.com"
148
+ )
149
+ await async_client.aclose()
150
+
151
+ # Sync pool
152
+ with DialClientPool() as pool:
153
+ pooled_client = pool.create_client(
154
+ base_url="https://your-dial-instance.com", api_key="your-api-key"
155
+ )
156
+
157
+ # Async pool
158
+ async with AsyncDialClientPool() as async_pool:
159
+ pooled_async_client = async_pool.create_client(
160
+ base_url="https://your-dial-instance.com", api_key="your-api-key"
161
+ )
162
+ ```
163
+
124
164
  You can also pass `bearer_token` as a function without parameters, that returns a `string`:
125
165
 
126
166
  ```python
@@ -492,6 +532,16 @@ result = await async_client.files.download(
492
532
  )
493
533
  ```
494
534
 
535
+ For large async downloads, use `stream_download()` to process bytes as they arrive without buffering the full response in memory:
536
+
537
+ ```python
538
+ async with async_client.files.stream_download(
539
+ url=await async_client.my_files_home() / "relative_folder/my-file.txt"
540
+ ) as result:
541
+ async for bytes_chunk in result:
542
+ ...
543
+ ```
544
+
495
545
  As a result, you will receive an object of type `FileDownloadResponse`, that you can iterate by byte chunks:
496
546
 
497
547
  ```python
@@ -508,6 +558,13 @@ all_content = result.get_content()
508
558
  all_content = await result.aget_content()
509
559
  ```
510
560
 
561
+ or access response metadata:
562
+
563
+ ```python
564
+ headers = result.headers
565
+ content_type = result.content_type
566
+ ```
567
+
511
568
  or write it to the file:
512
569
 
513
570
  ```python
@@ -928,7 +985,7 @@ second_client = client_pool.create_client(
928
985
  #### Asynchronous Client Pool
929
986
 
930
987
  ```python
931
- from dial_client import (
988
+ from aidial_client import (
932
989
  AsyncDialClientPool,
933
990
  )
934
991
 
@@ -1,5 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from pathlib import PurePosixPath
3
+ from types import TracebackType
3
4
  from typing import Generic, TypeVar
4
5
  from urllib.parse import urljoin
5
6
 
@@ -164,6 +165,20 @@ class Dial(BaseDialClient[SyncHTTPClient, SyncAuthValue]):
164
165
  def auth_headers(self) -> dict[str, str]:
165
166
  return self._http_client.auth_headers()
166
167
 
168
+ def close(self) -> None:
169
+ self._http_client.internal_http_client.close()
170
+
171
+ def __enter__(self) -> "Dial":
172
+ return self
173
+
174
+ def __exit__(
175
+ self,
176
+ exc_type: type[BaseException] | None,
177
+ exc_value: BaseException | None,
178
+ traceback: TracebackType | None,
179
+ ) -> None:
180
+ self.close()
181
+
167
182
 
168
183
  class AsyncDial(BaseDialClient[AsyncHTTPClient, AsyncAuthValue]):
169
184
  def _init_resources(self) -> None:
@@ -253,3 +268,17 @@ class AsyncDial(BaseDialClient[AsyncHTTPClient, AsyncAuthValue]):
253
268
 
254
269
  async def auth_headers(self) -> dict[str, str]:
255
270
  return await self._http_client.auth_headers()
271
+
272
+ async def aclose(self) -> None:
273
+ await self._http_client.internal_http_client.aclose()
274
+
275
+ async def __aenter__(self) -> "AsyncDial":
276
+ return self
277
+
278
+ async def __aexit__(
279
+ self,
280
+ exc_type: type[BaseException] | None,
281
+ exc_value: BaseException | None,
282
+ traceback: TracebackType | None,
283
+ ) -> None:
284
+ await self.aclose()
@@ -1,3 +1,5 @@
1
+ from types import TracebackType
2
+
1
3
  import httpx
2
4
 
3
5
  from aidial_client._auth import AsyncAuthValue, SyncAuthValue
@@ -44,6 +46,20 @@ class DialClientPool:
44
46
  ),
45
47
  )
46
48
 
49
+ def close(self) -> None:
50
+ self._internal_http_client.close()
51
+
52
+ def __enter__(self) -> "DialClientPool":
53
+ return self
54
+
55
+ def __exit__(
56
+ self,
57
+ exc_type: type[BaseException] | None,
58
+ exc_value: BaseException | None,
59
+ traceback: TracebackType | None,
60
+ ) -> None:
61
+ self.close()
62
+
47
63
 
48
64
  class AsyncDialClientPool:
49
65
  def __init__(
@@ -78,3 +94,17 @@ class AsyncDialClientPool:
78
94
  internal_http_client=self._internal_http_client,
79
95
  ),
80
96
  )
97
+
98
+ async def aclose(self) -> None:
99
+ await self._internal_http_client.aclose()
100
+
101
+ async def __aenter__(self) -> "AsyncDialClientPool":
102
+ return self
103
+
104
+ async def __aexit__(
105
+ self,
106
+ exc_type: type[BaseException] | None,
107
+ exc_value: BaseException | None,
108
+ traceback: TracebackType | None,
109
+ ) -> None:
110
+ await self.aclose()
@@ -1,5 +1,5 @@
1
1
  import asyncio
2
- from collections.abc import AsyncIterator, Callable, Mapping
2
+ from collections.abc import AsyncIterator, Mapping
3
3
  from contextlib import asynccontextmanager, suppress
4
4
  from http import HTTPStatus
5
5
  from typing import Any
@@ -8,7 +8,7 @@ import httpx
8
8
 
9
9
  from aidial_client._auth import AsyncAuthValue, aget_combined_auth_headers
10
10
  from aidial_client._exception import DialException
11
- from aidial_client._http_client._base import BaseHTTPClient
11
+ from aidial_client._http_client._base import BaseHTTPClient, ErrorHandler
12
12
  from aidial_client._internal_types._defaults import NOT_GIVEN, NotGiven
13
13
  from aidial_client._internal_types._generic import ResponseT
14
14
  from aidial_client._internal_types._http_request import FinalRequestOptions
@@ -51,8 +51,7 @@ class AsyncHTTPClient(BaseHTTPClient[httpx.AsyncClient, AsyncAuthValue]):
51
51
  options: FinalRequestOptions,
52
52
  cast_to: type[ResponseT],
53
53
  remaining_retries: int | None = None,
54
- on_http_error: Callable[[httpx.HTTPStatusError], DialException | None]
55
- | None = None,
54
+ on_http_error: ErrorHandler | None = None,
56
55
  ) -> ResponseT:
57
56
  retries = self._remaining_retries(remaining_retries, options)
58
57
  auth_headers = await self.auth_headers()
@@ -101,16 +100,37 @@ class AsyncHTTPClient(BaseHTTPClient[httpx.AsyncClient, AsyncAuthValue]):
101
100
  cast_to=cast_to,
102
101
  remaining_retries=retries,
103
102
  )
104
- # Try to get a custom error from response status_code/code/message
105
- custom_error = on_http_error(err) if on_http_error else None
106
- # or fallback to default processing
107
- raised_error = custom_error or self._make_dial_error_from_response(
108
- err.response
109
- )
110
- raise raised_error from err
103
+ self._raise_for_status(response, on_http_error)
111
104
 
112
105
  return process_block_response(cast_to=cast_to, response=response)
113
106
 
107
+ @asynccontextmanager
108
+ async def stream(
109
+ self,
110
+ *,
111
+ options: FinalRequestOptions,
112
+ on_http_error: ErrorHandler | None = None,
113
+ ) -> AsyncIterator[httpx.Response]:
114
+ auth_headers = await self.auth_headers()
115
+ request = self._build_request(options, auth_headers)
116
+ try:
117
+ response = await self._internal_http_client.send(
118
+ request, stream=True
119
+ )
120
+ except httpx.TimeoutException as err:
121
+ raise DialException(
122
+ message="Request timed out",
123
+ status_code=HTTPStatus.REQUEST_TIMEOUT,
124
+ ) from err
125
+ except httpx.HTTPError as err:
126
+ raise DialException(message=f"Request failed: {err}") from err
127
+
128
+ try:
129
+ self._raise_for_status(response, on_http_error)
130
+ yield response
131
+ finally:
132
+ await response.aclose()
133
+
114
134
  @asynccontextmanager
115
135
  async def stream_sse(
116
136
  self,
@@ -1,4 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
+ from collections.abc import Callable
2
3
  from http import HTTPStatus
3
4
  from random import uniform
4
5
  from typing import Generic, TypeVar
@@ -16,6 +17,8 @@ _HttpInternalClientT = TypeVar(
16
17
  "_HttpInternalClientT", bound=httpx.Client | httpx.AsyncClient
17
18
  )
18
19
 
20
+ ErrorHandler = Callable[[httpx.HTTPStatusError], DialException | None]
21
+
19
22
 
20
23
  class BaseHTTPClient(ABC, Generic[_HttpInternalClientT, AuthValueT]):
21
24
  _internal_http_client: _HttpInternalClientT
@@ -106,6 +109,22 @@ class BaseHTTPClient(ABC, Generic[_HttpInternalClientT, AuthValueT]):
106
109
  timeout = sleep_seconds + uniform(-0.5, 0.5) # noqa: S311
107
110
  return max(0, timeout)
108
111
 
112
+ def _raise_for_status(
113
+ self,
114
+ response: httpx.Response,
115
+ on_http_error: ErrorHandler | None,
116
+ ) -> None:
117
+ try:
118
+ response.raise_for_status()
119
+ except httpx.HTTPStatusError as err:
120
+ # Try to get a custom error from response status_code/code/message
121
+ custom_error = on_http_error(err) if on_http_error else None
122
+ # or fallback to default processing
123
+ raised_error = custom_error or self._make_dial_error_from_response(
124
+ err.response
125
+ )
126
+ raise raised_error from err
127
+
109
128
  def _make_dial_error_from_response(
110
129
  self,
111
130
  response: httpx.Response,
@@ -1,5 +1,5 @@
1
1
  import time
2
- from collections.abc import Callable, Iterator, Mapping
2
+ from collections.abc import Iterator, Mapping
3
3
  from contextlib import contextmanager, suppress
4
4
  from http import HTTPStatus
5
5
  from typing import Any
@@ -8,7 +8,7 @@ import httpx
8
8
 
9
9
  from aidial_client._auth import SyncAuthValue, get_combined_auth_headers
10
10
  from aidial_client._exception import DialException
11
- from aidial_client._http_client._base import BaseHTTPClient
11
+ from aidial_client._http_client._base import BaseHTTPClient, ErrorHandler
12
12
  from aidial_client._internal_types._defaults import NOT_GIVEN, NotGiven
13
13
  from aidial_client._internal_types._generic import ResponseT
14
14
  from aidial_client._internal_types._http_request import FinalRequestOptions
@@ -50,8 +50,7 @@ class SyncHTTPClient(BaseHTTPClient[httpx.Client, SyncAuthValue]):
50
50
  cast_to: type[ResponseT],
51
51
  options: FinalRequestOptions,
52
52
  remaining_retries: int | None = None,
53
- on_http_error: Callable[[httpx.HTTPStatusError], DialException | None]
54
- | None = None,
53
+ on_http_error: ErrorHandler | None = None,
55
54
  ) -> ResponseT:
56
55
  retries = self._remaining_retries(remaining_retries, options)
57
56
  auth_headers = self.auth_headers()
@@ -101,13 +100,7 @@ class SyncHTTPClient(BaseHTTPClient[httpx.Client, SyncAuthValue]):
101
100
  cast_to=cast_to,
102
101
  remaining_retries=retries,
103
102
  )
104
- # Try to get a custom error from response status_code/code/message
105
- custom_error = on_http_error(err) if on_http_error else None
106
- # or fallback to default processing
107
- raised_error = custom_error or self._make_dial_error_from_response(
108
- err.response
109
- )
110
- raise raised_error from err
103
+ self._raise_for_status(response, on_http_error)
111
104
 
112
105
  return process_block_response(cast_to=cast_to, response=response)
113
106
 
@@ -5,6 +5,8 @@ from urllib.parse import urljoin, urlparse
5
5
  from aidial_client._compatibility.pydantic_v1 import BaseModel
6
6
  from aidial_client._constants import API_PREFIX
7
7
  from aidial_client._exception import InvalidDialURLError, NotDialURLError
8
+ from aidial_client._internal_types._http_request import FinalRequestOptions
9
+ from aidial_client._utils._dict import remove_none
8
10
  from aidial_client.helpers._url import enforce_trailing_slash
9
11
 
10
12
  StorageResourceType = Literal["files", "conversations", "prompts"]
@@ -156,3 +158,25 @@ class DialStorageResourceMixin(BaseModel):
156
158
  Get the display name of the resource from the URL
157
159
  """
158
160
  return self.get_storage_resource(url).bucket_path
161
+
162
+ def _prepare_download_request(
163
+ self,
164
+ url: str | PurePosixPath,
165
+ etag_if_match: str | None,
166
+ ) -> tuple[FinalRequestOptions, str]:
167
+ storage_resource = self.get_storage_resource(str(url))
168
+
169
+ if storage_resource.filename is None:
170
+ raise InvalidDialURLError("URL points to a directory, not a file")
171
+
172
+ options = FinalRequestOptions(
173
+ method="GET",
174
+ url=urljoin(API_PREFIX, storage_resource.api_path),
175
+ headers=remove_none(
176
+ {
177
+ "If-Match": etag_if_match,
178
+ }
179
+ ),
180
+ )
181
+
182
+ return options, storage_resource.filename
@@ -1,3 +1,5 @@
1
+ from collections.abc import AsyncIterator
2
+ from contextlib import asynccontextmanager
1
3
  from pathlib import PurePosixPath
2
4
  from typing import Literal
3
5
  from urllib.parse import urljoin
@@ -8,7 +10,6 @@ from aidial_client._constants import API_PREFIX
8
10
  from aidial_client._exception import (
9
11
  DialException,
10
12
  EtagMismatchError,
11
- InvalidDialURLError,
12
13
  ResourceNotFoundError,
13
14
  )
14
15
  from aidial_client._internal_types._generic import NoneType
@@ -70,25 +71,13 @@ class Files(Resource, DialStorageResourceMixin):
70
71
  url: str | PurePosixPath,
71
72
  etag_if_match: str | None = None,
72
73
  ) -> FileDownloadResponse:
73
- storage_resource = self.get_storage_resource(str(url))
74
- if storage_resource.filename is None:
75
- raise InvalidDialURLError("URL points to a directory, not a file")
74
+ options, filename = self._prepare_download_request(url, etag_if_match)
76
75
  response = self.http_client.request(
77
76
  cast_to=httpx.Response,
78
- options=FinalRequestOptions(
79
- method="GET",
80
- url=urljoin(API_PREFIX, storage_resource.api_path),
81
- headers=remove_none(
82
- {
83
- "If-Match": etag_if_match,
84
- }
85
- ),
86
- ),
77
+ options=options,
87
78
  on_http_error=_files_error_processor,
88
79
  )
89
- return FileDownloadResponse(
90
- response=response, filename=storage_resource.filename
91
- )
80
+ return FileDownloadResponse(response=response, filename=filename)
92
81
 
93
82
  def delete(
94
83
  self,
@@ -188,25 +177,26 @@ class AsyncFiles(AsyncResource, DialStorageResourceMixin):
188
177
  url: str | PurePosixPath,
189
178
  etag_if_match: str | None = None,
190
179
  ) -> FileDownloadResponse:
191
- storage_resource = self.get_storage_resource(str(url))
192
- if storage_resource.filename is None:
193
- raise InvalidDialURLError("URL points to a directory, not a file")
180
+ options, filename = self._prepare_download_request(url, etag_if_match)
194
181
  response = await self.http_client.request(
195
182
  cast_to=httpx.Response,
196
- options=FinalRequestOptions(
197
- method="GET",
198
- url=urljoin(API_PREFIX, storage_resource.api_path),
199
- headers=remove_none(
200
- {
201
- "If-Match": etag_if_match,
202
- }
203
- ),
204
- ),
183
+ options=options,
205
184
  on_http_error=_files_error_processor,
206
185
  )
207
- return FileDownloadResponse(
208
- response=response, filename=storage_resource.filename
209
- )
186
+ return FileDownloadResponse(response=response, filename=filename)
187
+
188
+ @asynccontextmanager
189
+ async def stream_download(
190
+ self,
191
+ url: str | PurePosixPath,
192
+ etag_if_match: str | None = None,
193
+ ) -> AsyncIterator[FileDownloadResponse]:
194
+ options, filename = self._prepare_download_request(url, etag_if_match)
195
+ async with self.http_client.stream(
196
+ options=options,
197
+ on_http_error=_files_error_processor,
198
+ ) as response:
199
+ yield FileDownloadResponse(response=response, filename=filename)
210
200
 
211
201
  async def delete(
212
202
  self,
@@ -40,3 +40,11 @@ class FileDownloadResponse:
40
40
  @property
41
41
  def filename(self) -> str:
42
42
  return self._filename
43
+
44
+ @property
45
+ def headers(self) -> httpx.Headers:
46
+ return self._response.headers
47
+
48
+ @property
49
+ def content_type(self) -> str | None:
50
+ return self.headers.get("content-type")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aidial-client"
3
- version = "0.11.0.dev4"
3
+ version = "0.12.0.dev1"
4
4
  description = "A Python client library for the AI DIAL API"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"