isaacus 0.6.1__py3-none-any.whl → 0.8.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.
isaacus/__init__.py CHANGED
@@ -26,7 +26,7 @@ from ._exceptions import (
26
26
  UnprocessableEntityError,
27
27
  APIResponseValidationError,
28
28
  )
29
- from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient
29
+ from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient
30
30
  from ._utils._logs import setup_logging as _setup_logging
31
31
 
32
32
  __all__ = [
@@ -68,6 +68,7 @@ __all__ = [
68
68
  "DEFAULT_CONNECTION_LIMITS",
69
69
  "DefaultHttpxClient",
70
70
  "DefaultAsyncHttpxClient",
71
+ "DefaultAioHttpClient",
71
72
  ]
72
73
 
73
74
  if not _t.TYPE_CHECKING:
isaacus/_base_client.py CHANGED
@@ -504,6 +504,15 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
504
504
  # work around https://github.com/encode/httpx/discussions/2880
505
505
  kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")}
506
506
 
507
+ is_body_allowed = options.method.lower() != "get"
508
+
509
+ if is_body_allowed:
510
+ kwargs["json"] = json_data if is_given(json_data) else None
511
+ kwargs["files"] = files
512
+ else:
513
+ headers.pop("Content-Type", None)
514
+ kwargs.pop("data", None)
515
+
507
516
  # TODO: report this error to httpx
508
517
  return self._client.build_request( # pyright: ignore[reportUnknownMemberType]
509
518
  headers=headers,
@@ -515,8 +524,6 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
515
524
  # so that passing a `TypedDict` doesn't cause an error.
516
525
  # https://github.com/microsoft/pyright/issues/3526#event-6715453066
517
526
  params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None,
518
- json=json_data if is_given(json_data) else None,
519
- files=files,
520
527
  **kwargs,
521
528
  )
522
529
 
@@ -935,6 +942,9 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
935
942
  if self.custom_auth is not None:
936
943
  kwargs["auth"] = self.custom_auth
937
944
 
945
+ if options.follow_redirects is not None:
946
+ kwargs["follow_redirects"] = options.follow_redirects
947
+
938
948
  log.debug("Sending HTTP Request: %s %s", request.method, request.url)
939
949
 
940
950
  response = None
@@ -1043,7 +1053,14 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
1043
1053
  ) -> ResponseT:
1044
1054
  origin = get_origin(cast_to) or cast_to
1045
1055
 
1046
- if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse):
1056
+ if (
1057
+ inspect.isclass(origin)
1058
+ and issubclass(origin, BaseAPIResponse)
1059
+ # we only want to actually return the custom BaseAPIResponse class if we're
1060
+ # returning the raw response, or if we're not streaming SSE, as if we're streaming
1061
+ # SSE then `cast_to` doesn't actively reflect the type we need to parse into
1062
+ and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER)))
1063
+ ):
1047
1064
  if not issubclass(origin, APIResponse):
1048
1065
  raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}")
1049
1066
 
@@ -1254,6 +1271,24 @@ class _DefaultAsyncHttpxClient(httpx.AsyncClient):
1254
1271
  super().__init__(**kwargs)
1255
1272
 
1256
1273
 
1274
+ try:
1275
+ import httpx_aiohttp
1276
+ except ImportError:
1277
+
1278
+ class _DefaultAioHttpClient(httpx.AsyncClient):
1279
+ def __init__(self, **_kwargs: Any) -> None:
1280
+ raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra")
1281
+ else:
1282
+
1283
+ class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore
1284
+ def __init__(self, **kwargs: Any) -> None:
1285
+ kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
1286
+ kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS)
1287
+ kwargs.setdefault("follow_redirects", True)
1288
+
1289
+ super().__init__(**kwargs)
1290
+
1291
+
1257
1292
  if TYPE_CHECKING:
1258
1293
  DefaultAsyncHttpxClient = httpx.AsyncClient
1259
1294
  """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK
@@ -1262,8 +1297,12 @@ if TYPE_CHECKING:
1262
1297
  This is useful because overriding the `http_client` with your own instance of
1263
1298
  `httpx.AsyncClient` will result in httpx's defaults being used, not ours.
1264
1299
  """
1300
+
1301
+ DefaultAioHttpClient = httpx.AsyncClient
1302
+ """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`."""
1265
1303
  else:
1266
1304
  DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient
1305
+ DefaultAioHttpClient = _DefaultAioHttpClient
1267
1306
 
1268
1307
 
1269
1308
  class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient):
@@ -1435,6 +1474,9 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1435
1474
  if self.custom_auth is not None:
1436
1475
  kwargs["auth"] = self.custom_auth
1437
1476
 
1477
+ if options.follow_redirects is not None:
1478
+ kwargs["follow_redirects"] = options.follow_redirects
1479
+
1438
1480
  log.debug("Sending HTTP Request: %s %s", request.method, request.url)
1439
1481
 
1440
1482
  response = None
@@ -1543,7 +1585,14 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1543
1585
  ) -> ResponseT:
1544
1586
  origin = get_origin(cast_to) or cast_to
1545
1587
 
1546
- if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse):
1588
+ if (
1589
+ inspect.isclass(origin)
1590
+ and issubclass(origin, BaseAPIResponse)
1591
+ # we only want to actually return the custom BaseAPIResponse class if we're
1592
+ # returning the raw response, or if we're not streaming SSE, as if we're streaming
1593
+ # SSE then `cast_to` doesn't actively reflect the type we need to parse into
1594
+ and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER)))
1595
+ ):
1547
1596
  if not issubclass(origin, AsyncAPIResponse):
1548
1597
  raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}")
1549
1598
 
isaacus/_models.py CHANGED
@@ -2,9 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import inspect
5
- from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast
5
+ from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
6
6
  from datetime import date, datetime
7
7
  from typing_extensions import (
8
+ List,
8
9
  Unpack,
9
10
  Literal,
10
11
  ClassVar,
@@ -207,14 +208,18 @@ class BaseModel(pydantic.BaseModel):
207
208
  else:
208
209
  fields_values[name] = field_get_default(field)
209
210
 
211
+ extra_field_type = _get_extra_fields_type(__cls)
212
+
210
213
  _extra = {}
211
214
  for key, value in values.items():
212
215
  if key not in model_fields:
216
+ parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value
217
+
213
218
  if PYDANTIC_V2:
214
- _extra[key] = value
219
+ _extra[key] = parsed
215
220
  else:
216
221
  _fields_set.add(key)
217
- fields_values[key] = value
222
+ fields_values[key] = parsed
218
223
 
219
224
  object.__setattr__(m, "__dict__", fields_values)
220
225
 
@@ -366,7 +371,24 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object:
366
371
  if type_ is None:
367
372
  raise RuntimeError(f"Unexpected field type is None for {key}")
368
373
 
369
- return construct_type(value=value, type_=type_)
374
+ return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None))
375
+
376
+
377
+ def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None:
378
+ if not PYDANTIC_V2:
379
+ # TODO
380
+ return None
381
+
382
+ schema = cls.__pydantic_core_schema__
383
+ if schema["type"] == "model":
384
+ fields = schema["schema"]
385
+ if fields["type"] == "model-fields":
386
+ extras = fields.get("extras_schema")
387
+ if extras and "cls" in extras:
388
+ # mypy can't narrow the type
389
+ return extras["cls"] # type: ignore[no-any-return]
390
+
391
+ return None
370
392
 
371
393
 
372
394
  def is_basemodel(type_: type) -> bool:
@@ -420,7 +442,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T:
420
442
  return cast(_T, construct_type(value=value, type_=type_))
421
443
 
422
444
 
423
- def construct_type(*, value: object, type_: object) -> object:
445
+ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object:
424
446
  """Loose coercion to the expected type with construction of nested values.
425
447
 
426
448
  If the given value does not match the expected type then it is returned as-is.
@@ -438,8 +460,10 @@ def construct_type(*, value: object, type_: object) -> object:
438
460
  type_ = type_.__value__ # type: ignore[unreachable]
439
461
 
440
462
  # unwrap `Annotated[T, ...]` -> `T`
441
- if is_annotated_type(type_):
442
- meta: tuple[Any, ...] = get_args(type_)[1:]
463
+ if metadata is not None and len(metadata) > 0:
464
+ meta: tuple[Any, ...] = tuple(metadata)
465
+ elif is_annotated_type(type_):
466
+ meta = get_args(type_)[1:]
443
467
  type_ = extract_type_arg(type_, 0)
444
468
  else:
445
469
  meta = tuple()
@@ -737,6 +761,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
737
761
  idempotency_key: str
738
762
  json_data: Body
739
763
  extra_json: AnyMapping
764
+ follow_redirects: bool
740
765
 
741
766
 
742
767
  @final
@@ -750,6 +775,7 @@ class FinalRequestOptions(pydantic.BaseModel):
750
775
  files: Union[HttpxRequestFiles, None] = None
751
776
  idempotency_key: Union[str, None] = None
752
777
  post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
778
+ follow_redirects: Union[bool, None] = None
753
779
 
754
780
  # It should be noted that we cannot use `json` here as that would override
755
781
  # a BaseModel method in an incompatible fashion.
isaacus/_types.py CHANGED
@@ -100,6 +100,7 @@ class RequestOptions(TypedDict, total=False):
100
100
  params: Query
101
101
  extra_json: AnyMapping
102
102
  idempotency_key: str
103
+ follow_redirects: bool
103
104
 
104
105
 
105
106
  # Sentinel class used until PEP 0661 is accepted
@@ -215,3 +216,4 @@ class _GenericAlias(Protocol):
215
216
 
216
217
  class HttpxSendArgs(TypedDict, total=False):
217
218
  auth: httpx.Auth
219
+ follow_redirects: bool
isaacus/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "isaacus"
4
- __version__ = "0.6.1" # x-release-please-version
4
+ __version__ = "0.8.0" # x-release-please-version
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: isaacus
3
- Version: 0.6.1
3
+ Version: 0.8.0
4
4
  Summary: The official Python library for the isaacus API
5
5
  Project-URL: Homepage, https://github.com/isaacus-dev/isaacus-python
6
6
  Project-URL: Repository, https://github.com/isaacus-dev/isaacus-python
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.9
18
18
  Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
21
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
23
  Classifier: Typing :: Typed
23
24
  Requires-Python: >=3.8
@@ -27,11 +28,15 @@ Requires-Dist: httpx<1,>=0.23.0
27
28
  Requires-Dist: pydantic<3,>=1.9.0
28
29
  Requires-Dist: sniffio
29
30
  Requires-Dist: typing-extensions<5,>=4.10
31
+ Provides-Extra: aiohttp
32
+ Requires-Dist: aiohttp; extra == 'aiohttp'
33
+ Requires-Dist: httpx-aiohttp>=0.1.8; extra == 'aiohttp'
30
34
  Description-Content-Type: text/markdown
31
35
 
32
36
  # Isaacus Python API library
33
37
 
34
- [![PyPI version](https://img.shields.io/pypi/v/isaacus.svg)](https://pypi.org/project/isaacus/)
38
+ <!-- prettier-ignore -->
39
+ [![PyPI version](https://img.shields.io/pypi/v/isaacus.svg?label=pypi%20(stable))](https://pypi.org/project/isaacus/)
35
40
 
36
41
  The Isaacus Python library provides convenient access to the Isaacus REST API from any Python 3.8+
37
42
  application. The library includes type definitions for all request params and response fields,
@@ -103,6 +108,41 @@ asyncio.run(main())
103
108
 
104
109
  Functionality between the synchronous and asynchronous clients is otherwise identical.
105
110
 
111
+ ### With aiohttp
112
+
113
+ By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend.
114
+
115
+ You can enable this by installing `aiohttp`:
116
+
117
+ ```sh
118
+ # install from PyPI
119
+ pip install isaacus[aiohttp]
120
+ ```
121
+
122
+ Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
123
+
124
+ ```python
125
+ import asyncio
126
+ from isaacus import DefaultAioHttpClient
127
+ from isaacus import AsyncIsaacus
128
+
129
+
130
+ async def main() -> None:
131
+ async with AsyncIsaacus(
132
+ api_key="My API Key",
133
+ http_client=DefaultAioHttpClient(),
134
+ ) as client:
135
+ universal_classification = await client.classifications.universal.create(
136
+ model="kanon-universal-classifier",
137
+ query="This is a confidentiality clause.",
138
+ texts=["I agree not to tell anyone about the document."],
139
+ )
140
+ print(universal_classification.classifications)
141
+
142
+
143
+ asyncio.run(main())
144
+ ```
145
+
106
146
  ## Using types
107
147
 
108
148
  Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like:
@@ -207,7 +247,7 @@ client.with_options(max_retries=5).classifications.universal.create(
207
247
  ### Timeouts
208
248
 
209
249
  By default requests time out after 1 minute. You can configure this with a `timeout` option,
210
- which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object:
250
+ which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object:
211
251
 
212
252
  ```python
213
253
  from isaacus import Isaacus
@@ -1,17 +1,17 @@
1
- isaacus/__init__.py,sha256=wr6ddQI6xaQxDY-GoGzbBSqxrHcTB0JXRu35iJSo4f8,2537
2
- isaacus/_base_client.py,sha256=ppcrw3sdH8GjKCCOzQq8IFBYxDeZUc7b7ABov3VPbYI,64055
1
+ isaacus/__init__.py,sha256=UFaPvzUXNjeEhBaZBmS5pHsOT4jdgePx9tN-_-7rQvQ,2587
2
+ isaacus/_base_client.py,sha256=d6I9vr1FKcXgokcfwYkXtgkHKrrsJmeKS14QJ6wOIDQ,66133
3
3
  isaacus/_client.py,sha256=-2sN8vyAm_qN2P6JuLxKel6BfvA6mragbo4TPOwLs3Y,16544
4
4
  isaacus/_compat.py,sha256=VWemUKbj6DDkQ-O4baSpHVLJafotzeXmCQGJugfVTIw,6580
5
5
  isaacus/_constants.py,sha256=S14PFzyN9-I31wiV7SmIlL5Ga0MLHxdvegInGdXH7tM,462
6
6
  isaacus/_exceptions.py,sha256=L82uluhizzc94VydHIaJkNxkcG-2DAe74tNhrE2eN2A,3222
7
7
  isaacus/_files.py,sha256=mf4dOgL4b0ryyZlbqLhggD3GVgDf6XxdGFAgce01ugE,3549
8
- isaacus/_models.py,sha256=mB2r2VWQq49jG-F0RIXDrBxPp3v-Eg12wMOtVTNxtv4,29057
8
+ isaacus/_models.py,sha256=KvjsMfb88XZlFUKVoOxr8OyDj47MhoH2OKqWNEbBhk4,30010
9
9
  isaacus/_qs.py,sha256=AOkSz4rHtK4YI3ZU_kzea-zpwBUgEY8WniGmTPyEimc,4846
10
10
  isaacus/_resource.py,sha256=iP_oYhz5enCI58mK7hlwLoPMPh4Q5s8-KBv-jGfv2aM,1106
11
11
  isaacus/_response.py,sha256=aXLF5ia58bjjQXTxY574lh7JfKXiGL2tDTX09klm8lw,28794
12
12
  isaacus/_streaming.py,sha256=tMBfwrfEFWm0v7vWFgjn_lizsoD70lPkYigIBuADaCM,10104
13
- isaacus/_types.py,sha256=WCRAb8jikEJoOi8nza8l5NnOTKgZlpmN5fkiHoKoY08,6144
14
- isaacus/_version.py,sha256=mXn9Voi9RjCSKBeTeNhHxaqwo8_2Zcwq04UnPfptUUc,159
13
+ isaacus/_types.py,sha256=OWOeOlYfnHj59_Lq34ua9FIuR_UTSHQzo4SxlaQBS9Y,6198
14
+ isaacus/_version.py,sha256=zwbXJiibrQFE8HHRyah1KPLOSRL6I7y8cADvmmrXCHw,159
15
15
  isaacus/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  isaacus/_utils/__init__.py,sha256=PNZ_QJuzZEgyYXqkO1HVhGkj5IU9bglVUcw7H-Knjzw,2062
17
17
  isaacus/_utils/_logs.py,sha256=rwa1Yzjbs2JaFn9KQ06rH5c_GSNa--BVwWnWhvvT1tY,777
@@ -41,7 +41,7 @@ isaacus/types/classifications/universal_create_params.py,sha256=0RYreNEY9j5BqxJL
41
41
  isaacus/types/extractions/__init__.py,sha256=9FahSE48xCUSAdkb1DaqRBBznuQ805lmRdlY22iLtQw,254
42
42
  isaacus/types/extractions/answer_extraction.py,sha256=KIqfIsdlYAAjDDwzM8XuFOW_1u5zv34RWbIa3WchBxc,2152
43
43
  isaacus/types/extractions/qa_create_params.py,sha256=iaGIDnuJ8Kek5jW6QMQdbAQJYYiihcjZ1k_iSDbt1Mk,2065
44
- isaacus-0.6.1.dist-info/METADATA,sha256=DKStXBPJ7xD0C_Sbod0sChzemCCr63-EWjyMckbpH4E,14346
45
- isaacus-0.6.1.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
46
- isaacus-0.6.1.dist-info/licenses/LICENSE,sha256=lUen4LYVFVGEVXBsntBAPsQsOWgMkno1e9WfgWkpZ-k,11337
47
- isaacus-0.6.1.dist-info/RECORD,,
44
+ isaacus-0.8.0.dist-info/METADATA,sha256=iSs7rQtVwrt8TEiuZgMoeoGGsctLaus2Dytg9VYL0ww,15546
45
+ isaacus-0.8.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
46
+ isaacus-0.8.0.dist-info/licenses/LICENSE,sha256=lUen4LYVFVGEVXBsntBAPsQsOWgMkno1e9WfgWkpZ-k,11337
47
+ isaacus-0.8.0.dist-info/RECORD,,