From 51c40252cc130836f517ec20601618b67f5b5c43 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 23 Apr 2024 07:42:06 +0000
Subject: [PATCH 001/200] feat(api): OpenAPI spec update via Stainless API
(#18)
---
.github/workflows/ci.yml | 4 +-
.stats.yml | 2 +-
CONTRIBUTING.md | 4 +-
README.md | 60 +++-
api.md | 42 +--
pyproject.toml | 27 +-
requirements-dev.lock | 2 +-
src/maisa/__init__.py | 13 +-
src/maisa/_base_client.py | 58 +++-
src/maisa/_client.py | 18 +-
src/maisa/_constants.py | 4 +-
src/maisa/_exceptions.py | 2 +-
src/maisa/_models.py | 269 +++++++++++++++-
src/maisa/_resource.py | 2 +-
src/maisa/_response.py | 35 ++-
src/maisa/_streaming.py | 73 +++--
src/maisa/_utils/__init__.py | 1 +
src/maisa/_utils/_proxy.py | 2 +-
src/maisa/_utils/_transform.py | 5 +-
src/maisa/_utils/_utils.py | 12 +
src/maisa/_version.py | 2 +-
src/maisa/resources/__init__.py | 16 +-
src/maisa/resources/capabilities/__init__.py | 2 +-
.../resources/capabilities/capabilities.py | 2 +-
src/maisa/resources/capabilities/media.py | 16 +-
.../resources/file_interpreter/__init__.py | 40 ++-
.../file_interpreter/file_interpreter.py | 70 +++--
.../resources/file_interpreter/from_audio.py | 2 +-
.../resources/file_interpreter/from_docx.py | 2 +-
.../resources/file_interpreter/from_html.py | 2 +-
.../resources/file_interpreter/from_image.py | 50 +--
.../resources/file_interpreter/from_pdf.py | 2 +-
.../file_interpreter/from_pdf_scanned.py | 288 ++++++++++++++++++
src/maisa/resources/kpu.py | 51 +++-
src/maisa/resources/mainet/__init__.py | 33 --
src/maisa/resources/mainet/mainet.py | 80 -----
src/maisa/resources/mainet/search.py | 149 ---------
src/maisa/resources/models/__init__.py | 16 +-
src/maisa/resources/models/embeddings.py | 2 +-
src/maisa/resources/models/models.py | 34 +--
src/maisa/resources/models/rerank.py | 169 ----------
src/maisa/types/__init__.py | 3 +-
src/maisa/types/capabilities/__init__.py | 2 +-
.../capabilities/media_compare_params.py | 2 +-
.../capabilities/media_extract_params.py | 2 +-
.../capabilities/media_summarize_params.py | 2 +-
src/maisa/types/capability_compare_params.py | 2 +-
src/maisa/types/capability_extract_params.py | 2 +-
.../types/capability_summarize_params.py | 2 +-
src/maisa/types/file_interpreter/__init__.py | 4 +-
.../from_audio_create_params.py | 2 +-
.../from_docx_create_params.py | 2 +-
.../from_html_create_params.py | 2 +-
.../types/file_interpreter/from_image.py | 11 -
.../from_image_create_params.py | 2 +-
.../from_pdf_create_params.py | 2 +-
.../from_pdf_scanned_create_params.py | 59 ++++
src/maisa/types/kpu_run_params.py | 21 +-
src/maisa/types/kpu_run_response.py | 41 ---
src/maisa/types/mainet/__init__.py | 5 +-
src/maisa/types/mainet/search.py | 23 --
.../types/mainet/search_create_params.py | 12 -
src/maisa/types/models/__init__.py | 4 +-
.../types/models/embedding_create_params.py | 2 +-
src/maisa/types/models/embeddings.py | 2 +-
src/maisa/types/models/rerank.py | 11 -
.../types/models/rerank_create_params.py | 16 -
src/maisa/types/shared/__init__.py | 2 +-
src/maisa/types/shared/text_comparator.py | 2 +-
src/maisa/types/shared/text_extractor.py | 2 +-
src/maisa/types/shared/text_summary.py | 2 +-
tests/__init__.py | 2 +-
tests/api_resources/__init__.py | 2 +-
tests/api_resources/capabilities/__init__.py | 2 +-
.../api_resources/capabilities/test_media.py | 2 +-
.../file_interpreter/__init__.py | 2 +-
.../file_interpreter/test_from_audio.py | 2 +-
.../file_interpreter/test_from_docx.py | 2 +-
.../file_interpreter/test_from_html.py | 2 +-
.../file_interpreter/test_from_image.py | 15 +-
.../file_interpreter/test_from_pdf.py | 2 +-
.../file_interpreter/test_from_pdf_scanned.py | 125 ++++++++
tests/api_resources/mainet/__init__.py | 2 +-
tests/api_resources/mainet/test_search.py | 84 -----
tests/api_resources/models/__init__.py | 2 +-
tests/api_resources/models/test_embeddings.py | 2 +-
tests/api_resources/models/test_rerank.py | 90 ------
tests/api_resources/test_capabilities.py | 2 +-
tests/api_resources/test_kpu.py | 23 +-
tests/test_client.py | 12 +-
tests/test_models.py | 260 +++++++++++++++-
tests/test_response.py | 37 ++-
tests/test_streaming.py | 268 ++++++++++++----
tests/utils.py | 6 +
94 files changed, 1743 insertions(+), 1112 deletions(-)
create mode 100644 src/maisa/resources/file_interpreter/from_pdf_scanned.py
delete mode 100644 src/maisa/resources/mainet/__init__.py
delete mode 100644 src/maisa/resources/mainet/mainet.py
delete mode 100644 src/maisa/resources/mainet/search.py
delete mode 100644 src/maisa/resources/models/rerank.py
delete mode 100644 src/maisa/types/file_interpreter/from_image.py
create mode 100644 src/maisa/types/file_interpreter/from_pdf_scanned_create_params.py
delete mode 100644 src/maisa/types/kpu_run_response.py
delete mode 100644 src/maisa/types/mainet/search.py
delete mode 100644 src/maisa/types/mainet/search_create_params.py
delete mode 100644 src/maisa/types/models/rerank.py
delete mode 100644 src/maisa/types/models/rerank_create_params.py
create mode 100644 tests/api_resources/file_interpreter/test_from_pdf_scanned.py
delete mode 100644 tests/api_resources/mainet/test_search.py
delete mode 100644 tests/api_resources/models/test_rerank.py
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b88cdcbc..4ddec27a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,7 +22,7 @@ jobs:
echo "$HOME/.rye/shims" >> $GITHUB_PATH
env:
RYE_VERSION: 0.24.0
- RYE_INSTALL_OPTION: "--yes"
+ RYE_INSTALL_OPTION: '--yes'
- name: Install dependencies
run: |
@@ -39,3 +39,5 @@ jobs:
- name: Ensure importable
run: |
rye run python -c 'import maisa'
+
+
diff --git a/.stats.yml b/.stats.yml
index c2549479..dd473053 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 15
+configured_endpoints: 14
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 54364e97..cf8d4723 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -86,7 +86,7 @@ Most tests require you to [set up a mock server](https://github.com/stoplightio/
```bash
# you will need npm installed
-npx prism path/to/your/openapi.yml
+npx prism mock path/to/your/openapi.yml
```
```bash
@@ -121,5 +121,5 @@ You can release to package managers by using [the `Publish PyPI` GitHub action](
### Publish manually
-If you need to manually release a package, you can run the `bin/publish-pypi` script with an `PYPI_TOKEN` set on
+If you need to manually release a package, you can run the `bin/publish-pypi` script with a `PYPI_TOKEN` set on
the environment.
diff --git a/README.md b/README.md
index 78f19d95..9a62dbb3 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,8 @@ The Maisa Python library provides convenient access to the Maisa REST API from a
application. The library includes type definitions for all request params and response fields,
and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).
+It is generated with [Stainless](https://www.stainlessapi.com/).
+
## Documentation
The REST API documentation can be found [on docs.maisa.ai](https://docs.maisa.ai/). The full API of this library can be found in [api.md](api.md).
@@ -22,13 +24,9 @@ pip install --pre maisa
The full API of this library can be found in [api.md](api.md).
```python
-import os
from maisa import Maisa
-client = Maisa(
- # This is the default and can be omitted
- api_key=os.environ.get("MAISA_API_KEY"),
-)
+client = Maisa()
text_summary = client.capabilities.summarize(
text="Example long text...",
@@ -46,14 +44,10 @@ so that your API Key is not stored in source control.
Simply import `AsyncMaisa` instead of `Maisa` and use `await` with each API call:
```python
-import os
import asyncio
from maisa import AsyncMaisa
-client = AsyncMaisa(
- # This is the default and can be omitted
- api_key=os.environ.get("MAISA_API_KEY"),
-)
+client = AsyncMaisa()
async def main() -> None:
@@ -70,10 +64,10 @@ Functionality between the synchronous and asynchronous clients is otherwise iden
## Using types
-Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev), which provide helper methods for things like:
+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:
-- Serializing back into JSON, `model.model_dump_json(indent=2, exclude_unset=True)`
-- Converting to a dictionary, `model.model_dump(exclude_unset=True)`
+- Serializing back into JSON, `model.to_json()`
+- Converting to a dictionary, `model.to_dict()`
Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`.
@@ -235,6 +229,41 @@ with client.capabilities.with_streaming_response.summarize(
The context manager is required so that the response will reliably be closed.
+### Making custom/undocumented requests
+
+This library is typed for convenient access the documented API.
+
+If you need to access undocumented endpoints, params, or response properties, the library can still be used.
+
+#### Undocumented endpoints
+
+To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other
+http verbs. Options on the client will be respected (such as retries) will be respected when making this
+request.
+
+```py
+import httpx
+
+response = client.post(
+ "/foo",
+ cast_to=httpx.Response,
+ body={"my_param": True},
+)
+
+print(response.headers.get("x-foo"))
+```
+
+#### Undocumented request params
+
+If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request
+options.
+
+#### Undocumented response properties
+
+To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You
+can also get all the extra fields on the Pydantic model as a dict with
+[`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra).
+
### Configuring the HTTP client
You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including:
@@ -244,13 +273,12 @@ You can directly override the [httpx client](https://www.python-httpx.org/api/#c
- Additional [advanced](https://www.python-httpx.org/advanced/#client-instances) functionality
```python
-import httpx
-from maisa import Maisa
+from maisa import Maisa, DefaultHttpxClient
client = Maisa(
# Or use the `MAISA_BASE_URL` env var
base_url="http://my.test.server.example.com:8083",
- http_client=httpx.Client(
+ http_client=DefaultHttpxClient(
proxies="http://my.test.proxy.example.com",
transport=httpx.HTTPTransport(local_address="0.0.0.0"),
),
diff --git a/api.md b/api.md
index 39b91ab5..406ac2f4 100644
--- a/api.md
+++ b/api.md
@@ -34,43 +34,43 @@ Methods:
- client.models.embeddings.create(\*\*params) -> Embeddings
-## RerankResource
+# Kpu
Types:
```python
-from maisa.types.models import Rerank
+from maisa.types import KpuRunResponse
```
Methods:
-- client.models.rerank.create(\*\*params) -> Rerank
+- client.kpu.run(\*\*params) -> object
-# Kpu
+# FileInterpreter
+
+## FromPdf
Types:
```python
-from maisa.types import KpuRunResponse
+from maisa.types.file_interpreter import FromPdfCreateResponse
```
Methods:
-- client.kpu.run(\*\*params) -> KpuRunResponse
-
-# FileInterpreter
+- client.file_interpreter.from_pdf.create(\*\*params) -> object
-## FromPdf
+## FromPdfScanned
Types:
```python
-from maisa.types.file_interpreter import FromPdfCreateResponse
+from maisa.types.file_interpreter import FromPdfScannedCreateResponse
```
Methods:
-- client.file_interpreter.from_pdf.create(\*\*params) -> object
+- client.file_interpreter.from_pdf_scanned.create(\*\*params) -> object
## FromDocx
@@ -96,17 +96,17 @@ Methods:
- client.file_interpreter.from_html.create(\*\*params) -> object
-## FromImageResource
+## FromImage
Types:
```python
-from maisa.types.file_interpreter import FromImage
+from maisa.types.file_interpreter import FromImageCreateResponse
```
Methods:
-- client.file_interpreter.from_image.create(\*\*params) -> FromImage
+- client.file_interpreter.from_image.create(\*\*params) -> object
## FromAudio
@@ -119,17 +119,3 @@ from maisa.types.file_interpreter import FromAudioCreateResponse
Methods:
- client.file_interpreter.from_audio.create(\*\*params) -> object
-
-# Mainet
-
-## SearchResource
-
-Types:
-
-```python
-from maisa.types.mainet import Search
-```
-
-Methods:
-
-- client.mainet.search.create(\*\*params) -> Search
diff --git a/pyproject.toml b/pyproject.toml
index 31f9a12c..e3159c4e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,10 +2,10 @@
name = "maisa"
version = "0.1.0-alpha.4"
description = "The official Python library for the Maisa API"
-readme = "README.md"
+dynamic = ["readme"]
license = "Apache-2.0"
authors = [
-{ name = "Maisa", email = "support@clibrain.com" },
+{ name = "Maisa", email = "support@maisa.ai" },
]
dependencies = [
"httpx>=0.23.0, <1",
@@ -48,7 +48,7 @@ Repository = "https://github.com/maisaai/python-sdk"
managed = true
# version pins are in requirements-dev.lock
dev-dependencies = [
- "pyright",
+ "pyright>=1.1.359",
"mypy",
"respx",
"pytest",
@@ -88,7 +88,7 @@ typecheck = { chain = [
"typecheck:mypy" = "mypy ."
[build-system]
-requires = ["hatchling"]
+requires = ["hatchling", "hatch-fancy-pypi-readme"]
build-backend = "hatchling.build"
[tool.hatch.build]
@@ -99,6 +99,17 @@ include = [
[tool.hatch.build.targets.wheel]
packages = ["src/maisa"]
+[tool.hatch.metadata.hooks.fancy-pypi-readme]
+content-type = "text/markdown"
+
+[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
+path = "README.md"
+
+[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
+# replace relative links with absolute links
+pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)'
+replacement = '[\1](https://github.com/maisaai/python-sdk/tree/main/\g<2>)'
+
[tool.black]
line-length = 120
target-version = ["py37"]
@@ -130,6 +141,7 @@ reportImplicitOverride = true
reportImportCycles = false
reportPrivateUsage = false
+
[tool.ruff]
line-length = 120
output-format = "grouped"
@@ -149,7 +161,9 @@ select = [
"T201",
"T203",
# misuse of typing.TYPE_CHECKING
- "TCH004"
+ "TCH004",
+ # import rules
+ "TID251",
]
ignore = [
# mutable defaults
@@ -165,6 +179,9 @@ ignore-init-module-imports = true
[tool.ruff.format]
docstring-code-format = true
+[tool.ruff.lint.flake8-tidy-imports.banned-api]
+"functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead"
+
[tool.ruff.lint.isort]
length-sort = true
length-sort-straight = true
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 204fcb08..7a6ac29b 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -63,7 +63,7 @@ pydantic==2.4.2
# via maisa
pydantic-core==2.10.1
# via pydantic
-pyright==1.1.351
+pyright==1.1.359
pytest==7.1.1
# via pytest-asyncio
pytest-asyncio==0.21.1
diff --git a/src/maisa/__init__.py b/src/maisa/__init__.py
index 8855fe8f..dce249d9 100644
--- a/src/maisa/__init__.py
+++ b/src/maisa/__init__.py
@@ -1,12 +1,13 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from . import types
-from ._types import NoneType, Transport, ProxiesTypes
+from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes
from ._utils import file_from_path
from ._client import Maisa, Client, Stream, Timeout, Transport, AsyncMaisa, AsyncClient, AsyncStream, RequestOptions
from ._models import BaseModel
from ._version import __title__, __version__
from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse
+from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS
from ._exceptions import (
APIError,
MaisaError,
@@ -23,6 +24,7 @@
UnprocessableEntityError,
APIResponseValidationError,
)
+from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient
from ._utils._logs import setup_logging as _setup_logging
__all__ = [
@@ -32,6 +34,8 @@
"NoneType",
"Transport",
"ProxiesTypes",
+ "NotGiven",
+ "NOT_GIVEN",
"MaisaError",
"APIError",
"APIStatusError",
@@ -56,6 +60,11 @@
"AsyncMaisa",
"file_from_path",
"BaseModel",
+ "DEFAULT_TIMEOUT",
+ "DEFAULT_MAX_RETRIES",
+ "DEFAULT_CONNECTION_LIMITS",
+ "DefaultHttpxClient",
+ "DefaultAsyncHttpxClient",
]
_setup_logging()
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index 2b3a1f98..e8f6709d 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -29,7 +29,6 @@
cast,
overload,
)
-from functools import lru_cache
from typing_extensions import Literal, override, get_origin
import anyio
@@ -61,7 +60,7 @@
RequestOptions,
ModelBuilderProtocol,
)
-from ._utils import is_dict, is_list, is_given, is_mapping
+from ._utils import is_dict, is_list, is_given, lru_cache, is_mapping
from ._compat import model_copy, model_dump
from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type
from ._response import (
@@ -71,13 +70,13 @@
extract_response_type,
)
from ._constants import (
- DEFAULT_LIMITS,
DEFAULT_TIMEOUT,
MAX_RETRY_DELAY,
DEFAULT_MAX_RETRIES,
INITIAL_RETRY_DELAY,
RAW_RESPONSE_HEADER,
OVERRIDE_CAST_TO_HEADER,
+ DEFAULT_CONNECTION_LIMITS,
)
from ._streaming import Stream, SSEDecoder, AsyncStream, SSEBytesDecoder
from ._exceptions import (
@@ -360,6 +359,11 @@ def __init__(
self._strict_response_validation = _strict_response_validation
self._idempotency_header = None
+ if max_retries is None: # pyright: ignore[reportUnnecessaryComparison]
+ raise TypeError(
+ "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `maisa.DEFAULT_MAX_RETRIES`"
+ )
+
def _enforce_trailing_slash(self, url: URL) -> URL:
if url.raw_path.endswith(b"/"):
return url
@@ -710,7 +714,27 @@ def _idempotency_key(self) -> str:
return f"stainless-python-retry-{uuid.uuid4()}"
-class SyncHttpxClientWrapper(httpx.Client):
+class _DefaultHttpxClient(httpx.Client):
+ def __init__(self, **kwargs: Any) -> None:
+ kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
+ kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS)
+ kwargs.setdefault("follow_redirects", True)
+ super().__init__(**kwargs)
+
+
+if TYPE_CHECKING:
+ DefaultHttpxClient = httpx.Client
+ """An alias to `httpx.Client` that provides the same defaults that this SDK
+ uses internally.
+
+ This is useful because overriding the `http_client` with your own instance of
+ `httpx.Client` will result in httpx's defaults being used, not ours.
+ """
+else:
+ DefaultHttpxClient = _DefaultHttpxClient
+
+
+class SyncHttpxClientWrapper(DefaultHttpxClient):
def __del__(self) -> None:
try:
self.close()
@@ -746,7 +770,7 @@ def __init__(
if http_client is not None:
raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`")
else:
- limits = DEFAULT_LIMITS
+ limits = DEFAULT_CONNECTION_LIMITS
if transport is not None:
warnings.warn(
@@ -1243,7 +1267,27 @@ def get_api_list(
return self._request_api_list(model, page, opts)
-class AsyncHttpxClientWrapper(httpx.AsyncClient):
+class _DefaultAsyncHttpxClient(httpx.AsyncClient):
+ def __init__(self, **kwargs: Any) -> None:
+ kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
+ kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS)
+ kwargs.setdefault("follow_redirects", True)
+ super().__init__(**kwargs)
+
+
+if TYPE_CHECKING:
+ DefaultAsyncHttpxClient = httpx.AsyncClient
+ """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK
+ uses internally.
+
+ This is useful because overriding the `http_client` with your own instance of
+ `httpx.AsyncClient` will result in httpx's defaults being used, not ours.
+ """
+else:
+ DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient
+
+
+class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient):
def __del__(self) -> None:
try:
# TODO(someday): support non asyncio runtimes here
@@ -1280,7 +1324,7 @@ def __init__(
if http_client is not None:
raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`")
else:
- limits = DEFAULT_LIMITS
+ limits = DEFAULT_CONNECTION_LIMITS
if transport is not None:
warnings.warn(
diff --git a/src/maisa/_client.py b/src/maisa/_client.py
index 11d68726..f7c6e9e5 100644
--- a/src/maisa/_client.py
+++ b/src/maisa/_client.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
@@ -50,7 +50,6 @@ class Maisa(SyncAPIClient):
models: resources.Models
kpu: resources.Kpu
file_interpreter: resources.FileInterpreter
- mainet: resources.Mainet
with_raw_response: MaisaWithRawResponse
with_streaming_response: MaisaWithStreamedResponse
@@ -66,7 +65,9 @@ def __init__(
max_retries: int = DEFAULT_MAX_RETRIES,
default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
- # Configure a custom httpx client. See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details.
+ # Configure a custom httpx client.
+ # We provide a `DefaultHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`.
+ # See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details.
http_client: httpx.Client | None = None,
# Enable or disable schema validation for data returned by the API.
# When enabled an error APIResponseValidationError is raised
@@ -110,7 +111,6 @@ def __init__(
self.models = resources.Models(self)
self.kpu = resources.Kpu(self)
self.file_interpreter = resources.FileInterpreter(self)
- self.mainet = resources.Mainet(self)
self.with_raw_response = MaisaWithRawResponse(self)
self.with_streaming_response = MaisaWithStreamedResponse(self)
@@ -224,7 +224,6 @@ class AsyncMaisa(AsyncAPIClient):
models: resources.AsyncModels
kpu: resources.AsyncKpu
file_interpreter: resources.AsyncFileInterpreter
- mainet: resources.AsyncMainet
with_raw_response: AsyncMaisaWithRawResponse
with_streaming_response: AsyncMaisaWithStreamedResponse
@@ -240,7 +239,9 @@ def __init__(
max_retries: int = DEFAULT_MAX_RETRIES,
default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
- # Configure a custom httpx client. See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details.
+ # Configure a custom httpx client.
+ # We provide a `DefaultAsyncHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`.
+ # See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details.
http_client: httpx.AsyncClient | None = None,
# Enable or disable schema validation for data returned by the API.
# When enabled an error APIResponseValidationError is raised
@@ -284,7 +285,6 @@ def __init__(
self.models = resources.AsyncModels(self)
self.kpu = resources.AsyncKpu(self)
self.file_interpreter = resources.AsyncFileInterpreter(self)
- self.mainet = resources.AsyncMainet(self)
self.with_raw_response = AsyncMaisaWithRawResponse(self)
self.with_streaming_response = AsyncMaisaWithStreamedResponse(self)
@@ -399,7 +399,6 @@ def __init__(self, client: Maisa) -> None:
self.models = resources.ModelsWithRawResponse(client.models)
self.kpu = resources.KpuWithRawResponse(client.kpu)
self.file_interpreter = resources.FileInterpreterWithRawResponse(client.file_interpreter)
- self.mainet = resources.MainetWithRawResponse(client.mainet)
class AsyncMaisaWithRawResponse:
@@ -408,7 +407,6 @@ def __init__(self, client: AsyncMaisa) -> None:
self.models = resources.AsyncModelsWithRawResponse(client.models)
self.kpu = resources.AsyncKpuWithRawResponse(client.kpu)
self.file_interpreter = resources.AsyncFileInterpreterWithRawResponse(client.file_interpreter)
- self.mainet = resources.AsyncMainetWithRawResponse(client.mainet)
class MaisaWithStreamedResponse:
@@ -417,7 +415,6 @@ def __init__(self, client: Maisa) -> None:
self.models = resources.ModelsWithStreamingResponse(client.models)
self.kpu = resources.KpuWithStreamingResponse(client.kpu)
self.file_interpreter = resources.FileInterpreterWithStreamingResponse(client.file_interpreter)
- self.mainet = resources.MainetWithStreamingResponse(client.mainet)
class AsyncMaisaWithStreamedResponse:
@@ -426,7 +423,6 @@ def __init__(self, client: AsyncMaisa) -> None:
self.models = resources.AsyncModelsWithStreamingResponse(client.models)
self.kpu = resources.AsyncKpuWithStreamingResponse(client.kpu)
self.file_interpreter = resources.AsyncFileInterpreterWithStreamingResponse(client.file_interpreter)
- self.mainet = resources.AsyncMainetWithStreamingResponse(client.mainet)
Client = Maisa
diff --git a/src/maisa/_constants.py b/src/maisa/_constants.py
index bf15141a..a2ac3b6f 100644
--- a/src/maisa/_constants.py
+++ b/src/maisa/_constants.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import httpx
@@ -8,7 +8,7 @@
# default timeout is 1 minute
DEFAULT_TIMEOUT = httpx.Timeout(timeout=60.0, connect=5.0)
DEFAULT_MAX_RETRIES = 2
-DEFAULT_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20)
+DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20)
INITIAL_RETRY_DELAY = 0.5
MAX_RETRY_DELAY = 8.0
diff --git a/src/maisa/_exceptions.py b/src/maisa/_exceptions.py
index 770f95fa..a5055ef0 100644
--- a/src/maisa/_exceptions.py
+++ b/src/maisa/_exceptions.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index 81089149..ff3f54e2 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import os
import inspect
from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast
from datetime import date, datetime
@@ -10,6 +11,7 @@
Protocol,
Required,
TypedDict,
+ TypeGuard,
final,
override,
runtime_checkable,
@@ -30,7 +32,20 @@
AnyMapping,
HttpxRequestFiles,
)
-from ._utils import is_list, is_given, is_mapping, parse_date, parse_datetime, strip_not_given
+from ._utils import (
+ PropertyInfo,
+ is_list,
+ is_given,
+ lru_cache,
+ is_mapping,
+ parse_date,
+ coerce_boolean,
+ parse_datetime,
+ strip_not_given,
+ extract_type_arg,
+ is_annotated_type,
+ strip_annotated_type,
+)
from ._compat import (
PYDANTIC_V2,
ConfigDict,
@@ -46,6 +61,9 @@
)
from ._constants import RAW_RESPONSE_HEADER
+if TYPE_CHECKING:
+ from pydantic_core.core_schema import ModelField, ModelFieldsSchema
+
__all__ = ["BaseModel", "GenericModel"]
_T = TypeVar("_T")
@@ -58,7 +76,9 @@ class _ConfigProtocol(Protocol):
class BaseModel(pydantic.BaseModel):
if PYDANTIC_V2:
- model_config: ClassVar[ConfigDict] = ConfigDict(extra="allow")
+ model_config: ClassVar[ConfigDict] = ConfigDict(
+ extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true"))
+ )
else:
@property
@@ -70,6 +90,79 @@ def model_fields_set(self) -> set[str]:
class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
extra: Any = pydantic.Extra.allow # type: ignore
+ def to_dict(
+ self,
+ *,
+ mode: Literal["json", "python"] = "python",
+ use_api_names: bool = True,
+ exclude_unset: bool = True,
+ exclude_defaults: bool = False,
+ exclude_none: bool = False,
+ warnings: bool = True,
+ ) -> dict[str, object]:
+ """Recursively generate a dictionary representation of the model, optionally specifying which fields to include or exclude.
+
+ By default, fields that were not set by the API will not be included,
+ and keys will match the API response, *not* the property names from the model.
+
+ For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property,
+ the output will use the `"fooBar"` key (unless `use_api_names=False` is passed).
+
+ Args:
+ mode:
+ If mode is 'json', the dictionary will only contain JSON serializable types. e.g. `datetime` will be turned into a string, `"2024-3-22T18:11:19.117000Z"`.
+ If mode is 'python', the dictionary may contain any Python objects. e.g. `datetime(2024, 3, 22)`
+
+ use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`.
+ exclude_unset: Whether to exclude fields that have not been explicitly set.
+ exclude_defaults: Whether to exclude fields that are set to their default value from the output.
+ exclude_none: Whether to exclude fields that have a value of `None` from the output.
+ warnings: Whether to log warnings when invalid fields are encountered. This is only supported in Pydantic v2.
+ """
+ return self.model_dump(
+ mode=mode,
+ by_alias=use_api_names,
+ exclude_unset=exclude_unset,
+ exclude_defaults=exclude_defaults,
+ exclude_none=exclude_none,
+ warnings=warnings,
+ )
+
+ def to_json(
+ self,
+ *,
+ indent: int | None = 2,
+ use_api_names: bool = True,
+ exclude_unset: bool = True,
+ exclude_defaults: bool = False,
+ exclude_none: bool = False,
+ warnings: bool = True,
+ ) -> str:
+ """Generates a JSON string representing this model as it would be received from or sent to the API (but with indentation).
+
+ By default, fields that were not set by the API will not be included,
+ and keys will match the API response, *not* the property names from the model.
+
+ For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property,
+ the output will use the `"fooBar"` key (unless `use_api_names=False` is passed).
+
+ Args:
+ indent: Indentation to use in the JSON output. If `None` is passed, the output will be compact. Defaults to `2`
+ use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`.
+ exclude_unset: Whether to exclude fields that have not been explicitly set.
+ exclude_defaults: Whether to exclude fields that have the default value.
+ exclude_none: Whether to exclude fields that have a value of `None`.
+ warnings: Whether to show any warnings that occurred during serialization. This is only supported in Pydantic v2.
+ """
+ return self.model_dump_json(
+ indent=indent,
+ by_alias=use_api_names,
+ exclude_unset=exclude_unset,
+ exclude_defaults=exclude_defaults,
+ exclude_none=exclude_none,
+ warnings=warnings,
+ )
+
@override
def __str__(self) -> str:
# mypy complains about an invalid self arg
@@ -259,7 +352,6 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object:
def is_basemodel(type_: type) -> bool:
"""Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`"""
- origin = get_origin(type_) or type_
if is_union(type_):
for variant in get_args(type_):
if is_basemodel(variant):
@@ -267,14 +359,29 @@ def is_basemodel(type_: type) -> bool:
return False
+ return is_basemodel_type(type_)
+
+
+def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]:
+ origin = get_origin(type_) or type_
return issubclass(origin, BaseModel) or issubclass(origin, GenericModel)
-def construct_type(*, value: object, type_: type) -> object:
+def construct_type(*, value: object, type_: object) -> object:
"""Loose coercion to the expected type with construction of nested values.
If the given value does not match the expected type then it is returned as-is.
"""
+ # we allow `object` as the input type because otherwise, passing things like
+ # `Literal['value']` will be reported as a type error by type checkers
+ type_ = cast("type[object]", type_)
+
+ # unwrap `Annotated[T, ...]` -> `T`
+ if is_annotated_type(type_):
+ meta: tuple[Any, ...] = get_args(type_)[1:]
+ type_ = extract_type_arg(type_, 0)
+ else:
+ meta = tuple()
# we need to use the origin class for any types that are subscripted generics
# e.g. Dict[str, object]
@@ -287,6 +394,28 @@ def construct_type(*, value: object, type_: type) -> object:
except Exception:
pass
+ # if the type is a discriminated union then we want to construct the right variant
+ # in the union, even if the data doesn't match exactly, otherwise we'd break code
+ # that relies on the constructed class types, e.g.
+ #
+ # class FooType:
+ # kind: Literal['foo']
+ # value: str
+ #
+ # class BarType:
+ # kind: Literal['bar']
+ # value: int
+ #
+ # without this block, if the data we get is something like `{'kind': 'bar', 'value': 'foo'}` then
+ # we'd end up constructing `FooType` when it should be `BarType`.
+ discriminator = _build_discriminated_union_meta(union=type_, meta_annotations=meta)
+ if discriminator and is_mapping(value):
+ variant_value = value.get(discriminator.field_alias_from or discriminator.field_name)
+ if variant_value and isinstance(variant_value, str):
+ variant_type = discriminator.mapping.get(variant_value)
+ if variant_type:
+ return construct_type(type_=variant_type, value=value)
+
# if the data is not valid, use the first variant that doesn't fail while deserializing
for variant in args:
try:
@@ -344,6 +473,129 @@ def construct_type(*, value: object, type_: type) -> object:
return value
+@runtime_checkable
+class CachedDiscriminatorType(Protocol):
+ __discriminator__: DiscriminatorDetails
+
+
+class DiscriminatorDetails:
+ field_name: str
+ """The name of the discriminator field in the variant class, e.g.
+
+ ```py
+ class Foo(BaseModel):
+ type: Literal['foo']
+ ```
+
+ Will result in field_name='type'
+ """
+
+ field_alias_from: str | None
+ """The name of the discriminator field in the API response, e.g.
+
+ ```py
+ class Foo(BaseModel):
+ type: Literal['foo'] = Field(alias='type_from_api')
+ ```
+
+ Will result in field_alias_from='type_from_api'
+ """
+
+ mapping: dict[str, type]
+ """Mapping of discriminator value to variant type, e.g.
+
+ {'foo': FooVariant, 'bar': BarVariant}
+ """
+
+ def __init__(
+ self,
+ *,
+ mapping: dict[str, type],
+ discriminator_field: str,
+ discriminator_alias: str | None,
+ ) -> None:
+ self.mapping = mapping
+ self.field_name = discriminator_field
+ self.field_alias_from = discriminator_alias
+
+
+def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None:
+ if isinstance(union, CachedDiscriminatorType):
+ return union.__discriminator__
+
+ discriminator_field_name: str | None = None
+
+ for annotation in meta_annotations:
+ if isinstance(annotation, PropertyInfo) and annotation.discriminator is not None:
+ discriminator_field_name = annotation.discriminator
+ break
+
+ if not discriminator_field_name:
+ return None
+
+ mapping: dict[str, type] = {}
+ discriminator_alias: str | None = None
+
+ for variant in get_args(union):
+ variant = strip_annotated_type(variant)
+ if is_basemodel_type(variant):
+ if PYDANTIC_V2:
+ field = _extract_field_schema_pv2(variant, discriminator_field_name)
+ if not field:
+ continue
+
+ # Note: if one variant defines an alias then they all should
+ discriminator_alias = field.get("serialization_alias")
+
+ field_schema = field["schema"]
+
+ if field_schema["type"] == "literal":
+ for entry in field_schema["expected"]:
+ if isinstance(entry, str):
+ mapping[entry] = variant
+ else:
+ field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
+ if not field_info:
+ continue
+
+ # Note: if one variant defines an alias then they all should
+ discriminator_alias = field_info.alias
+
+ if field_info.annotation and is_literal_type(field_info.annotation):
+ for entry in get_args(field_info.annotation):
+ if isinstance(entry, str):
+ mapping[entry] = variant
+
+ if not mapping:
+ return None
+
+ details = DiscriminatorDetails(
+ mapping=mapping,
+ discriminator_field=discriminator_field_name,
+ discriminator_alias=discriminator_alias,
+ )
+ cast(CachedDiscriminatorType, union).__discriminator__ = details
+ return details
+
+
+def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None:
+ schema = model.__pydantic_core_schema__
+ if schema["type"] != "model":
+ return None
+
+ fields_schema = schema["schema"]
+ if fields_schema["type"] != "model-fields":
+ return None
+
+ fields_schema = cast("ModelFieldsSchema", fields_schema)
+
+ field = fields_schema["fields"].get(field_name)
+ if not field:
+ return None
+
+ return cast("ModelField", field) # pyright: ignore[reportUnnecessaryCast]
+
+
def validate_type(*, type_: type[_T], value: object) -> _T:
"""Strict validation that the given value matches the expected type"""
if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel):
@@ -363,7 +615,14 @@ class GenericModel(BaseGenericModel, BaseModel):
if PYDANTIC_V2:
- from pydantic import TypeAdapter
+ from pydantic import TypeAdapter as _TypeAdapter
+
+ _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter))
+
+ if TYPE_CHECKING:
+ from pydantic import TypeAdapter
+ else:
+ TypeAdapter = _CachedTypeAdapter
def _validate_non_model_type(*, type_: type[_T], value: object) -> _T:
return TypeAdapter(type_).validate_python(value)
diff --git a/src/maisa/_resource.py b/src/maisa/_resource.py
index ee940c3d..45878bbb 100644
--- a/src/maisa/_resource.py
+++ b/src/maisa/_resource.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/_response.py b/src/maisa/_response.py
index 4c3f8258..0a3b276a 100644
--- a/src/maisa/_response.py
+++ b/src/maisa/_response.py
@@ -25,7 +25,7 @@
import pydantic
from ._types import NoneType
-from ._utils import is_given, extract_type_var_from_base
+from ._utils import is_given, extract_type_arg, is_annotated_type, extract_type_var_from_base
from ._models import BaseModel, is_basemodel
from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER
from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type
@@ -121,6 +121,10 @@ def __repr__(self) -> str:
)
def _parse(self, *, to: type[_T] | None = None) -> R | _T:
+ # unwrap `Annotated[T, ...]` -> `T`
+ if to and is_annotated_type(to):
+ to = extract_type_arg(to, 0)
+
if self._is_sse_stream:
if to:
if not is_stream_class_type(to):
@@ -162,6 +166,11 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
)
cast_to = to if to is not None else self._cast_to
+
+ # unwrap `Annotated[T, ...]` -> `T`
+ if is_annotated_type(cast_to):
+ cast_to = extract_type_arg(cast_to, 0)
+
if cast_to is NoneType:
return cast(R, None)
@@ -172,6 +181,12 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
if cast_to == bytes:
return cast(R, response.content)
+ if cast_to == int:
+ return cast(R, int(response.text))
+
+ if cast_to == float:
+ return cast(R, float(response.text))
+
origin = get_origin(cast_to) or cast_to
if origin == APIResponse:
@@ -273,6 +288,8 @@ class MyModel(BaseModel):
- `list`
- `Union`
- `str`
+ - `int`
+ - `float`
- `httpx.Response`
"""
cache_key = to if to is not None else self._cast_to
@@ -622,7 +639,7 @@ def to_streamed_response_wrapper(func: Callable[P, R]) -> Callable[P, ResponseCo
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[APIResponse[R]]:
- extra_headers = {**(cast(Any, kwargs.get("extra_headers")) or {})}
+ extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "stream"
kwargs["extra_headers"] = extra_headers
@@ -643,7 +660,7 @@ def async_to_streamed_response_wrapper(
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[AsyncAPIResponse[R]]:
- extra_headers = {**(cast(Any, kwargs.get("extra_headers")) or {})}
+ extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "stream"
kwargs["extra_headers"] = extra_headers
@@ -667,7 +684,7 @@ def to_custom_streamed_response_wrapper(
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[_APIResponseT]:
- extra_headers = {**(cast(Any, kwargs.get("extra_headers")) or {})}
+ extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "stream"
extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
@@ -692,7 +709,7 @@ def async_to_custom_streamed_response_wrapper(
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[_AsyncAPIResponseT]:
- extra_headers = {**(cast(Any, kwargs.get("extra_headers")) or {})}
+ extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "stream"
extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
@@ -712,7 +729,7 @@ def to_raw_response_wrapper(func: Callable[P, R]) -> Callable[P, APIResponse[R]]
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> APIResponse[R]:
- extra_headers = {**(cast(Any, kwargs.get("extra_headers")) or {})}
+ extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "raw"
kwargs["extra_headers"] = extra_headers
@@ -729,7 +746,7 @@ def async_to_raw_response_wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P
@functools.wraps(func)
async def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncAPIResponse[R]:
- extra_headers = {**(cast(Any, kwargs.get("extra_headers")) or {})}
+ extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "raw"
kwargs["extra_headers"] = extra_headers
@@ -751,7 +768,7 @@ def to_custom_raw_response_wrapper(
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> _APIResponseT:
- extra_headers = {**(cast(Any, kwargs.get("extra_headers")) or {})}
+ extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "raw"
extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
@@ -774,7 +791,7 @@ def async_to_custom_raw_response_wrapper(
@functools.wraps(func)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> Awaitable[_AsyncAPIResponseT]:
- extra_headers = {**(cast(Any, kwargs.get("extra_headers")) or {})}
+ extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
extra_headers[RAW_RESPONSE_HEADER] = "raw"
extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
diff --git a/src/maisa/_streaming.py b/src/maisa/_streaming.py
index 0b48ef2c..4093e98c 100644
--- a/src/maisa/_streaming.py
+++ b/src/maisa/_streaming.py
@@ -23,7 +23,7 @@ class Stream(Generic[_T]):
response: httpx.Response
- _decoder: SSEDecoder | SSEBytesDecoder
+ _decoder: SSEBytesDecoder
def __init__(
self,
@@ -46,10 +46,7 @@ def __iter__(self) -> Iterator[_T]:
yield item
def _iter_events(self) -> Iterator[ServerSentEvent]:
- if isinstance(self._decoder, SSEBytesDecoder):
- yield from self._decoder.iter_bytes(self.response.iter_bytes())
- else:
- yield from self._decoder.iter(self.response.iter_lines())
+ yield from self._decoder.iter_bytes(self.response.iter_bytes())
def __stream__(self) -> Iterator[_T]:
cast_to = cast(Any, self._cast_to)
@@ -112,12 +109,8 @@ async def __aiter__(self) -> AsyncIterator[_T]:
yield item
async def _iter_events(self) -> AsyncIterator[ServerSentEvent]:
- if isinstance(self._decoder, SSEBytesDecoder):
- async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()):
- yield sse
- else:
- async for sse in self._decoder.aiter(self.response.aiter_lines()):
- yield sse
+ async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()):
+ yield sse
async def __stream__(self) -> AsyncIterator[_T]:
cast_to = cast(Any, self._cast_to)
@@ -205,21 +198,49 @@ def __init__(self) -> None:
self._last_event_id = None
self._retry = None
- def iter(self, iterator: Iterator[str]) -> Iterator[ServerSentEvent]:
- """Given an iterator that yields lines, iterate over it & yield every event encountered"""
- for line in iterator:
- line = line.rstrip("\n")
- sse = self.decode(line)
- if sse is not None:
- yield sse
-
- async def aiter(self, iterator: AsyncIterator[str]) -> AsyncIterator[ServerSentEvent]:
- """Given an async iterator that yields lines, iterate over it & yield every event encountered"""
- async for line in iterator:
- line = line.rstrip("\n")
- sse = self.decode(line)
- if sse is not None:
- yield sse
+ def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]:
+ """Given an iterator that yields raw binary data, iterate over it & yield every event encountered"""
+ for chunk in self._iter_chunks(iterator):
+ # Split before decoding so splitlines() only uses \r and \n
+ for raw_line in chunk.splitlines():
+ line = raw_line.decode("utf-8")
+ sse = self.decode(line)
+ if sse:
+ yield sse
+
+ def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]:
+ """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks"""
+ data = b""
+ for chunk in iterator:
+ for line in chunk.splitlines(keepends=True):
+ data += line
+ if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")):
+ yield data
+ data = b""
+ if data:
+ yield data
+
+ async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]:
+ """Given an iterator that yields raw binary data, iterate over it & yield every event encountered"""
+ async for chunk in self._aiter_chunks(iterator):
+ # Split before decoding so splitlines() only uses \r and \n
+ for raw_line in chunk.splitlines():
+ line = raw_line.decode("utf-8")
+ sse = self.decode(line)
+ if sse:
+ yield sse
+
+ async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]:
+ """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks"""
+ data = b""
+ async for chunk in iterator:
+ for line in chunk.splitlines(keepends=True):
+ data += line
+ if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")):
+ yield data
+ data = b""
+ if data:
+ yield data
def decode(self, line: str) -> ServerSentEvent | None:
# See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501
diff --git a/src/maisa/_utils/__init__.py b/src/maisa/_utils/__init__.py
index 56978941..31b5b227 100644
--- a/src/maisa/_utils/__init__.py
+++ b/src/maisa/_utils/__init__.py
@@ -6,6 +6,7 @@
is_list as is_list,
is_given as is_given,
is_tuple as is_tuple,
+ lru_cache as lru_cache,
is_mapping as is_mapping,
is_tuple_t as is_tuple_t,
parse_date as parse_date,
diff --git a/src/maisa/_utils/_proxy.py b/src/maisa/_utils/_proxy.py
index b9c12dc3..c46a62a6 100644
--- a/src/maisa/_utils/_proxy.py
+++ b/src/maisa/_utils/_proxy.py
@@ -10,7 +10,7 @@
class LazyProxy(Generic[T], ABC):
"""Implements data methods to pretend that an instance is another instance.
- This includes forwarding attribute access and othe methods.
+ This includes forwarding attribute access and other methods.
"""
# Note: we have to special case proxies that themselves return proxies
diff --git a/src/maisa/_utils/_transform.py b/src/maisa/_utils/_transform.py
index 1bd1330c..47e262a5 100644
--- a/src/maisa/_utils/_transform.py
+++ b/src/maisa/_utils/_transform.py
@@ -51,6 +51,7 @@ class MyParams(TypedDict):
alias: str | None
format: PropertyFormat | None
format_template: str | None
+ discriminator: str | None
def __init__(
self,
@@ -58,14 +59,16 @@ def __init__(
alias: str | None = None,
format: PropertyFormat | None = None,
format_template: str | None = None,
+ discriminator: str | None = None,
) -> None:
self.alias = alias
self.format = format
self.format_template = format_template
+ self.discriminator = discriminator
@override
def __repr__(self) -> str:
- return f"{self.__class__.__name__}(alias='{self.alias}', format={self.format}, format_template='{self.format_template}')"
+ return f"{self.__class__.__name__}(alias='{self.alias}', format={self.format}, format_template='{self.format_template}', discriminator='{self.discriminator}')"
def maybe_transform(
diff --git a/src/maisa/_utils/_utils.py b/src/maisa/_utils/_utils.py
index 93c95517..17904ce6 100644
--- a/src/maisa/_utils/_utils.py
+++ b/src/maisa/_utils/_utils.py
@@ -265,6 +265,8 @@ def wrapper(*args: object, **kwargs: object) -> object:
)
msg = f"Missing required arguments; Expected either {variations} arguments to be given"
else:
+ assert len(variants) > 0
+
# TODO: this error message is not deterministic
missing = list(set(variants[0]) - given_params)
if len(missing) > 1:
@@ -389,3 +391,13 @@ def get_async_library() -> str:
return sniffio.current_async_library()
except Exception:
return "false"
+
+
+def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]:
+ """A version of functools.lru_cache that retains the type signature
+ for the wrapped function arguments.
+ """
+ wrapper = functools.lru_cache( # noqa: TID251
+ maxsize=maxsize,
+ )
+ return cast(Any, wrapper) # type: ignore[no-any-return]
diff --git a/src/maisa/_version.py b/src/maisa/_version.py
index 1fac4025..9fc7974d 100644
--- a/src/maisa/_version.py
+++ b/src/maisa/_version.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "maisa"
__version__ = "0.1.0-alpha.4" # x-release-please-version
diff --git a/src/maisa/resources/__init__.py b/src/maisa/resources/__init__.py
index cd1034e9..89db5456 100644
--- a/src/maisa/resources/__init__.py
+++ b/src/maisa/resources/__init__.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from .kpu import (
Kpu,
@@ -8,14 +8,6 @@
KpuWithStreamingResponse,
AsyncKpuWithStreamingResponse,
)
-from .mainet import (
- Mainet,
- AsyncMainet,
- MainetWithRawResponse,
- AsyncMainetWithRawResponse,
- MainetWithStreamingResponse,
- AsyncMainetWithStreamingResponse,
-)
from .models import (
Models,
AsyncModels,
@@ -66,10 +58,4 @@
"AsyncFileInterpreterWithRawResponse",
"FileInterpreterWithStreamingResponse",
"AsyncFileInterpreterWithStreamingResponse",
- "Mainet",
- "AsyncMainet",
- "MainetWithRawResponse",
- "AsyncMainetWithRawResponse",
- "MainetWithStreamingResponse",
- "AsyncMainetWithStreamingResponse",
]
diff --git a/src/maisa/resources/capabilities/__init__.py b/src/maisa/resources/capabilities/__init__.py
index 0d4acfa5..d2288b1c 100644
--- a/src/maisa/resources/capabilities/__init__.py
+++ b/src/maisa/resources/capabilities/__init__.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from .media import (
Media,
diff --git a/src/maisa/resources/capabilities/capabilities.py b/src/maisa/resources/capabilities/capabilities.py
index 84eb7f74..e8ce4551 100644
--- a/src/maisa/resources/capabilities/capabilities.py
+++ b/src/maisa/resources/capabilities/capabilities.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/resources/capabilities/media.py b/src/maisa/resources/capabilities/media.py
index 9a16c043..1dcd2f56 100644
--- a/src/maisa/resources/capabilities/media.py
+++ b/src/maisa/resources/capabilities/media.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
@@ -70,7 +70,7 @@ def compare(
This endpoint supports
an additional field `model` documented in this url:
- https://dash.readme.com/project/clibrain-platform-api/v1.0/docs/capabilities-with-media-via-json-config
+ https://docs.maisa.ai/docs/capabilities-with-media-via-json-config
Args:
lang: The language of the output. If not provided, the language used will be the same
@@ -174,8 +174,7 @@ def extract(
The text is analyzed and the variables are
extracted. This endpoint supports an additional field `model` documented in this
- url:
- https://dash.readme.com/project/clibrain-platform-api/v1.0/docs/capabilities-with-media-via-json-config
+ url: https://docs.maisa.ai/docs/capabilities-with-media-via-json-config
Args:
lang: The language of the output. If not provided, the language used will be the same
@@ -266,7 +265,7 @@ def summarize(
This endpoint supports an additional field `model`
documented in this url:
- https://dash.readme.com/project/clibrain-platform-api/v1.0/docs/capabilities-with-media-via-json-config
+ https://docs.maisa.ai/docs/capabilities-with-media-via-json-config
Args:
format: Text Summary Request Format.
@@ -351,7 +350,7 @@ async def compare(
This endpoint supports
an additional field `model` documented in this url:
- https://dash.readme.com/project/clibrain-platform-api/v1.0/docs/capabilities-with-media-via-json-config
+ https://docs.maisa.ai/docs/capabilities-with-media-via-json-config
Args:
lang: The language of the output. If not provided, the language used will be the same
@@ -455,8 +454,7 @@ async def extract(
The text is analyzed and the variables are
extracted. This endpoint supports an additional field `model` documented in this
- url:
- https://dash.readme.com/project/clibrain-platform-api/v1.0/docs/capabilities-with-media-via-json-config
+ url: https://docs.maisa.ai/docs/capabilities-with-media-via-json-config
Args:
lang: The language of the output. If not provided, the language used will be the same
@@ -547,7 +545,7 @@ async def summarize(
This endpoint supports an additional field `model`
documented in this url:
- https://dash.readme.com/project/clibrain-platform-api/v1.0/docs/capabilities-with-media-via-json-config
+ https://docs.maisa.ai/docs/capabilities-with-media-via-json-config
Args:
format: Text Summary Request Format.
diff --git a/src/maisa/resources/file_interpreter/__init__.py b/src/maisa/resources/file_interpreter/__init__.py
index 5c8085dc..59aecbec 100644
--- a/src/maisa/resources/file_interpreter/__init__.py
+++ b/src/maisa/resources/file_interpreter/__init__.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from .from_pdf import (
FromPdf,
@@ -33,12 +33,12 @@
AsyncFromAudioWithStreamingResponse,
)
from .from_image import (
- FromImageResource,
- AsyncFromImageResource,
- FromImageResourceWithRawResponse,
- AsyncFromImageResourceWithRawResponse,
- FromImageResourceWithStreamingResponse,
- AsyncFromImageResourceWithStreamingResponse,
+ FromImage,
+ AsyncFromImage,
+ FromImageWithRawResponse,
+ AsyncFromImageWithRawResponse,
+ FromImageWithStreamingResponse,
+ AsyncFromImageWithStreamingResponse,
)
from .file_interpreter import (
FileInterpreter,
@@ -48,6 +48,14 @@
FileInterpreterWithStreamingResponse,
AsyncFileInterpreterWithStreamingResponse,
)
+from .from_pdf_scanned import (
+ FromPdfScanned,
+ AsyncFromPdfScanned,
+ FromPdfScannedWithRawResponse,
+ AsyncFromPdfScannedWithRawResponse,
+ FromPdfScannedWithStreamingResponse,
+ AsyncFromPdfScannedWithStreamingResponse,
+)
__all__ = [
"FromPdf",
@@ -56,6 +64,12 @@
"AsyncFromPdfWithRawResponse",
"FromPdfWithStreamingResponse",
"AsyncFromPdfWithStreamingResponse",
+ "FromPdfScanned",
+ "AsyncFromPdfScanned",
+ "FromPdfScannedWithRawResponse",
+ "AsyncFromPdfScannedWithRawResponse",
+ "FromPdfScannedWithStreamingResponse",
+ "AsyncFromPdfScannedWithStreamingResponse",
"FromDocx",
"AsyncFromDocx",
"FromDocxWithRawResponse",
@@ -68,12 +82,12 @@
"AsyncFromHTMLWithRawResponse",
"FromHTMLWithStreamingResponse",
"AsyncFromHTMLWithStreamingResponse",
- "FromImageResource",
- "AsyncFromImageResource",
- "FromImageResourceWithRawResponse",
- "AsyncFromImageResourceWithRawResponse",
- "FromImageResourceWithStreamingResponse",
- "AsyncFromImageResourceWithStreamingResponse",
+ "FromImage",
+ "AsyncFromImage",
+ "FromImageWithRawResponse",
+ "AsyncFromImageWithRawResponse",
+ "FromImageWithStreamingResponse",
+ "AsyncFromImageWithStreamingResponse",
"FromAudio",
"AsyncFromAudio",
"FromAudioWithRawResponse",
diff --git a/src/maisa/resources/file_interpreter/file_interpreter.py b/src/maisa/resources/file_interpreter/file_interpreter.py
index 5eee030a..30b61e8f 100644
--- a/src/maisa/resources/file_interpreter/file_interpreter.py
+++ b/src/maisa/resources/file_interpreter/file_interpreter.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
@@ -36,14 +36,22 @@
AsyncFromAudioWithStreamingResponse,
)
from .from_image import (
- FromImageResource,
- AsyncFromImageResource,
- FromImageResourceWithRawResponse,
- AsyncFromImageResourceWithRawResponse,
- FromImageResourceWithStreamingResponse,
- AsyncFromImageResourceWithStreamingResponse,
+ FromImage,
+ AsyncFromImage,
+ FromImageWithRawResponse,
+ AsyncFromImageWithRawResponse,
+ FromImageWithStreamingResponse,
+ AsyncFromImageWithStreamingResponse,
)
from ..._resource import SyncAPIResource, AsyncAPIResource
+from .from_pdf_scanned import (
+ FromPdfScanned,
+ AsyncFromPdfScanned,
+ FromPdfScannedWithRawResponse,
+ AsyncFromPdfScannedWithRawResponse,
+ FromPdfScannedWithStreamingResponse,
+ AsyncFromPdfScannedWithStreamingResponse,
+)
__all__ = ["FileInterpreter", "AsyncFileInterpreter"]
@@ -53,6 +61,10 @@ class FileInterpreter(SyncAPIResource):
def from_pdf(self) -> FromPdf:
return FromPdf(self._client)
+ @cached_property
+ def from_pdf_scanned(self) -> FromPdfScanned:
+ return FromPdfScanned(self._client)
+
@cached_property
def from_docx(self) -> FromDocx:
return FromDocx(self._client)
@@ -62,8 +74,8 @@ def from_html(self) -> FromHTML:
return FromHTML(self._client)
@cached_property
- def from_image(self) -> FromImageResource:
- return FromImageResource(self._client)
+ def from_image(self) -> FromImage:
+ return FromImage(self._client)
@cached_property
def from_audio(self) -> FromAudio:
@@ -83,6 +95,10 @@ class AsyncFileInterpreter(AsyncAPIResource):
def from_pdf(self) -> AsyncFromPdf:
return AsyncFromPdf(self._client)
+ @cached_property
+ def from_pdf_scanned(self) -> AsyncFromPdfScanned:
+ return AsyncFromPdfScanned(self._client)
+
@cached_property
def from_docx(self) -> AsyncFromDocx:
return AsyncFromDocx(self._client)
@@ -92,8 +108,8 @@ def from_html(self) -> AsyncFromHTML:
return AsyncFromHTML(self._client)
@cached_property
- def from_image(self) -> AsyncFromImageResource:
- return AsyncFromImageResource(self._client)
+ def from_image(self) -> AsyncFromImage:
+ return AsyncFromImage(self._client)
@cached_property
def from_audio(self) -> AsyncFromAudio:
@@ -116,6 +132,10 @@ def __init__(self, file_interpreter: FileInterpreter) -> None:
def from_pdf(self) -> FromPdfWithRawResponse:
return FromPdfWithRawResponse(self._file_interpreter.from_pdf)
+ @cached_property
+ def from_pdf_scanned(self) -> FromPdfScannedWithRawResponse:
+ return FromPdfScannedWithRawResponse(self._file_interpreter.from_pdf_scanned)
+
@cached_property
def from_docx(self) -> FromDocxWithRawResponse:
return FromDocxWithRawResponse(self._file_interpreter.from_docx)
@@ -125,8 +145,8 @@ def from_html(self) -> FromHTMLWithRawResponse:
return FromHTMLWithRawResponse(self._file_interpreter.from_html)
@cached_property
- def from_image(self) -> FromImageResourceWithRawResponse:
- return FromImageResourceWithRawResponse(self._file_interpreter.from_image)
+ def from_image(self) -> FromImageWithRawResponse:
+ return FromImageWithRawResponse(self._file_interpreter.from_image)
@cached_property
def from_audio(self) -> FromAudioWithRawResponse:
@@ -141,6 +161,10 @@ def __init__(self, file_interpreter: AsyncFileInterpreter) -> None:
def from_pdf(self) -> AsyncFromPdfWithRawResponse:
return AsyncFromPdfWithRawResponse(self._file_interpreter.from_pdf)
+ @cached_property
+ def from_pdf_scanned(self) -> AsyncFromPdfScannedWithRawResponse:
+ return AsyncFromPdfScannedWithRawResponse(self._file_interpreter.from_pdf_scanned)
+
@cached_property
def from_docx(self) -> AsyncFromDocxWithRawResponse:
return AsyncFromDocxWithRawResponse(self._file_interpreter.from_docx)
@@ -150,8 +174,8 @@ def from_html(self) -> AsyncFromHTMLWithRawResponse:
return AsyncFromHTMLWithRawResponse(self._file_interpreter.from_html)
@cached_property
- def from_image(self) -> AsyncFromImageResourceWithRawResponse:
- return AsyncFromImageResourceWithRawResponse(self._file_interpreter.from_image)
+ def from_image(self) -> AsyncFromImageWithRawResponse:
+ return AsyncFromImageWithRawResponse(self._file_interpreter.from_image)
@cached_property
def from_audio(self) -> AsyncFromAudioWithRawResponse:
@@ -166,6 +190,10 @@ def __init__(self, file_interpreter: FileInterpreter) -> None:
def from_pdf(self) -> FromPdfWithStreamingResponse:
return FromPdfWithStreamingResponse(self._file_interpreter.from_pdf)
+ @cached_property
+ def from_pdf_scanned(self) -> FromPdfScannedWithStreamingResponse:
+ return FromPdfScannedWithStreamingResponse(self._file_interpreter.from_pdf_scanned)
+
@cached_property
def from_docx(self) -> FromDocxWithStreamingResponse:
return FromDocxWithStreamingResponse(self._file_interpreter.from_docx)
@@ -175,8 +203,8 @@ def from_html(self) -> FromHTMLWithStreamingResponse:
return FromHTMLWithStreamingResponse(self._file_interpreter.from_html)
@cached_property
- def from_image(self) -> FromImageResourceWithStreamingResponse:
- return FromImageResourceWithStreamingResponse(self._file_interpreter.from_image)
+ def from_image(self) -> FromImageWithStreamingResponse:
+ return FromImageWithStreamingResponse(self._file_interpreter.from_image)
@cached_property
def from_audio(self) -> FromAudioWithStreamingResponse:
@@ -191,6 +219,10 @@ def __init__(self, file_interpreter: AsyncFileInterpreter) -> None:
def from_pdf(self) -> AsyncFromPdfWithStreamingResponse:
return AsyncFromPdfWithStreamingResponse(self._file_interpreter.from_pdf)
+ @cached_property
+ def from_pdf_scanned(self) -> AsyncFromPdfScannedWithStreamingResponse:
+ return AsyncFromPdfScannedWithStreamingResponse(self._file_interpreter.from_pdf_scanned)
+
@cached_property
def from_docx(self) -> AsyncFromDocxWithStreamingResponse:
return AsyncFromDocxWithStreamingResponse(self._file_interpreter.from_docx)
@@ -200,8 +232,8 @@ def from_html(self) -> AsyncFromHTMLWithStreamingResponse:
return AsyncFromHTMLWithStreamingResponse(self._file_interpreter.from_html)
@cached_property
- def from_image(self) -> AsyncFromImageResourceWithStreamingResponse:
- return AsyncFromImageResourceWithStreamingResponse(self._file_interpreter.from_image)
+ def from_image(self) -> AsyncFromImageWithStreamingResponse:
+ return AsyncFromImageWithStreamingResponse(self._file_interpreter.from_image)
@cached_property
def from_audio(self) -> AsyncFromAudioWithStreamingResponse:
diff --git a/src/maisa/resources/file_interpreter/from_audio.py b/src/maisa/resources/file_interpreter/from_audio.py
index 9b93ef88..f73df7e8 100644
--- a/src/maisa/resources/file_interpreter/from_audio.py
+++ b/src/maisa/resources/file_interpreter/from_audio.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/resources/file_interpreter/from_docx.py b/src/maisa/resources/file_interpreter/from_docx.py
index a2dd8103..016a0a8e 100644
--- a/src/maisa/resources/file_interpreter/from_docx.py
+++ b/src/maisa/resources/file_interpreter/from_docx.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/resources/file_interpreter/from_html.py b/src/maisa/resources/file_interpreter/from_html.py
index c01b2522..22afd81e 100644
--- a/src/maisa/resources/file_interpreter/from_html.py
+++ b/src/maisa/resources/file_interpreter/from_html.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/resources/file_interpreter/from_image.py b/src/maisa/resources/file_interpreter/from_image.py
index b640ee22..842ee12b 100644
--- a/src/maisa/resources/file_interpreter/from_image.py
+++ b/src/maisa/resources/file_interpreter/from_image.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
@@ -24,19 +24,19 @@
from ..._base_client import (
make_request_options,
)
-from ...types.file_interpreter import FromImage, from_image_create_params
+from ...types.file_interpreter import from_image_create_params
-__all__ = ["FromImageResource", "AsyncFromImageResource"]
+__all__ = ["FromImage", "AsyncFromImage"]
-class FromImageResource(SyncAPIResource):
+class FromImage(SyncAPIResource):
@cached_property
- def with_raw_response(self) -> FromImageResourceWithRawResponse:
- return FromImageResourceWithRawResponse(self)
+ def with_raw_response(self) -> FromImageWithRawResponse:
+ return FromImageWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> FromImageResourceWithStreamingResponse:
- return FromImageResourceWithStreamingResponse(self)
+ def with_streaming_response(self) -> FromImageWithStreamingResponse:
+ return FromImageWithStreamingResponse(self)
def create(
self,
@@ -48,7 +48,7 @@ def create(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- ) -> FromImage:
+ ) -> object:
"""
Interprets an image file and returns a description of the image.
@@ -75,18 +75,18 @@ def create(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=FromImage,
+ cast_to=object,
)
-class AsyncFromImageResource(AsyncAPIResource):
+class AsyncFromImage(AsyncAPIResource):
@cached_property
- def with_raw_response(self) -> AsyncFromImageResourceWithRawResponse:
- return AsyncFromImageResourceWithRawResponse(self)
+ def with_raw_response(self) -> AsyncFromImageWithRawResponse:
+ return AsyncFromImageWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> AsyncFromImageResourceWithStreamingResponse:
- return AsyncFromImageResourceWithStreamingResponse(self)
+ def with_streaming_response(self) -> AsyncFromImageWithStreamingResponse:
+ return AsyncFromImageWithStreamingResponse(self)
async def create(
self,
@@ -98,7 +98,7 @@ async def create(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- ) -> FromImage:
+ ) -> object:
"""
Interprets an image file and returns a description of the image.
@@ -125,12 +125,12 @@ async def create(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=FromImage,
+ cast_to=object,
)
-class FromImageResourceWithRawResponse:
- def __init__(self, from_image: FromImageResource) -> None:
+class FromImageWithRawResponse:
+ def __init__(self, from_image: FromImage) -> None:
self._from_image = from_image
self.create = to_raw_response_wrapper(
@@ -138,8 +138,8 @@ def __init__(self, from_image: FromImageResource) -> None:
)
-class AsyncFromImageResourceWithRawResponse:
- def __init__(self, from_image: AsyncFromImageResource) -> None:
+class AsyncFromImageWithRawResponse:
+ def __init__(self, from_image: AsyncFromImage) -> None:
self._from_image = from_image
self.create = async_to_raw_response_wrapper(
@@ -147,8 +147,8 @@ def __init__(self, from_image: AsyncFromImageResource) -> None:
)
-class FromImageResourceWithStreamingResponse:
- def __init__(self, from_image: FromImageResource) -> None:
+class FromImageWithStreamingResponse:
+ def __init__(self, from_image: FromImage) -> None:
self._from_image = from_image
self.create = to_streamed_response_wrapper(
@@ -156,8 +156,8 @@ def __init__(self, from_image: FromImageResource) -> None:
)
-class AsyncFromImageResourceWithStreamingResponse:
- def __init__(self, from_image: AsyncFromImageResource) -> None:
+class AsyncFromImageWithStreamingResponse:
+ def __init__(self, from_image: AsyncFromImage) -> None:
self._from_image = from_image
self.create = async_to_streamed_response_wrapper(
diff --git a/src/maisa/resources/file_interpreter/from_pdf.py b/src/maisa/resources/file_interpreter/from_pdf.py
index 6a050e64..03a9ad1c 100644
--- a/src/maisa/resources/file_interpreter/from_pdf.py
+++ b/src/maisa/resources/file_interpreter/from_pdf.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/resources/file_interpreter/from_pdf_scanned.py b/src/maisa/resources/file_interpreter/from_pdf_scanned.py
new file mode 100644
index 00000000..db292da9
--- /dev/null
+++ b/src/maisa/resources/file_interpreter/from_pdf_scanned.py
@@ -0,0 +1,288 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Mapping, cast
+from typing_extensions import Literal
+
+import httpx
+
+from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
+from ..._utils import (
+ extract_files,
+ maybe_transform,
+ deepcopy_minimal,
+ async_maybe_transform,
+)
+from ..._compat import cached_property
+from ..._resource import SyncAPIResource, AsyncAPIResource
+from ..._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ..._base_client import (
+ make_request_options,
+)
+from ...types.file_interpreter import from_pdf_scanned_create_params
+
+__all__ = ["FromPdfScanned", "AsyncFromPdfScanned"]
+
+
+class FromPdfScanned(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> FromPdfScannedWithRawResponse:
+ return FromPdfScannedWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> FromPdfScannedWithStreamingResponse:
+ return FromPdfScannedWithStreamingResponse(self)
+
+ def create(
+ self,
+ *,
+ file: FileTypes,
+ lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
+ max_pages: int | NotGiven = NOT_GIVEN,
+ variable1_description: str | NotGiven = NOT_GIVEN,
+ variable1_name: str | NotGiven = NOT_GIVEN,
+ variable1_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
+ variable2_description: str | NotGiven = NOT_GIVEN,
+ variable2_name: str | NotGiven = NOT_GIVEN,
+ variable2_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
+ variable3_description: str | NotGiven = NOT_GIVEN,
+ variable3_name: str | NotGiven = NOT_GIVEN,
+ variable3_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
+ variable4_description: str | NotGiven = NOT_GIVEN,
+ variable4_name: str | NotGiven = NOT_GIVEN,
+ variable4_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ ) -> object:
+ """
+ Interprets pdf file and returns some extracted variables.
+
+ Args:
+ lang: The language of the output. If not provided, the language used will be the same
+ as the language of the text provided.
+
+ max_pages: The maximum number of pages to be extracted.
+
+ variable1_description: The description of the variable.
+
+ variable1_name: The name of the variable to be extracted.
+
+ variable1_type: Text Extraction Request Variable Type.
+
+ variable2_description: The description of the variable.
+
+ variable2_name: The name of the variable to be extracted.
+
+ variable2_type: Text Extraction Request Variable Type.
+
+ variable3_description: The description of the variable.
+
+ variable3_name: The name of the variable to be extracted.
+
+ variable3_type: Text Extraction Request Variable Type.
+
+ variable4_description: The description of the variable.
+
+ variable4_name: The name of the variable to be extracted.
+
+ variable4_type: Text Extraction Request Variable Type.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ body = deepcopy_minimal(
+ {
+ "file": file,
+ "lang": lang,
+ "max_pages": max_pages,
+ "variable1_description": variable1_description,
+ "variable1_name": variable1_name,
+ "variable1_type": variable1_type,
+ "variable2_description": variable2_description,
+ "variable2_name": variable2_name,
+ "variable2_type": variable2_type,
+ "variable3_description": variable3_description,
+ "variable3_name": variable3_name,
+ "variable3_type": variable3_type,
+ "variable4_description": variable4_description,
+ "variable4_name": variable4_name,
+ "variable4_type": variable4_type,
+ }
+ )
+ files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
+ if files:
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ return self._post(
+ "/v1/file-interpreter/from-pdf-scanned",
+ body=maybe_transform(body, from_pdf_scanned_create_params.FromPdfScannedCreateParams),
+ files=files,
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=object,
+ )
+
+
+class AsyncFromPdfScanned(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncFromPdfScannedWithRawResponse:
+ return AsyncFromPdfScannedWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncFromPdfScannedWithStreamingResponse:
+ return AsyncFromPdfScannedWithStreamingResponse(self)
+
+ async def create(
+ self,
+ *,
+ file: FileTypes,
+ lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
+ max_pages: int | NotGiven = NOT_GIVEN,
+ variable1_description: str | NotGiven = NOT_GIVEN,
+ variable1_name: str | NotGiven = NOT_GIVEN,
+ variable1_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
+ variable2_description: str | NotGiven = NOT_GIVEN,
+ variable2_name: str | NotGiven = NOT_GIVEN,
+ variable2_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
+ variable3_description: str | NotGiven = NOT_GIVEN,
+ variable3_name: str | NotGiven = NOT_GIVEN,
+ variable3_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
+ variable4_description: str | NotGiven = NOT_GIVEN,
+ variable4_name: str | NotGiven = NOT_GIVEN,
+ variable4_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ ) -> object:
+ """
+ Interprets pdf file and returns some extracted variables.
+
+ Args:
+ lang: The language of the output. If not provided, the language used will be the same
+ as the language of the text provided.
+
+ max_pages: The maximum number of pages to be extracted.
+
+ variable1_description: The description of the variable.
+
+ variable1_name: The name of the variable to be extracted.
+
+ variable1_type: Text Extraction Request Variable Type.
+
+ variable2_description: The description of the variable.
+
+ variable2_name: The name of the variable to be extracted.
+
+ variable2_type: Text Extraction Request Variable Type.
+
+ variable3_description: The description of the variable.
+
+ variable3_name: The name of the variable to be extracted.
+
+ variable3_type: Text Extraction Request Variable Type.
+
+ variable4_description: The description of the variable.
+
+ variable4_name: The name of the variable to be extracted.
+
+ variable4_type: Text Extraction Request Variable Type.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ body = deepcopy_minimal(
+ {
+ "file": file,
+ "lang": lang,
+ "max_pages": max_pages,
+ "variable1_description": variable1_description,
+ "variable1_name": variable1_name,
+ "variable1_type": variable1_type,
+ "variable2_description": variable2_description,
+ "variable2_name": variable2_name,
+ "variable2_type": variable2_type,
+ "variable3_description": variable3_description,
+ "variable3_name": variable3_name,
+ "variable3_type": variable3_type,
+ "variable4_description": variable4_description,
+ "variable4_name": variable4_name,
+ "variable4_type": variable4_type,
+ }
+ )
+ files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
+ if files:
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ return await self._post(
+ "/v1/file-interpreter/from-pdf-scanned",
+ body=await async_maybe_transform(body, from_pdf_scanned_create_params.FromPdfScannedCreateParams),
+ files=files,
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=object,
+ )
+
+
+class FromPdfScannedWithRawResponse:
+ def __init__(self, from_pdf_scanned: FromPdfScanned) -> None:
+ self._from_pdf_scanned = from_pdf_scanned
+
+ self.create = to_raw_response_wrapper(
+ from_pdf_scanned.create,
+ )
+
+
+class AsyncFromPdfScannedWithRawResponse:
+ def __init__(self, from_pdf_scanned: AsyncFromPdfScanned) -> None:
+ self._from_pdf_scanned = from_pdf_scanned
+
+ self.create = async_to_raw_response_wrapper(
+ from_pdf_scanned.create,
+ )
+
+
+class FromPdfScannedWithStreamingResponse:
+ def __init__(self, from_pdf_scanned: FromPdfScanned) -> None:
+ self._from_pdf_scanned = from_pdf_scanned
+
+ self.create = to_streamed_response_wrapper(
+ from_pdf_scanned.create,
+ )
+
+
+class AsyncFromPdfScannedWithStreamingResponse:
+ def __init__(self, from_pdf_scanned: AsyncFromPdfScanned) -> None:
+ self._from_pdf_scanned = from_pdf_scanned
+
+ self.create = async_to_streamed_response_wrapper(
+ from_pdf_scanned.create,
+ )
diff --git a/src/maisa/resources/kpu.py b/src/maisa/resources/kpu.py
index 3743abfc..eaee89a9 100644
--- a/src/maisa/resources/kpu.py
+++ b/src/maisa/resources/kpu.py
@@ -1,12 +1,13 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
-from typing import List, Mapping, cast
+from typing import List, Mapping, Optional, cast
+from typing_extensions import Literal
import httpx
-from ..types import KpuRunResponse, kpu_run_params
+from ..types import kpu_run_params
from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
from .._utils import (
extract_files,
@@ -45,15 +46,31 @@ def run(
explain_steps: bool | NotGiven = NOT_GIVEN,
retries: int | NotGiven = NOT_GIVEN,
file: List[FileTypes] | NotGiven = NOT_GIVEN,
+ reasoner_model: Optional[
+ Literal[
+ "gpt-4-turbo",
+ "mistral-large",
+ "gpt-3.5-turbo",
+ "claude-3-sonnet",
+ "claude-3-opus",
+ "gemini-pro",
+ "azure/gpt-4-turbo",
+ "openai/gpt-4-turbo",
+ ]
+ ]
+ | NotGiven = NOT_GIVEN,
+ reasoner_prompt: Optional[str] | NotGiven = NOT_GIVEN,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- ) -> KpuRunResponse:
+ ) -> object:
"""
Executes the KPU in sync, sending the response when the KPU execution is done.
+ The KPU beta is currently available for selected users. Submit your request to
+ be granted access: https://maisa.ai
Args:
query: User text with the query or request to be commanded to the KPU.
@@ -79,6 +96,8 @@ def run(
{
"query": query,
"file": file,
+ "reasoner_model": reasoner_model,
+ "reasoner_prompt": reasoner_prompt,
}
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file", ""]])
@@ -104,7 +123,7 @@ def run(
kpu_run_params.KpuRunParams,
),
),
- cast_to=KpuRunResponse,
+ cast_to=object,
)
@@ -124,15 +143,31 @@ async def run(
explain_steps: bool | NotGiven = NOT_GIVEN,
retries: int | NotGiven = NOT_GIVEN,
file: List[FileTypes] | NotGiven = NOT_GIVEN,
+ reasoner_model: Optional[
+ Literal[
+ "gpt-4-turbo",
+ "mistral-large",
+ "gpt-3.5-turbo",
+ "claude-3-sonnet",
+ "claude-3-opus",
+ "gemini-pro",
+ "azure/gpt-4-turbo",
+ "openai/gpt-4-turbo",
+ ]
+ ]
+ | NotGiven = NOT_GIVEN,
+ reasoner_prompt: Optional[str] | NotGiven = NOT_GIVEN,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- ) -> KpuRunResponse:
+ ) -> object:
"""
Executes the KPU in sync, sending the response when the KPU execution is done.
+ The KPU beta is currently available for selected users. Submit your request to
+ be granted access: https://maisa.ai
Args:
query: User text with the query or request to be commanded to the KPU.
@@ -158,6 +193,8 @@ async def run(
{
"query": query,
"file": file,
+ "reasoner_model": reasoner_model,
+ "reasoner_prompt": reasoner_prompt,
}
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file", ""]])
@@ -183,7 +220,7 @@ async def run(
kpu_run_params.KpuRunParams,
),
),
- cast_to=KpuRunResponse,
+ cast_to=object,
)
diff --git a/src/maisa/resources/mainet/__init__.py b/src/maisa/resources/mainet/__init__.py
deleted file mode 100644
index c799096e..00000000
--- a/src/maisa/resources/mainet/__init__.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# File generated from our OpenAPI spec by Stainless.
-
-from .mainet import (
- Mainet,
- AsyncMainet,
- MainetWithRawResponse,
- AsyncMainetWithRawResponse,
- MainetWithStreamingResponse,
- AsyncMainetWithStreamingResponse,
-)
-from .search import (
- SearchResource,
- AsyncSearchResource,
- SearchResourceWithRawResponse,
- AsyncSearchResourceWithRawResponse,
- SearchResourceWithStreamingResponse,
- AsyncSearchResourceWithStreamingResponse,
-)
-
-__all__ = [
- "SearchResource",
- "AsyncSearchResource",
- "SearchResourceWithRawResponse",
- "AsyncSearchResourceWithRawResponse",
- "SearchResourceWithStreamingResponse",
- "AsyncSearchResourceWithStreamingResponse",
- "Mainet",
- "AsyncMainet",
- "MainetWithRawResponse",
- "AsyncMainetWithRawResponse",
- "MainetWithStreamingResponse",
- "AsyncMainetWithStreamingResponse",
-]
diff --git a/src/maisa/resources/mainet/mainet.py b/src/maisa/resources/mainet/mainet.py
deleted file mode 100644
index 61c2c480..00000000
--- a/src/maisa/resources/mainet/mainet.py
+++ /dev/null
@@ -1,80 +0,0 @@
-# File generated from our OpenAPI spec by Stainless.
-
-from __future__ import annotations
-
-from .search import (
- SearchResource,
- AsyncSearchResource,
- SearchResourceWithRawResponse,
- AsyncSearchResourceWithRawResponse,
- SearchResourceWithStreamingResponse,
- AsyncSearchResourceWithStreamingResponse,
-)
-from ..._compat import cached_property
-from ..._resource import SyncAPIResource, AsyncAPIResource
-
-__all__ = ["Mainet", "AsyncMainet"]
-
-
-class Mainet(SyncAPIResource):
- @cached_property
- def search(self) -> SearchResource:
- return SearchResource(self._client)
-
- @cached_property
- def with_raw_response(self) -> MainetWithRawResponse:
- return MainetWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> MainetWithStreamingResponse:
- return MainetWithStreamingResponse(self)
-
-
-class AsyncMainet(AsyncAPIResource):
- @cached_property
- def search(self) -> AsyncSearchResource:
- return AsyncSearchResource(self._client)
-
- @cached_property
- def with_raw_response(self) -> AsyncMainetWithRawResponse:
- return AsyncMainetWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> AsyncMainetWithStreamingResponse:
- return AsyncMainetWithStreamingResponse(self)
-
-
-class MainetWithRawResponse:
- def __init__(self, mainet: Mainet) -> None:
- self._mainet = mainet
-
- @cached_property
- def search(self) -> SearchResourceWithRawResponse:
- return SearchResourceWithRawResponse(self._mainet.search)
-
-
-class AsyncMainetWithRawResponse:
- def __init__(self, mainet: AsyncMainet) -> None:
- self._mainet = mainet
-
- @cached_property
- def search(self) -> AsyncSearchResourceWithRawResponse:
- return AsyncSearchResourceWithRawResponse(self._mainet.search)
-
-
-class MainetWithStreamingResponse:
- def __init__(self, mainet: Mainet) -> None:
- self._mainet = mainet
-
- @cached_property
- def search(self) -> SearchResourceWithStreamingResponse:
- return SearchResourceWithStreamingResponse(self._mainet.search)
-
-
-class AsyncMainetWithStreamingResponse:
- def __init__(self, mainet: AsyncMainet) -> None:
- self._mainet = mainet
-
- @cached_property
- def search(self) -> AsyncSearchResourceWithStreamingResponse:
- return AsyncSearchResourceWithStreamingResponse(self._mainet.search)
diff --git a/src/maisa/resources/mainet/search.py b/src/maisa/resources/mainet/search.py
deleted file mode 100644
index 6b376d54..00000000
--- a/src/maisa/resources/mainet/search.py
+++ /dev/null
@@ -1,149 +0,0 @@
-# File generated from our OpenAPI spec by Stainless.
-
-from __future__ import annotations
-
-import httpx
-
-from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from ..._utils import (
- maybe_transform,
- async_maybe_transform,
-)
-from ..._compat import cached_property
-from ..._resource import SyncAPIResource, AsyncAPIResource
-from ..._response import (
- to_raw_response_wrapper,
- to_streamed_response_wrapper,
- async_to_raw_response_wrapper,
- async_to_streamed_response_wrapper,
-)
-from ..._base_client import (
- make_request_options,
-)
-from ...types.mainet import Search, search_create_params
-
-__all__ = ["SearchResource", "AsyncSearchResource"]
-
-
-class SearchResource(SyncAPIResource):
- @cached_property
- def with_raw_response(self) -> SearchResourceWithRawResponse:
- return SearchResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> SearchResourceWithStreamingResponse:
- return SearchResourceWithStreamingResponse(self)
-
- def create(
- self,
- *,
- text: str,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- ) -> Search:
- """
- Finds content in mainet network
-
- Args:
- text: The text or query to search for.
-
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- return self._post(
- "/v1/mainet/search",
- body=maybe_transform({"text": text}, search_create_params.SearchCreateParams),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=Search,
- )
-
-
-class AsyncSearchResource(AsyncAPIResource):
- @cached_property
- def with_raw_response(self) -> AsyncSearchResourceWithRawResponse:
- return AsyncSearchResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> AsyncSearchResourceWithStreamingResponse:
- return AsyncSearchResourceWithStreamingResponse(self)
-
- async def create(
- self,
- *,
- text: str,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- ) -> Search:
- """
- Finds content in mainet network
-
- Args:
- text: The text or query to search for.
-
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- return await self._post(
- "/v1/mainet/search",
- body=await async_maybe_transform({"text": text}, search_create_params.SearchCreateParams),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=Search,
- )
-
-
-class SearchResourceWithRawResponse:
- def __init__(self, search: SearchResource) -> None:
- self._search = search
-
- self.create = to_raw_response_wrapper(
- search.create,
- )
-
-
-class AsyncSearchResourceWithRawResponse:
- def __init__(self, search: AsyncSearchResource) -> None:
- self._search = search
-
- self.create = async_to_raw_response_wrapper(
- search.create,
- )
-
-
-class SearchResourceWithStreamingResponse:
- def __init__(self, search: SearchResource) -> None:
- self._search = search
-
- self.create = to_streamed_response_wrapper(
- search.create,
- )
-
-
-class AsyncSearchResourceWithStreamingResponse:
- def __init__(self, search: AsyncSearchResource) -> None:
- self._search = search
-
- self.create = async_to_streamed_response_wrapper(
- search.create,
- )
diff --git a/src/maisa/resources/models/__init__.py b/src/maisa/resources/models/__init__.py
index 4155408a..7b09253c 100644
--- a/src/maisa/resources/models/__init__.py
+++ b/src/maisa/resources/models/__init__.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from .models import (
Models,
@@ -8,14 +8,6 @@
ModelsWithStreamingResponse,
AsyncModelsWithStreamingResponse,
)
-from .rerank import (
- RerankResource,
- AsyncRerankResource,
- RerankResourceWithRawResponse,
- AsyncRerankResourceWithRawResponse,
- RerankResourceWithStreamingResponse,
- AsyncRerankResourceWithStreamingResponse,
-)
from .embeddings import (
Embeddings,
AsyncEmbeddings,
@@ -32,12 +24,6 @@
"AsyncEmbeddingsWithRawResponse",
"EmbeddingsWithStreamingResponse",
"AsyncEmbeddingsWithStreamingResponse",
- "RerankResource",
- "AsyncRerankResource",
- "RerankResourceWithRawResponse",
- "AsyncRerankResourceWithRawResponse",
- "RerankResourceWithStreamingResponse",
- "AsyncRerankResourceWithStreamingResponse",
"Models",
"AsyncModels",
"ModelsWithRawResponse",
diff --git a/src/maisa/resources/models/embeddings.py b/src/maisa/resources/models/embeddings.py
index eb3dfced..82cfffcc 100644
--- a/src/maisa/resources/models/embeddings.py
+++ b/src/maisa/resources/models/embeddings.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/resources/models/models.py b/src/maisa/resources/models/models.py
index f852a6b9..b67597bd 100644
--- a/src/maisa/resources/models/models.py
+++ b/src/maisa/resources/models/models.py
@@ -1,15 +1,7 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
-from .rerank import (
- RerankResource,
- AsyncRerankResource,
- RerankResourceWithRawResponse,
- AsyncRerankResourceWithRawResponse,
- RerankResourceWithStreamingResponse,
- AsyncRerankResourceWithStreamingResponse,
-)
from ..._compat import cached_property
from .embeddings import (
Embeddings,
@@ -29,10 +21,6 @@ class Models(SyncAPIResource):
def embeddings(self) -> Embeddings:
return Embeddings(self._client)
- @cached_property
- def rerank(self) -> RerankResource:
- return RerankResource(self._client)
-
@cached_property
def with_raw_response(self) -> ModelsWithRawResponse:
return ModelsWithRawResponse(self)
@@ -47,10 +35,6 @@ class AsyncModels(AsyncAPIResource):
def embeddings(self) -> AsyncEmbeddings:
return AsyncEmbeddings(self._client)
- @cached_property
- def rerank(self) -> AsyncRerankResource:
- return AsyncRerankResource(self._client)
-
@cached_property
def with_raw_response(self) -> AsyncModelsWithRawResponse:
return AsyncModelsWithRawResponse(self)
@@ -68,10 +52,6 @@ def __init__(self, models: Models) -> None:
def embeddings(self) -> EmbeddingsWithRawResponse:
return EmbeddingsWithRawResponse(self._models.embeddings)
- @cached_property
- def rerank(self) -> RerankResourceWithRawResponse:
- return RerankResourceWithRawResponse(self._models.rerank)
-
class AsyncModelsWithRawResponse:
def __init__(self, models: AsyncModels) -> None:
@@ -81,10 +61,6 @@ def __init__(self, models: AsyncModels) -> None:
def embeddings(self) -> AsyncEmbeddingsWithRawResponse:
return AsyncEmbeddingsWithRawResponse(self._models.embeddings)
- @cached_property
- def rerank(self) -> AsyncRerankResourceWithRawResponse:
- return AsyncRerankResourceWithRawResponse(self._models.rerank)
-
class ModelsWithStreamingResponse:
def __init__(self, models: Models) -> None:
@@ -94,10 +70,6 @@ def __init__(self, models: Models) -> None:
def embeddings(self) -> EmbeddingsWithStreamingResponse:
return EmbeddingsWithStreamingResponse(self._models.embeddings)
- @cached_property
- def rerank(self) -> RerankResourceWithStreamingResponse:
- return RerankResourceWithStreamingResponse(self._models.rerank)
-
class AsyncModelsWithStreamingResponse:
def __init__(self, models: AsyncModels) -> None:
@@ -106,7 +78,3 @@ def __init__(self, models: AsyncModels) -> None:
@cached_property
def embeddings(self) -> AsyncEmbeddingsWithStreamingResponse:
return AsyncEmbeddingsWithStreamingResponse(self._models.embeddings)
-
- @cached_property
- def rerank(self) -> AsyncRerankResourceWithStreamingResponse:
- return AsyncRerankResourceWithStreamingResponse(self._models.rerank)
diff --git a/src/maisa/resources/models/rerank.py b/src/maisa/resources/models/rerank.py
deleted file mode 100644
index 5807a961..00000000
--- a/src/maisa/resources/models/rerank.py
+++ /dev/null
@@ -1,169 +0,0 @@
-# File generated from our OpenAPI spec by Stainless.
-
-from __future__ import annotations
-
-from typing import List
-
-import httpx
-
-from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from ..._utils import (
- maybe_transform,
- async_maybe_transform,
-)
-from ..._compat import cached_property
-from ..._resource import SyncAPIResource, AsyncAPIResource
-from ..._response import (
- to_raw_response_wrapper,
- to_streamed_response_wrapper,
- async_to_raw_response_wrapper,
- async_to_streamed_response_wrapper,
-)
-from ..._base_client import (
- make_request_options,
-)
-from ...types.models import Rerank, rerank_create_params
-
-__all__ = ["RerankResource", "AsyncRerankResource"]
-
-
-class RerankResource(SyncAPIResource):
- @cached_property
- def with_raw_response(self) -> RerankResourceWithRawResponse:
- return RerankResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> RerankResourceWithStreamingResponse:
- return RerankResourceWithStreamingResponse(self)
-
- def create(
- self,
- *,
- sentences: List[str],
- source_sentence: str,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- ) -> Rerank:
- """
- Rerank the sentences based on the similarity with the source sentence.
-
- Args:
- sentences: A list of sentences to be reranked.
-
- source_sentence: The sentence to be used as a reference to rerank the sentences.
-
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- return self._post(
- "/v1/models/rerank",
- body=maybe_transform(
- {
- "sentences": sentences,
- "source_sentence": source_sentence,
- },
- rerank_create_params.RerankCreateParams,
- ),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=Rerank,
- )
-
-
-class AsyncRerankResource(AsyncAPIResource):
- @cached_property
- def with_raw_response(self) -> AsyncRerankResourceWithRawResponse:
- return AsyncRerankResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> AsyncRerankResourceWithStreamingResponse:
- return AsyncRerankResourceWithStreamingResponse(self)
-
- async def create(
- self,
- *,
- sentences: List[str],
- source_sentence: str,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- ) -> Rerank:
- """
- Rerank the sentences based on the similarity with the source sentence.
-
- Args:
- sentences: A list of sentences to be reranked.
-
- source_sentence: The sentence to be used as a reference to rerank the sentences.
-
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- return await self._post(
- "/v1/models/rerank",
- body=await async_maybe_transform(
- {
- "sentences": sentences,
- "source_sentence": source_sentence,
- },
- rerank_create_params.RerankCreateParams,
- ),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=Rerank,
- )
-
-
-class RerankResourceWithRawResponse:
- def __init__(self, rerank: RerankResource) -> None:
- self._rerank = rerank
-
- self.create = to_raw_response_wrapper(
- rerank.create,
- )
-
-
-class AsyncRerankResourceWithRawResponse:
- def __init__(self, rerank: AsyncRerankResource) -> None:
- self._rerank = rerank
-
- self.create = async_to_raw_response_wrapper(
- rerank.create,
- )
-
-
-class RerankResourceWithStreamingResponse:
- def __init__(self, rerank: RerankResource) -> None:
- self._rerank = rerank
-
- self.create = to_streamed_response_wrapper(
- rerank.create,
- )
-
-
-class AsyncRerankResourceWithStreamingResponse:
- def __init__(self, rerank: AsyncRerankResource) -> None:
- self._rerank = rerank
-
- self.create = async_to_streamed_response_wrapper(
- rerank.create,
- )
diff --git a/src/maisa/types/__init__.py b/src/maisa/types/__init__.py
index de43413c..60c5c116 100644
--- a/src/maisa/types/__init__.py
+++ b/src/maisa/types/__init__.py
@@ -1,10 +1,9 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
from .shared import TextSummary as TextSummary, TextExtractor as TextExtractor, TextComparator as TextComparator
from .kpu_run_params import KpuRunParams as KpuRunParams
-from .kpu_run_response import KpuRunResponse as KpuRunResponse
from .capability_compare_params import CapabilityCompareParams as CapabilityCompareParams
from .capability_extract_params import CapabilityExtractParams as CapabilityExtractParams
from .capability_summarize_params import CapabilitySummarizeParams as CapabilitySummarizeParams
diff --git a/src/maisa/types/capabilities/__init__.py b/src/maisa/types/capabilities/__init__.py
index 5ac72220..8cc61a87 100644
--- a/src/maisa/types/capabilities/__init__.py
+++ b/src/maisa/types/capabilities/__init__.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/types/capabilities/media_compare_params.py b/src/maisa/types/capabilities/media_compare_params.py
index 3f5a69b4..28131bb0 100644
--- a/src/maisa/types/capabilities/media_compare_params.py
+++ b/src/maisa/types/capabilities/media_compare_params.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/types/capabilities/media_extract_params.py b/src/maisa/types/capabilities/media_extract_params.py
index 210c29ba..0fb6772a 100644
--- a/src/maisa/types/capabilities/media_extract_params.py
+++ b/src/maisa/types/capabilities/media_extract_params.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/types/capabilities/media_summarize_params.py b/src/maisa/types/capabilities/media_summarize_params.py
index 0620b481..0176e4ab 100644
--- a/src/maisa/types/capabilities/media_summarize_params.py
+++ b/src/maisa/types/capabilities/media_summarize_params.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/types/capability_compare_params.py b/src/maisa/types/capability_compare_params.py
index 761d19ca..8bdc52ae 100644
--- a/src/maisa/types/capability_compare_params.py
+++ b/src/maisa/types/capability_compare_params.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/types/capability_extract_params.py b/src/maisa/types/capability_extract_params.py
index 82ab3b6a..2ac4f0f8 100644
--- a/src/maisa/types/capability_extract_params.py
+++ b/src/maisa/types/capability_extract_params.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/types/capability_summarize_params.py b/src/maisa/types/capability_summarize_params.py
index 3684c18d..0abea6d9 100644
--- a/src/maisa/types/capability_summarize_params.py
+++ b/src/maisa/types/capability_summarize_params.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/types/file_interpreter/__init__.py b/src/maisa/types/file_interpreter/__init__.py
index cc3d0cef..778c0a5f 100644
--- a/src/maisa/types/file_interpreter/__init__.py
+++ b/src/maisa/types/file_interpreter/__init__.py
@@ -1,10 +1,10 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
-from .from_image import FromImage as FromImage
from .from_pdf_create_params import FromPdfCreateParams as FromPdfCreateParams
from .from_docx_create_params import FromDocxCreateParams as FromDocxCreateParams
from .from_html_create_params import FromHTMLCreateParams as FromHTMLCreateParams
from .from_audio_create_params import FromAudioCreateParams as FromAudioCreateParams
from .from_image_create_params import FromImageCreateParams as FromImageCreateParams
+from .from_pdf_scanned_create_params import FromPdfScannedCreateParams as FromPdfScannedCreateParams
diff --git a/src/maisa/types/file_interpreter/from_audio_create_params.py b/src/maisa/types/file_interpreter/from_audio_create_params.py
index 506910bb..bbd89db7 100644
--- a/src/maisa/types/file_interpreter/from_audio_create_params.py
+++ b/src/maisa/types/file_interpreter/from_audio_create_params.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/types/file_interpreter/from_docx_create_params.py b/src/maisa/types/file_interpreter/from_docx_create_params.py
index 10786380..692ea715 100644
--- a/src/maisa/types/file_interpreter/from_docx_create_params.py
+++ b/src/maisa/types/file_interpreter/from_docx_create_params.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/types/file_interpreter/from_html_create_params.py b/src/maisa/types/file_interpreter/from_html_create_params.py
index 154b3478..dbe6db9e 100644
--- a/src/maisa/types/file_interpreter/from_html_create_params.py
+++ b/src/maisa/types/file_interpreter/from_html_create_params.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/types/file_interpreter/from_image.py b/src/maisa/types/file_interpreter/from_image.py
deleted file mode 100644
index 2fb2005e..00000000
--- a/src/maisa/types/file_interpreter/from_image.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# File generated from our OpenAPI spec by Stainless.
-
-from pydantic import Field as FieldInfo
-
-from ..._models import BaseModel
-
-__all__ = ["FromImage"]
-
-
-class FromImage(BaseModel):
- image_caption: str = FieldInfo(alias="imageCaption")
diff --git a/src/maisa/types/file_interpreter/from_image_create_params.py b/src/maisa/types/file_interpreter/from_image_create_params.py
index 48ff7db9..aac9be6c 100644
--- a/src/maisa/types/file_interpreter/from_image_create_params.py
+++ b/src/maisa/types/file_interpreter/from_image_create_params.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/types/file_interpreter/from_pdf_create_params.py b/src/maisa/types/file_interpreter/from_pdf_create_params.py
index 90331e75..40b06c9d 100644
--- a/src/maisa/types/file_interpreter/from_pdf_create_params.py
+++ b/src/maisa/types/file_interpreter/from_pdf_create_params.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/types/file_interpreter/from_pdf_scanned_create_params.py b/src/maisa/types/file_interpreter/from_pdf_scanned_create_params.py
new file mode 100644
index 00000000..2f64c3d2
--- /dev/null
+++ b/src/maisa/types/file_interpreter/from_pdf_scanned_create_params.py
@@ -0,0 +1,59 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Literal, Required, TypedDict
+
+from ..._types import FileTypes
+
+__all__ = ["FromPdfScannedCreateParams"]
+
+
+class FromPdfScannedCreateParams(TypedDict, total=False):
+ file: Required[FileTypes]
+
+ lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"]
+ """The language of the output.
+
+ If not provided, the language used will be the same as the language of the text
+ provided.
+ """
+
+ max_pages: int
+ """The maximum number of pages to be extracted."""
+
+ variable1_description: str
+ """The description of the variable."""
+
+ variable1_name: str
+ """The name of the variable to be extracted."""
+
+ variable1_type: Literal["string", "number", "date", "boolean"]
+ """Text Extraction Request Variable Type."""
+
+ variable2_description: str
+ """The description of the variable."""
+
+ variable2_name: str
+ """The name of the variable to be extracted."""
+
+ variable2_type: Literal["string", "number", "date", "boolean"]
+ """Text Extraction Request Variable Type."""
+
+ variable3_description: str
+ """The description of the variable."""
+
+ variable3_name: str
+ """The name of the variable to be extracted."""
+
+ variable3_type: Literal["string", "number", "date", "boolean"]
+ """Text Extraction Request Variable Type."""
+
+ variable4_description: str
+ """The description of the variable."""
+
+ variable4_name: str
+ """The name of the variable to be extracted."""
+
+ variable4_type: Literal["string", "number", "date", "boolean"]
+ """Text Extraction Request Variable Type."""
diff --git a/src/maisa/types/kpu_run_params.py b/src/maisa/types/kpu_run_params.py
index bad448ed..a1d36388 100644
--- a/src/maisa/types/kpu_run_params.py
+++ b/src/maisa/types/kpu_run_params.py
@@ -1,9 +1,9 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
-from typing import List
-from typing_extensions import Required, TypedDict
+from typing import List, Optional
+from typing_extensions import Literal, Required, TypedDict
from .._types import FileTypes
@@ -30,3 +30,18 @@ class KpuRunParams(TypedDict, total=False):
file: List[FileTypes]
"""Files to be used in the KPU execution. Files can be of any type."""
+
+ reasoner_model: Optional[
+ Literal[
+ "gpt-4-turbo",
+ "mistral-large",
+ "gpt-3.5-turbo",
+ "claude-3-sonnet",
+ "claude-3-opus",
+ "gemini-pro",
+ "azure/gpt-4-turbo",
+ "openai/gpt-4-turbo",
+ ]
+ ]
+
+ reasoner_prompt: Optional[str]
diff --git a/src/maisa/types/kpu_run_response.py b/src/maisa/types/kpu_run_response.py
deleted file mode 100644
index efacc7d6..00000000
--- a/src/maisa/types/kpu_run_response.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# File generated from our OpenAPI spec by Stainless.
-
-from typing import Dict, List, Optional
-
-from .._models import BaseModel
-
-__all__ = ["KpuRunResponse", "Intent"]
-
-
-class Intent(BaseModel):
- explain_steps: List[str]
- """Array of the steps of the intent explained in natural language.
-
- This field is an ampty array if `explain_steps` param is set to `false`
- """
-
- intent: int
- """Intent number, starting from 0."""
-
- result: str
- """The result of the intent."""
-
- solved: bool
- """Whether the intent was interpreted as solved by the KPU or not."""
-
- downloadable_files: Optional[Dict[str, str]] = None
- """Key-value of the files generated by the KPU."""
-
-
-class KpuRunResponse(BaseModel):
- intents: List[Intent]
- """Array of the intents executed by the KPU."""
-
- result: str
- """The result of the KPU execution.
-
- The result may be invalid if none of the intents were successful.
- """
-
- downloadable_files: Optional[Dict[str, str]] = None
- """Key-value of the files generated by the KPU."""
diff --git a/src/maisa/types/mainet/__init__.py b/src/maisa/types/mainet/__init__.py
index fa180571..f8ee8b14 100644
--- a/src/maisa/types/mainet/__init__.py
+++ b/src/maisa/types/mainet/__init__.py
@@ -1,6 +1,3 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
-
-from .search import Search as Search
-from .search_create_params import SearchCreateParams as SearchCreateParams
diff --git a/src/maisa/types/mainet/search.py b/src/maisa/types/mainet/search.py
deleted file mode 100644
index bdbf2577..00000000
--- a/src/maisa/types/mainet/search.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# File generated from our OpenAPI spec by Stainless.
-
-from typing import List
-
-from ..._models import BaseModel
-
-__all__ = ["Search", "Doc"]
-
-
-class Doc(BaseModel):
- id: str
- """The id of the document."""
-
- content: str
- """The content of the document."""
-
- source: str
- """The source of the document."""
-
-
-class Search(BaseModel):
- docs: List[Doc]
- """The list of documents found."""
diff --git a/src/maisa/types/mainet/search_create_params.py b/src/maisa/types/mainet/search_create_params.py
deleted file mode 100644
index 862c1da1..00000000
--- a/src/maisa/types/mainet/search_create_params.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# File generated from our OpenAPI spec by Stainless.
-
-from __future__ import annotations
-
-from typing_extensions import Required, TypedDict
-
-__all__ = ["SearchCreateParams"]
-
-
-class SearchCreateParams(TypedDict, total=False):
- text: Required[str]
- """The text or query to search for."""
diff --git a/src/maisa/types/models/__init__.py b/src/maisa/types/models/__init__.py
index e71abaa4..43ef90c4 100644
--- a/src/maisa/types/models/__init__.py
+++ b/src/maisa/types/models/__init__.py
@@ -1,8 +1,6 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
-from .rerank import Rerank as Rerank
from .embeddings import Embeddings as Embeddings
-from .rerank_create_params import RerankCreateParams as RerankCreateParams
from .embedding_create_params import EmbeddingCreateParams as EmbeddingCreateParams
diff --git a/src/maisa/types/models/embedding_create_params.py b/src/maisa/types/models/embedding_create_params.py
index ad2ae944..a09951d7 100644
--- a/src/maisa/types/models/embedding_create_params.py
+++ b/src/maisa/types/models/embedding_create_params.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/src/maisa/types/models/embeddings.py b/src/maisa/types/models/embeddings.py
index 9c563537..87de1ace 100644
--- a/src/maisa/types/models/embeddings.py
+++ b/src/maisa/types/models/embeddings.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from typing import List
diff --git a/src/maisa/types/models/rerank.py b/src/maisa/types/models/rerank.py
deleted file mode 100644
index a3a86e0f..00000000
--- a/src/maisa/types/models/rerank.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# File generated from our OpenAPI spec by Stainless.
-
-from typing import List
-
-from ..._models import BaseModel
-
-__all__ = ["Rerank"]
-
-
-class Rerank(BaseModel):
- sorted_sentences: List[str]
diff --git a/src/maisa/types/models/rerank_create_params.py b/src/maisa/types/models/rerank_create_params.py
deleted file mode 100644
index a96610ca..00000000
--- a/src/maisa/types/models/rerank_create_params.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# File generated from our OpenAPI spec by Stainless.
-
-from __future__ import annotations
-
-from typing import List
-from typing_extensions import Required, TypedDict
-
-__all__ = ["RerankCreateParams"]
-
-
-class RerankCreateParams(TypedDict, total=False):
- sentences: Required[List[str]]
- """A list of sentences to be reranked."""
-
- source_sentence: Required[str]
- """The sentence to be used as a reference to rerank the sentences."""
diff --git a/src/maisa/types/shared/__init__.py b/src/maisa/types/shared/__init__.py
index 35715a33..15a75b2a 100644
--- a/src/maisa/types/shared/__init__.py
+++ b/src/maisa/types/shared/__init__.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from .text_summary import TextSummary as TextSummary
from .text_extractor import TextExtractor as TextExtractor
diff --git a/src/maisa/types/shared/text_comparator.py b/src/maisa/types/shared/text_comparator.py
index 9177b363..1a052b1d 100644
--- a/src/maisa/types/shared/text_comparator.py
+++ b/src/maisa/types/shared/text_comparator.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/src/maisa/types/shared/text_extractor.py b/src/maisa/types/shared/text_extractor.py
index 0869f9fb..70d694a1 100644
--- a/src/maisa/types/shared/text_extractor.py
+++ b/src/maisa/types/shared/text_extractor.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/src/maisa/types/shared/text_summary.py b/src/maisa/types/shared/text_summary.py
index 74d0c3d1..91e2e375 100644
--- a/src/maisa/types/shared/text_summary.py
+++ b/src/maisa/types/shared/text_summary.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/__init__.py b/tests/__init__.py
index 1016754e..fd8019a9 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1 +1 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/__init__.py b/tests/api_resources/__init__.py
index 1016754e..fd8019a9 100644
--- a/tests/api_resources/__init__.py
+++ b/tests/api_resources/__init__.py
@@ -1 +1 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/capabilities/__init__.py b/tests/api_resources/capabilities/__init__.py
index 1016754e..fd8019a9 100644
--- a/tests/api_resources/capabilities/__init__.py
+++ b/tests/api_resources/capabilities/__init__.py
@@ -1 +1 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/capabilities/test_media.py b/tests/api_resources/capabilities/test_media.py
index 473c7254..65429e03 100644
--- a/tests/api_resources/capabilities/test_media.py
+++ b/tests/api_resources/capabilities/test_media.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/tests/api_resources/file_interpreter/__init__.py b/tests/api_resources/file_interpreter/__init__.py
index 1016754e..fd8019a9 100644
--- a/tests/api_resources/file_interpreter/__init__.py
+++ b/tests/api_resources/file_interpreter/__init__.py
@@ -1 +1 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/file_interpreter/test_from_audio.py b/tests/api_resources/file_interpreter/test_from_audio.py
index 5cabbc0d..9bbcd1db 100644
--- a/tests/api_resources/file_interpreter/test_from_audio.py
+++ b/tests/api_resources/file_interpreter/test_from_audio.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/tests/api_resources/file_interpreter/test_from_docx.py b/tests/api_resources/file_interpreter/test_from_docx.py
index 1844e948..cbe686ef 100644
--- a/tests/api_resources/file_interpreter/test_from_docx.py
+++ b/tests/api_resources/file_interpreter/test_from_docx.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/tests/api_resources/file_interpreter/test_from_html.py b/tests/api_resources/file_interpreter/test_from_html.py
index 577d1058..a08ea59b 100644
--- a/tests/api_resources/file_interpreter/test_from_html.py
+++ b/tests/api_resources/file_interpreter/test_from_html.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/tests/api_resources/file_interpreter/test_from_image.py b/tests/api_resources/file_interpreter/test_from_image.py
index 89a57985..9d81ce0d 100644
--- a/tests/api_resources/file_interpreter/test_from_image.py
+++ b/tests/api_resources/file_interpreter/test_from_image.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
@@ -9,7 +9,6 @@
from maisa import Maisa, AsyncMaisa
from tests.utils import assert_matches_type
-from maisa.types.file_interpreter import FromImage
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -22,7 +21,7 @@ def test_method_create(self, client: Maisa) -> None:
from_image = client.file_interpreter.from_image.create(
file=b"raw file contents",
)
- assert_matches_type(FromImage, from_image, path=["response"])
+ assert_matches_type(object, from_image, path=["response"])
@parametrize
def test_raw_response_create(self, client: Maisa) -> None:
@@ -33,7 +32,7 @@ def test_raw_response_create(self, client: Maisa) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
from_image = response.parse()
- assert_matches_type(FromImage, from_image, path=["response"])
+ assert_matches_type(object, from_image, path=["response"])
@parametrize
def test_streaming_response_create(self, client: Maisa) -> None:
@@ -44,7 +43,7 @@ def test_streaming_response_create(self, client: Maisa) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
from_image = response.parse()
- assert_matches_type(FromImage, from_image, path=["response"])
+ assert_matches_type(object, from_image, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -57,7 +56,7 @@ async def test_method_create(self, async_client: AsyncMaisa) -> None:
from_image = await async_client.file_interpreter.from_image.create(
file=b"raw file contents",
)
- assert_matches_type(FromImage, from_image, path=["response"])
+ assert_matches_type(object, from_image, path=["response"])
@parametrize
async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
@@ -68,7 +67,7 @@ async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
from_image = await response.parse()
- assert_matches_type(FromImage, from_image, path=["response"])
+ assert_matches_type(object, from_image, path=["response"])
@parametrize
async def test_streaming_response_create(self, async_client: AsyncMaisa) -> None:
@@ -79,6 +78,6 @@ async def test_streaming_response_create(self, async_client: AsyncMaisa) -> None
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
from_image = await response.parse()
- assert_matches_type(FromImage, from_image, path=["response"])
+ assert_matches_type(object, from_image, path=["response"])
assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/file_interpreter/test_from_pdf.py b/tests/api_resources/file_interpreter/test_from_pdf.py
index 3451794f..2aa4b8d0 100644
--- a/tests/api_resources/file_interpreter/test_from_pdf.py
+++ b/tests/api_resources/file_interpreter/test_from_pdf.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/tests/api_resources/file_interpreter/test_from_pdf_scanned.py b/tests/api_resources/file_interpreter/test_from_pdf_scanned.py
new file mode 100644
index 00000000..9a1dc1d7
--- /dev/null
+++ b/tests/api_resources/file_interpreter/test_from_pdf_scanned.py
@@ -0,0 +1,125 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from maisa import Maisa, AsyncMaisa
+from tests.utils import assert_matches_type
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestFromPdfScanned:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_create(self, client: Maisa) -> None:
+ from_pdf_scanned = client.file_interpreter.from_pdf_scanned.create(
+ file=b"raw file contents",
+ )
+ assert_matches_type(object, from_pdf_scanned, path=["response"])
+
+ @parametrize
+ def test_method_create_with_all_params(self, client: Maisa) -> None:
+ from_pdf_scanned = client.file_interpreter.from_pdf_scanned.create(
+ file=b"raw file contents",
+ lang="en",
+ max_pages=0,
+ variable1_description="The name of the person.",
+ variable1_name="Name",
+ variable1_type="string",
+ variable2_description="The name of the person.",
+ variable2_name="Name",
+ variable2_type="string",
+ variable3_description="The name of the person.",
+ variable3_name="Name",
+ variable3_type="string",
+ variable4_description="The name of the person.",
+ variable4_name="Name",
+ variable4_type="string",
+ )
+ assert_matches_type(object, from_pdf_scanned, path=["response"])
+
+ @parametrize
+ def test_raw_response_create(self, client: Maisa) -> None:
+ response = client.file_interpreter.from_pdf_scanned.with_raw_response.create(
+ file=b"raw file contents",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ from_pdf_scanned = response.parse()
+ assert_matches_type(object, from_pdf_scanned, path=["response"])
+
+ @parametrize
+ def test_streaming_response_create(self, client: Maisa) -> None:
+ with client.file_interpreter.from_pdf_scanned.with_streaming_response.create(
+ file=b"raw file contents",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ from_pdf_scanned = response.parse()
+ assert_matches_type(object, from_pdf_scanned, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncFromPdfScanned:
+ parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ async def test_method_create(self, async_client: AsyncMaisa) -> None:
+ from_pdf_scanned = await async_client.file_interpreter.from_pdf_scanned.create(
+ file=b"raw file contents",
+ )
+ assert_matches_type(object, from_pdf_scanned, path=["response"])
+
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncMaisa) -> None:
+ from_pdf_scanned = await async_client.file_interpreter.from_pdf_scanned.create(
+ file=b"raw file contents",
+ lang="en",
+ max_pages=0,
+ variable1_description="The name of the person.",
+ variable1_name="Name",
+ variable1_type="string",
+ variable2_description="The name of the person.",
+ variable2_name="Name",
+ variable2_type="string",
+ variable3_description="The name of the person.",
+ variable3_name="Name",
+ variable3_type="string",
+ variable4_description="The name of the person.",
+ variable4_name="Name",
+ variable4_type="string",
+ )
+ assert_matches_type(object, from_pdf_scanned, path=["response"])
+
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
+ response = await async_client.file_interpreter.from_pdf_scanned.with_raw_response.create(
+ file=b"raw file contents",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ from_pdf_scanned = await response.parse()
+ assert_matches_type(object, from_pdf_scanned, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncMaisa) -> None:
+ async with async_client.file_interpreter.from_pdf_scanned.with_streaming_response.create(
+ file=b"raw file contents",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ from_pdf_scanned = await response.parse()
+ assert_matches_type(object, from_pdf_scanned, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/mainet/__init__.py b/tests/api_resources/mainet/__init__.py
index 1016754e..fd8019a9 100644
--- a/tests/api_resources/mainet/__init__.py
+++ b/tests/api_resources/mainet/__init__.py
@@ -1 +1 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/mainet/test_search.py b/tests/api_resources/mainet/test_search.py
deleted file mode 100644
index 8733775d..00000000
--- a/tests/api_resources/mainet/test_search.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# File generated from our OpenAPI spec by Stainless.
-
-from __future__ import annotations
-
-import os
-from typing import Any, cast
-
-import pytest
-
-from maisa import Maisa, AsyncMaisa
-from tests.utils import assert_matches_type
-from maisa.types.mainet import Search
-
-base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
-
-
-class TestSearch:
- parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
-
- @parametrize
- def test_method_create(self, client: Maisa) -> None:
- search = client.mainet.search.create(
- text="Plato, the philosopher, was born in Athens.",
- )
- assert_matches_type(Search, search, path=["response"])
-
- @parametrize
- def test_raw_response_create(self, client: Maisa) -> None:
- response = client.mainet.search.with_raw_response.create(
- text="Plato, the philosopher, was born in Athens.",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- search = response.parse()
- assert_matches_type(Search, search, path=["response"])
-
- @parametrize
- def test_streaming_response_create(self, client: Maisa) -> None:
- with client.mainet.search.with_streaming_response.create(
- text="Plato, the philosopher, was born in Athens.",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- search = response.parse()
- assert_matches_type(Search, search, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
-
-class TestAsyncSearch:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
-
- @parametrize
- async def test_method_create(self, async_client: AsyncMaisa) -> None:
- search = await async_client.mainet.search.create(
- text="Plato, the philosopher, was born in Athens.",
- )
- assert_matches_type(Search, search, path=["response"])
-
- @parametrize
- async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
- response = await async_client.mainet.search.with_raw_response.create(
- text="Plato, the philosopher, was born in Athens.",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- search = await response.parse()
- assert_matches_type(Search, search, path=["response"])
-
- @parametrize
- async def test_streaming_response_create(self, async_client: AsyncMaisa) -> None:
- async with async_client.mainet.search.with_streaming_response.create(
- text="Plato, the philosopher, was born in Athens.",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- search = await response.parse()
- assert_matches_type(Search, search, path=["response"])
-
- assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/models/__init__.py b/tests/api_resources/models/__init__.py
index 1016754e..fd8019a9 100644
--- a/tests/api_resources/models/__init__.py
+++ b/tests/api_resources/models/__init__.py
@@ -1 +1 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/models/test_embeddings.py b/tests/api_resources/models/test_embeddings.py
index 074301e5..ab820d70 100644
--- a/tests/api_resources/models/test_embeddings.py
+++ b/tests/api_resources/models/test_embeddings.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/tests/api_resources/models/test_rerank.py b/tests/api_resources/models/test_rerank.py
deleted file mode 100644
index 8e043d8d..00000000
--- a/tests/api_resources/models/test_rerank.py
+++ /dev/null
@@ -1,90 +0,0 @@
-# File generated from our OpenAPI spec by Stainless.
-
-from __future__ import annotations
-
-import os
-from typing import Any, cast
-
-import pytest
-
-from maisa import Maisa, AsyncMaisa
-from tests.utils import assert_matches_type
-from maisa.types.models import Rerank
-
-base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
-
-
-class TestRerank:
- parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
-
- @parametrize
- def test_method_create(self, client: Maisa) -> None:
- rerank = client.models.rerank.create(
- sentences=["The light bulb was invented by Thomas Edison", "It's a nice day, isn't it"],
- source_sentence="Who invented the light bulb?",
- )
- assert_matches_type(Rerank, rerank, path=["response"])
-
- @parametrize
- def test_raw_response_create(self, client: Maisa) -> None:
- response = client.models.rerank.with_raw_response.create(
- sentences=["The light bulb was invented by Thomas Edison", "It's a nice day, isn't it"],
- source_sentence="Who invented the light bulb?",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- rerank = response.parse()
- assert_matches_type(Rerank, rerank, path=["response"])
-
- @parametrize
- def test_streaming_response_create(self, client: Maisa) -> None:
- with client.models.rerank.with_streaming_response.create(
- sentences=["The light bulb was invented by Thomas Edison", "It's a nice day, isn't it"],
- source_sentence="Who invented the light bulb?",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- rerank = response.parse()
- assert_matches_type(Rerank, rerank, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
-
-class TestAsyncRerank:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
-
- @parametrize
- async def test_method_create(self, async_client: AsyncMaisa) -> None:
- rerank = await async_client.models.rerank.create(
- sentences=["The light bulb was invented by Thomas Edison", "It's a nice day, isn't it"],
- source_sentence="Who invented the light bulb?",
- )
- assert_matches_type(Rerank, rerank, path=["response"])
-
- @parametrize
- async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
- response = await async_client.models.rerank.with_raw_response.create(
- sentences=["The light bulb was invented by Thomas Edison", "It's a nice day, isn't it"],
- source_sentence="Who invented the light bulb?",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- rerank = await response.parse()
- assert_matches_type(Rerank, rerank, path=["response"])
-
- @parametrize
- async def test_streaming_response_create(self, async_client: AsyncMaisa) -> None:
- async with async_client.models.rerank.with_streaming_response.create(
- sentences=["The light bulb was invented by Thomas Edison", "It's a nice day, isn't it"],
- source_sentence="Who invented the light bulb?",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- rerank = await response.parse()
- assert_matches_type(Rerank, rerank, path=["response"])
-
- assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/test_capabilities.py b/tests/api_resources/test_capabilities.py
index e3363ebf..c5df9a99 100644
--- a/tests/api_resources/test_capabilities.py
+++ b/tests/api_resources/test_capabilities.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
diff --git a/tests/api_resources/test_kpu.py b/tests/api_resources/test_kpu.py
index 93eb736a..a9f9f1be 100644
--- a/tests/api_resources/test_kpu.py
+++ b/tests/api_resources/test_kpu.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
@@ -8,7 +8,6 @@
import pytest
from maisa import Maisa, AsyncMaisa
-from maisa.types import KpuRunResponse
from tests.utils import assert_matches_type
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -22,7 +21,7 @@ def test_method_run(self, client: Maisa) -> None:
kpu = client.kpu.run(
query="string",
)
- assert_matches_type(KpuRunResponse, kpu, path=["response"])
+ assert_matches_type(object, kpu, path=["response"])
@parametrize
def test_method_run_with_all_params(self, client: Maisa) -> None:
@@ -31,8 +30,10 @@ def test_method_run_with_all_params(self, client: Maisa) -> None:
explain_steps=True,
retries=1,
file=[b"raw file contents", b"raw file contents", b"raw file contents"],
+ reasoner_model="gpt-4-turbo",
+ reasoner_prompt="string",
)
- assert_matches_type(KpuRunResponse, kpu, path=["response"])
+ assert_matches_type(object, kpu, path=["response"])
@parametrize
def test_raw_response_run(self, client: Maisa) -> None:
@@ -43,7 +44,7 @@ def test_raw_response_run(self, client: Maisa) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
kpu = response.parse()
- assert_matches_type(KpuRunResponse, kpu, path=["response"])
+ assert_matches_type(object, kpu, path=["response"])
@parametrize
def test_streaming_response_run(self, client: Maisa) -> None:
@@ -54,7 +55,7 @@ def test_streaming_response_run(self, client: Maisa) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
kpu = response.parse()
- assert_matches_type(KpuRunResponse, kpu, path=["response"])
+ assert_matches_type(object, kpu, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -67,7 +68,7 @@ async def test_method_run(self, async_client: AsyncMaisa) -> None:
kpu = await async_client.kpu.run(
query="string",
)
- assert_matches_type(KpuRunResponse, kpu, path=["response"])
+ assert_matches_type(object, kpu, path=["response"])
@parametrize
async def test_method_run_with_all_params(self, async_client: AsyncMaisa) -> None:
@@ -76,8 +77,10 @@ async def test_method_run_with_all_params(self, async_client: AsyncMaisa) -> Non
explain_steps=True,
retries=1,
file=[b"raw file contents", b"raw file contents", b"raw file contents"],
+ reasoner_model="gpt-4-turbo",
+ reasoner_prompt="string",
)
- assert_matches_type(KpuRunResponse, kpu, path=["response"])
+ assert_matches_type(object, kpu, path=["response"])
@parametrize
async def test_raw_response_run(self, async_client: AsyncMaisa) -> None:
@@ -88,7 +91,7 @@ async def test_raw_response_run(self, async_client: AsyncMaisa) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
kpu = await response.parse()
- assert_matches_type(KpuRunResponse, kpu, path=["response"])
+ assert_matches_type(object, kpu, path=["response"])
@parametrize
async def test_streaming_response_run(self, async_client: AsyncMaisa) -> None:
@@ -99,6 +102,6 @@ async def test_streaming_response_run(self, async_client: AsyncMaisa) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
kpu = await response.parse()
- assert_matches_type(KpuRunResponse, kpu, path=["response"])
+ assert_matches_type(object, kpu, path=["response"])
assert cast(Any, response.is_closed) is True
diff --git a/tests/test_client.py b/tests/test_client.py
index 050dd28c..5f8ba8e7 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -1,4 +1,4 @@
-# File generated from our OpenAPI spec by Stainless.
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
@@ -636,6 +636,10 @@ class Model(BaseModel):
assert isinstance(exc.value.__cause__, ValidationError)
+ def test_client_max_retries_validation(self) -> None:
+ with pytest.raises(TypeError, match=r"max_retries cannot be None"):
+ Maisa(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None))
+
@pytest.mark.respx(base_url=base_url)
def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
class Model(BaseModel):
@@ -1314,6 +1318,12 @@ class Model(BaseModel):
assert isinstance(exc.value.__cause__, ValidationError)
+ async def test_client_max_retries_validation(self) -> None:
+ with pytest.raises(TypeError, match=r"max_retries cannot be None"):
+ AsyncMaisa(
+ base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)
+ )
+
@pytest.mark.respx(base_url=base_url)
@pytest.mark.asyncio
async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
diff --git a/tests/test_models.py b/tests/test_models.py
index 0eaf4beb..9e18d54e 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,14 +1,15 @@
import json
from typing import Any, Dict, List, Union, Optional, cast
from datetime import datetime, timezone
-from typing_extensions import Literal
+from typing_extensions import Literal, Annotated
import pytest
import pydantic
from pydantic import Field
+from maisa._utils import PropertyInfo
from maisa._compat import PYDANTIC_V2, parse_obj, model_dump, model_json
-from maisa._models import BaseModel
+from maisa._models import BaseModel, construct_type
class BasicModel(BaseModel):
@@ -500,6 +501,42 @@ class Model(BaseModel):
assert "resource_id" in m.model_fields_set
+def test_to_dict() -> None:
+ class Model(BaseModel):
+ foo: Optional[str] = Field(alias="FOO", default=None)
+
+ m = Model(FOO="hello")
+ assert m.to_dict() == {"FOO": "hello"}
+ assert m.to_dict(use_api_names=False) == {"foo": "hello"}
+
+ m2 = Model()
+ assert m2.to_dict() == {}
+ assert m2.to_dict(exclude_unset=False) == {"FOO": None}
+ assert m2.to_dict(exclude_unset=False, exclude_none=True) == {}
+ assert m2.to_dict(exclude_unset=False, exclude_defaults=True) == {}
+
+ m3 = Model(FOO=None)
+ assert m3.to_dict() == {"FOO": None}
+ assert m3.to_dict(exclude_none=True) == {}
+ assert m3.to_dict(exclude_defaults=True) == {}
+
+ if PYDANTIC_V2:
+
+ class Model2(BaseModel):
+ created_at: datetime
+
+ time_str = "2024-03-21T11:39:01.275859"
+ m4 = Model2.construct(created_at=time_str)
+ assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)}
+ assert m4.to_dict(mode="json") == {"created_at": time_str}
+ else:
+ with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"):
+ m.to_dict(mode="json")
+
+ with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"):
+ m.to_dict(warnings=False)
+
+
def test_forwards_compat_model_dump_method() -> None:
class Model(BaseModel):
foo: Optional[str] = Field(alias="FOO", default=None)
@@ -531,6 +568,34 @@ class Model(BaseModel):
m.model_dump(warnings=False)
+def test_to_json() -> None:
+ class Model(BaseModel):
+ foo: Optional[str] = Field(alias="FOO", default=None)
+
+ m = Model(FOO="hello")
+ assert json.loads(m.to_json()) == {"FOO": "hello"}
+ assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"}
+
+ if PYDANTIC_V2:
+ assert m.to_json(indent=None) == '{"FOO":"hello"}'
+ else:
+ assert m.to_json(indent=None) == '{"FOO": "hello"}'
+
+ m2 = Model()
+ assert json.loads(m2.to_json()) == {}
+ assert json.loads(m2.to_json(exclude_unset=False)) == {"FOO": None}
+ assert json.loads(m2.to_json(exclude_unset=False, exclude_none=True)) == {}
+ assert json.loads(m2.to_json(exclude_unset=False, exclude_defaults=True)) == {}
+
+ m3 = Model(FOO=None)
+ assert json.loads(m3.to_json()) == {"FOO": None}
+ assert json.loads(m3.to_json(exclude_none=True)) == {}
+
+ if not PYDANTIC_V2:
+ with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"):
+ m.to_json(warnings=False)
+
+
def test_forwards_compat_model_dump_json_method() -> None:
class Model(BaseModel):
foo: Optional[str] = Field(alias="FOO", default=None)
@@ -571,3 +636,194 @@ class OurModel(BaseModel):
foo: Optional[str] = None
takes_pydantic(OurModel())
+
+
+def test_annotated_types() -> None:
+ class Model(BaseModel):
+ value: str
+
+ m = construct_type(
+ value={"value": "foo"},
+ type_=cast(Any, Annotated[Model, "random metadata"]),
+ )
+ assert isinstance(m, Model)
+ assert m.value == "foo"
+
+
+def test_discriminated_unions_invalid_data() -> None:
+ class A(BaseModel):
+ type: Literal["a"]
+
+ data: str
+
+ class B(BaseModel):
+ type: Literal["b"]
+
+ data: int
+
+ m = construct_type(
+ value={"type": "b", "data": "foo"},
+ type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]),
+ )
+ assert isinstance(m, B)
+ assert m.type == "b"
+ assert m.data == "foo" # type: ignore[comparison-overlap]
+
+ m = construct_type(
+ value={"type": "a", "data": 100},
+ type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]),
+ )
+ assert isinstance(m, A)
+ assert m.type == "a"
+ if PYDANTIC_V2:
+ assert m.data == 100 # type: ignore[comparison-overlap]
+ else:
+ # pydantic v1 automatically converts inputs to strings
+ # if the expected type is a str
+ assert m.data == "100"
+
+
+def test_discriminated_unions_unknown_variant() -> None:
+ class A(BaseModel):
+ type: Literal["a"]
+
+ data: str
+
+ class B(BaseModel):
+ type: Literal["b"]
+
+ data: int
+
+ m = construct_type(
+ value={"type": "c", "data": None, "new_thing": "bar"},
+ type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]),
+ )
+
+ # just chooses the first variant
+ assert isinstance(m, A)
+ assert m.type == "c" # type: ignore[comparison-overlap]
+ assert m.data == None # type: ignore[unreachable]
+ assert m.new_thing == "bar"
+
+
+def test_discriminated_unions_invalid_data_nested_unions() -> None:
+ class A(BaseModel):
+ type: Literal["a"]
+
+ data: str
+
+ class B(BaseModel):
+ type: Literal["b"]
+
+ data: int
+
+ class C(BaseModel):
+ type: Literal["c"]
+
+ data: bool
+
+ m = construct_type(
+ value={"type": "b", "data": "foo"},
+ type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]),
+ )
+ assert isinstance(m, B)
+ assert m.type == "b"
+ assert m.data == "foo" # type: ignore[comparison-overlap]
+
+ m = construct_type(
+ value={"type": "c", "data": "foo"},
+ type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]),
+ )
+ assert isinstance(m, C)
+ assert m.type == "c"
+ assert m.data == "foo" # type: ignore[comparison-overlap]
+
+
+def test_discriminated_unions_with_aliases_invalid_data() -> None:
+ class A(BaseModel):
+ foo_type: Literal["a"] = Field(alias="type")
+
+ data: str
+
+ class B(BaseModel):
+ foo_type: Literal["b"] = Field(alias="type")
+
+ data: int
+
+ m = construct_type(
+ value={"type": "b", "data": "foo"},
+ type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]),
+ )
+ assert isinstance(m, B)
+ assert m.foo_type == "b"
+ assert m.data == "foo" # type: ignore[comparison-overlap]
+
+ m = construct_type(
+ value={"type": "a", "data": 100},
+ type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]),
+ )
+ assert isinstance(m, A)
+ assert m.foo_type == "a"
+ if PYDANTIC_V2:
+ assert m.data == 100 # type: ignore[comparison-overlap]
+ else:
+ # pydantic v1 automatically converts inputs to strings
+ # if the expected type is a str
+ assert m.data == "100"
+
+
+def test_discriminated_unions_overlapping_discriminators_invalid_data() -> None:
+ class A(BaseModel):
+ type: Literal["a"]
+
+ data: bool
+
+ class B(BaseModel):
+ type: Literal["a"]
+
+ data: int
+
+ m = construct_type(
+ value={"type": "a", "data": "foo"},
+ type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]),
+ )
+ assert isinstance(m, B)
+ assert m.type == "a"
+ assert m.data == "foo" # type: ignore[comparison-overlap]
+
+
+def test_discriminated_unions_invalid_data_uses_cache() -> None:
+ class A(BaseModel):
+ type: Literal["a"]
+
+ data: str
+
+ class B(BaseModel):
+ type: Literal["b"]
+
+ data: int
+
+ UnionType = cast(Any, Union[A, B])
+
+ assert not hasattr(UnionType, "__discriminator__")
+
+ m = construct_type(
+ value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")])
+ )
+ assert isinstance(m, B)
+ assert m.type == "b"
+ assert m.data == "foo" # type: ignore[comparison-overlap]
+
+ discriminator = UnionType.__discriminator__
+ assert discriminator is not None
+
+ m = construct_type(
+ value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")])
+ )
+ assert isinstance(m, B)
+ assert m.type == "b"
+ assert m.data == "foo" # type: ignore[comparison-overlap]
+
+ # if the discriminator details object stays the same between invocations then
+ # we hit the cache
+ assert UnionType.__discriminator__ is discriminator
diff --git a/tests/test_response.py b/tests/test_response.py
index ac09ad19..eebb8027 100644
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -1,5 +1,6 @@
import json
-from typing import List
+from typing import List, cast
+from typing_extensions import Annotated
import httpx
import pytest
@@ -157,3 +158,37 @@ async def test_async_response_parse_custom_model(async_client: AsyncMaisa) -> No
obj = await response.parse(to=CustomModel)
assert obj.foo == "hello!"
assert obj.bar == 2
+
+
+def test_response_parse_annotated_type(client: Maisa) -> None:
+ response = APIResponse(
+ raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})),
+ client=client,
+ stream=False,
+ stream_cls=None,
+ cast_to=str,
+ options=FinalRequestOptions.construct(method="get", url="/foo"),
+ )
+
+ obj = response.parse(
+ to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]),
+ )
+ assert obj.foo == "hello!"
+ assert obj.bar == 2
+
+
+async def test_async_response_parse_annotated_type(async_client: AsyncMaisa) -> None:
+ response = AsyncAPIResponse(
+ raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})),
+ client=async_client,
+ stream=False,
+ stream_cls=None,
+ cast_to=str,
+ options=FinalRequestOptions.construct(method="get", url="/foo"),
+ )
+
+ obj = await response.parse(
+ to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]),
+ )
+ assert obj.foo == "hello!"
+ assert obj.bar == 2
diff --git a/tests/test_streaming.py b/tests/test_streaming.py
index 9a3d470d..f57f3e2c 100644
--- a/tests/test_streaming.py
+++ b/tests/test_streaming.py
@@ -1,104 +1,248 @@
+from __future__ import annotations
+
from typing import Iterator, AsyncIterator
+import httpx
import pytest
-from maisa._streaming import SSEDecoder
+from maisa import Maisa, AsyncMaisa
+from maisa._streaming import Stream, AsyncStream, ServerSentEvent
@pytest.mark.asyncio
-async def test_basic_async() -> None:
- async def body() -> AsyncIterator[str]:
- yield "event: completion"
- yield 'data: {"foo":true}'
- yield ""
-
- async for sse in SSEDecoder().aiter(body()):
- assert sse.event == "completion"
- assert sse.json() == {"foo": True}
+@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
+async def test_basic(sync: bool, client: Maisa, async_client: AsyncMaisa) -> None:
+ def body() -> Iterator[bytes]:
+ yield b"event: completion\n"
+ yield b'data: {"foo":true}\n'
+ yield b"\n"
+ iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client)
-def test_basic() -> None:
- def body() -> Iterator[str]:
- yield "event: completion"
- yield 'data: {"foo":true}'
- yield ""
-
- it = SSEDecoder().iter(body())
- sse = next(it)
+ sse = await iter_next(iterator)
assert sse.event == "completion"
assert sse.json() == {"foo": True}
- with pytest.raises(StopIteration):
- next(it)
+ await assert_empty_iter(iterator)
-def test_data_missing_event() -> None:
- def body() -> Iterator[str]:
- yield 'data: {"foo":true}'
- yield ""
+@pytest.mark.asyncio
+@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
+async def test_data_missing_event(sync: bool, client: Maisa, async_client: AsyncMaisa) -> None:
+ def body() -> Iterator[bytes]:
+ yield b'data: {"foo":true}\n'
+ yield b"\n"
- it = SSEDecoder().iter(body())
- sse = next(it)
+ iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client)
+
+ sse = await iter_next(iterator)
assert sse.event is None
assert sse.json() == {"foo": True}
- with pytest.raises(StopIteration):
- next(it)
+ await assert_empty_iter(iterator)
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
+async def test_event_missing_data(sync: bool, client: Maisa, async_client: AsyncMaisa) -> None:
+ def body() -> Iterator[bytes]:
+ yield b"event: ping\n"
+ yield b"\n"
-def test_event_missing_data() -> None:
- def body() -> Iterator[str]:
- yield "event: ping"
- yield ""
+ iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client)
- it = SSEDecoder().iter(body())
- sse = next(it)
+ sse = await iter_next(iterator)
assert sse.event == "ping"
assert sse.data == ""
- with pytest.raises(StopIteration):
- next(it)
+ await assert_empty_iter(iterator)
-def test_multiple_events() -> None:
- def body() -> Iterator[str]:
- yield "event: ping"
- yield ""
- yield "event: completion"
- yield ""
+@pytest.mark.asyncio
+@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
+async def test_multiple_events(sync: bool, client: Maisa, async_client: AsyncMaisa) -> None:
+ def body() -> Iterator[bytes]:
+ yield b"event: ping\n"
+ yield b"\n"
+ yield b"event: completion\n"
+ yield b"\n"
- it = SSEDecoder().iter(body())
+ iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client)
- sse = next(it)
+ sse = await iter_next(iterator)
assert sse.event == "ping"
assert sse.data == ""
- sse = next(it)
+ sse = await iter_next(iterator)
assert sse.event == "completion"
assert sse.data == ""
- with pytest.raises(StopIteration):
- next(it)
-
-
-def test_multiple_events_with_data() -> None:
- def body() -> Iterator[str]:
- yield "event: ping"
- yield 'data: {"foo":true}'
- yield ""
- yield "event: completion"
- yield 'data: {"bar":false}'
- yield ""
+ await assert_empty_iter(iterator)
- it = SSEDecoder().iter(body())
- sse = next(it)
+@pytest.mark.asyncio
+@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
+async def test_multiple_events_with_data(sync: bool, client: Maisa, async_client: AsyncMaisa) -> None:
+ def body() -> Iterator[bytes]:
+ yield b"event: ping\n"
+ yield b'data: {"foo":true}\n'
+ yield b"\n"
+ yield b"event: completion\n"
+ yield b'data: {"bar":false}\n'
+ yield b"\n"
+
+ iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client)
+
+ sse = await iter_next(iterator)
assert sse.event == "ping"
assert sse.json() == {"foo": True}
- sse = next(it)
+ sse = await iter_next(iterator)
assert sse.event == "completion"
assert sse.json() == {"bar": False}
- with pytest.raises(StopIteration):
- next(it)
+ await assert_empty_iter(iterator)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
+async def test_multiple_data_lines_with_empty_line(sync: bool, client: Maisa, async_client: AsyncMaisa) -> None:
+ def body() -> Iterator[bytes]:
+ yield b"event: ping\n"
+ yield b"data: {\n"
+ yield b'data: "foo":\n'
+ yield b"data: \n"
+ yield b"data:\n"
+ yield b"data: true}\n"
+ yield b"\n\n"
+
+ iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client)
+
+ sse = await iter_next(iterator)
+ assert sse.event == "ping"
+ assert sse.json() == {"foo": True}
+ assert sse.data == '{\n"foo":\n\n\ntrue}'
+
+ await assert_empty_iter(iterator)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
+async def test_data_json_escaped_double_new_line(sync: bool, client: Maisa, async_client: AsyncMaisa) -> None:
+ def body() -> Iterator[bytes]:
+ yield b"event: ping\n"
+ yield b'data: {"foo": "my long\\n\\ncontent"}'
+ yield b"\n\n"
+
+ iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client)
+
+ sse = await iter_next(iterator)
+ assert sse.event == "ping"
+ assert sse.json() == {"foo": "my long\n\ncontent"}
+
+ await assert_empty_iter(iterator)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
+async def test_multiple_data_lines(sync: bool, client: Maisa, async_client: AsyncMaisa) -> None:
+ def body() -> Iterator[bytes]:
+ yield b"event: ping\n"
+ yield b"data: {\n"
+ yield b'data: "foo":\n'
+ yield b"data: true}\n"
+ yield b"\n\n"
+
+ iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client)
+
+ sse = await iter_next(iterator)
+ assert sse.event == "ping"
+ assert sse.json() == {"foo": True}
+
+ await assert_empty_iter(iterator)
+
+
+@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
+async def test_special_new_line_character(
+ sync: bool,
+ client: Maisa,
+ async_client: AsyncMaisa,
+) -> None:
+ def body() -> Iterator[bytes]:
+ yield b'data: {"content":" culpa"}\n'
+ yield b"\n"
+ yield b'data: {"content":" \xe2\x80\xa8"}\n'
+ yield b"\n"
+ yield b'data: {"content":"foo"}\n'
+ yield b"\n"
+
+ iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client)
+
+ sse = await iter_next(iterator)
+ assert sse.event is None
+ assert sse.json() == {"content": " culpa"}
+
+ sse = await iter_next(iterator)
+ assert sse.event is None
+ assert sse.json() == {"content": "
"}
+
+ sse = await iter_next(iterator)
+ assert sse.event is None
+ assert sse.json() == {"content": "foo"}
+
+ await assert_empty_iter(iterator)
+
+
+@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
+async def test_multi_byte_character_multiple_chunks(
+ sync: bool,
+ client: Maisa,
+ async_client: AsyncMaisa,
+) -> None:
+ def body() -> Iterator[bytes]:
+ yield b'data: {"content":"'
+ # bytes taken from the string 'известни' and arbitrarily split
+ # so that some multi-byte characters span multiple chunks
+ yield b"\xd0"
+ yield b"\xb8\xd0\xb7\xd0"
+ yield b"\xb2\xd0\xb5\xd1\x81\xd1\x82\xd0\xbd\xd0\xb8"
+ yield b'"}\n'
+ yield b"\n"
+
+ iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client)
+
+ sse = await iter_next(iterator)
+ assert sse.event is None
+ assert sse.json() == {"content": "известни"}
+
+
+async def to_aiter(iter: Iterator[bytes]) -> AsyncIterator[bytes]:
+ for chunk in iter:
+ yield chunk
+
+
+async def iter_next(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> ServerSentEvent:
+ if isinstance(iter, AsyncIterator):
+ return await iter.__anext__()
+
+ return next(iter)
+
+
+async def assert_empty_iter(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> None:
+ with pytest.raises((StopAsyncIteration, RuntimeError)):
+ await iter_next(iter)
+
+
+def make_event_iterator(
+ content: Iterator[bytes],
+ *,
+ sync: bool,
+ client: Maisa,
+ async_client: AsyncMaisa,
+) -> Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]:
+ if sync:
+ return Stream(cast_to=object, client=client, response=httpx.Response(200, content=content))._iter_events()
+
+ return AsyncStream(
+ cast_to=object, client=async_client, response=httpx.Response(200, content=to_aiter(content))
+ )._iter_events()
diff --git a/tests/utils.py b/tests/utils.py
index 3fac0a31..be2c2445 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -14,6 +14,8 @@
is_list,
is_list_type,
is_union_type,
+ extract_type_arg,
+ is_annotated_type,
)
from maisa._compat import PYDANTIC_V2, field_outer_type, get_model_fields
from maisa._models import BaseModel
@@ -49,6 +51,10 @@ def assert_matches_type(
path: list[str],
allow_none: bool = False,
) -> None:
+ # unwrap `Annotated[T, ...]` -> `T`
+ if is_annotated_type(type_):
+ type_ = extract_type_arg(type_, 0)
+
if allow_none and value is None:
return
From 9d1408bab9d6a93e0058ebe9a62b4f84d036f133 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 May 2024 11:54:26 +0000
Subject: [PATCH 002/200] chore: rebuild project due to oas spec rename (#20)
---
.github/workflows/ci.yml | 21 +-
.github/workflows/publish-pypi.yml | 2 +-
.github/workflows/release-doctor.yml | 2 +-
.gitignore | 1 +
.stats.yml | 3 +-
Brewfile | 2 +
README.md | 4 +-
api.md | 12 -
bin/check-env-state.py | 40 ---
bin/check-test-server | 50 ---
bin/test | 3 -
pyproject.toml | 3 +-
scripts/bootstrap | 19 ++
scripts/format | 8 +
scripts/lint | 8 +
scripts/mock | 41 +++
scripts/test | 57 ++++
{bin => scripts/utils}/ruffen-docs.py | 0
src/maisa/_base_client.py | 9 +-
src/maisa/_client.py | 64 ++--
src/maisa/resources/__init__.py | 96 +++---
src/maisa/resources/capabilities/__init__.py | 48 +--
.../resources/capabilities/capabilities.py | 78 ++---
src/maisa/resources/capabilities/media.py | 42 +--
.../resources/file_interpreter/__init__.py | 158 +++++-----
.../file_interpreter/file_interpreter.py | 250 +++++++--------
.../resources/file_interpreter/from_audio.py | 38 +--
.../resources/file_interpreter/from_docx.py | 38 +--
.../resources/file_interpreter/from_html.py | 38 +--
.../resources/file_interpreter/from_image.py | 38 +--
.../resources/file_interpreter/from_pdf.py | 38 +--
.../file_interpreter/from_pdf_scanned.py | 288 ------------------
src/maisa/resources/kpu.py | 38 +--
src/maisa/resources/models/__init__.py | 48 +--
src/maisa/resources/models/embeddings.py | 49 +--
src/maisa/resources/models/models.py | 74 ++---
src/maisa/types/file_interpreter/__init__.py | 1 -
.../from_pdf_scanned_create_params.py | 59 ----
.../file_interpreter/test_from_pdf_scanned.py | 125 --------
tests/api_resources/mainet/__init__.py | 1 -
tests/test_client.py | 1 -
tests/utils.py | 17 +-
42 files changed, 735 insertions(+), 1177 deletions(-)
create mode 100644 Brewfile
delete mode 100644 bin/check-env-state.py
delete mode 100755 bin/check-test-server
delete mode 100755 bin/test
create mode 100755 scripts/bootstrap
create mode 100755 scripts/format
create mode 100755 scripts/lint
create mode 100755 scripts/mock
create mode 100755 scripts/test
rename {bin => scripts/utils}/ruffen-docs.py (100%)
delete mode 100644 src/maisa/resources/file_interpreter/from_pdf_scanned.py
delete mode 100644 src/maisa/types/file_interpreter/from_pdf_scanned_create_params.py
delete mode 100644 tests/api_resources/file_interpreter/test_from_pdf_scanned.py
delete mode 100644 tests/api_resources/mainet/__init__.py
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4ddec27a..6ce2f72b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -39,5 +39,24 @@ jobs:
- name: Ensure importable
run: |
rye run python -c 'import maisa'
+ test:
+ name: test
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rye
+ run: |
+ curl -sSf https://rye-up.com/get | bash
+ echo "$HOME/.rye/shims" >> $GITHUB_PATH
+ env:
+ RYE_VERSION: 0.24.0
+ RYE_INSTALL_OPTION: '--yes'
+
+ - name: Bootstrap
+ run: ./scripts/bootstrap
+
+ - name: Run tests
+ run: ./scripts/test
-
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index 81fea1d1..3a18e4be 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Install Rye
run: |
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index 7a5fc2f2..588669b5 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -10,7 +10,7 @@ jobs:
if: github.repository == 'maisaai/python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Check release environment
run: |
diff --git a/.gitignore b/.gitignore
index a4b2f8c0..0f9a66a9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ dist
.env
.envrc
codegen.log
+Brewfile.lock.json
diff --git a/.stats.yml b/.stats.yml
index dd473053..4076c464 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1,2 @@
-configured_endpoints: 14
+configured_endpoints: 13
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/maisa%2FMaisa-c82e0a80b379d33d11af783865ec15844bcd750043bf0f1543ebdf72bda79f3a.yml
diff --git a/Brewfile b/Brewfile
new file mode 100644
index 00000000..492ca37b
--- /dev/null
+++ b/Brewfile
@@ -0,0 +1,2 @@
+brew "rye"
+
diff --git a/README.md b/README.md
index 9a62dbb3..5d5c0a00 100644
--- a/README.md
+++ b/README.md
@@ -157,7 +157,7 @@ client = Maisa(
)
# Override per-request:
-client.with_options(timeout=5 * 1000).capabilities.summarize(
+client.with_options(timeout=5.0).capabilities.summarize(
text="Example long text...",
)
```
@@ -231,7 +231,7 @@ The context manager is required so that the response will reliably be closed.
### Making custom/undocumented requests
-This library is typed for convenient access the documented API.
+This library is typed for convenient access to the documented API.
If you need to access undocumented endpoints, params, or response properties, the library can still be used.
diff --git a/api.md b/api.md
index 406ac2f4..163cb96c 100644
--- a/api.md
+++ b/api.md
@@ -60,18 +60,6 @@ Methods:
- client.file_interpreter.from_pdf.create(\*\*params) -> object
-## FromPdfScanned
-
-Types:
-
-```python
-from maisa.types.file_interpreter import FromPdfScannedCreateResponse
-```
-
-Methods:
-
-- client.file_interpreter.from_pdf_scanned.create(\*\*params) -> object
-
## FromDocx
Types:
diff --git a/bin/check-env-state.py b/bin/check-env-state.py
deleted file mode 100644
index e1b8b6cb..00000000
--- a/bin/check-env-state.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""Script that exits 1 if the current environment is not
-in sync with the `requirements-dev.lock` file.
-"""
-
-from pathlib import Path
-
-import importlib_metadata
-
-
-def should_run_sync() -> bool:
- dev_lock = Path(__file__).parent.parent.joinpath("requirements-dev.lock")
-
- for line in dev_lock.read_text().splitlines():
- if not line or line.startswith("#") or line.startswith("-e"):
- continue
-
- dep, lock_version = line.split("==")
-
- try:
- version = importlib_metadata.version(dep)
-
- if lock_version != version:
- print(f"mismatch for {dep} current={version} lock={lock_version}")
- return True
- except Exception:
- print(f"could not import {dep}")
- return True
-
- return False
-
-
-def main() -> None:
- if should_run_sync():
- exit(1)
- else:
- exit(0)
-
-
-if __name__ == "__main__":
- main()
diff --git a/bin/check-test-server b/bin/check-test-server
deleted file mode 100755
index a6fa3495..00000000
--- a/bin/check-test-server
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/usr/bin/env bash
-
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[0;33m'
-NC='\033[0m' # No Color
-
-function prism_is_running() {
- curl --silent "http://localhost:4010" >/dev/null 2>&1
-}
-
-function is_overriding_api_base_url() {
- [ -n "$TEST_API_BASE_URL" ]
-}
-
-if is_overriding_api_base_url ; then
- # If someone is running the tests against the live API, we can trust they know
- # what they're doing and exit early.
- echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}"
-
- exit 0
-elif prism_is_running ; then
- echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}"
- echo
-
- exit 0
-else
- echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server"
- echo -e "running against your OpenAPI spec."
- echo
- echo -e "${YELLOW}To fix:${NC}"
- echo
- echo -e "1. Install Prism (requires Node 16+):"
- echo
- echo -e " With npm:"
- echo -e " \$ ${YELLOW}npm install -g @stoplight/prism-cli${NC}"
- echo
- echo -e " With yarn:"
- echo -e " \$ ${YELLOW}yarn global add @stoplight/prism-cli${NC}"
- echo
- echo -e "2. Run the mock server"
- echo
- echo -e " To run the server, pass in the path of your OpenAPI"
- echo -e " spec to the prism command:"
- echo
- echo -e " \$ ${YELLOW}prism mock path/to/your.openapi.yml${NC}"
- echo
-
- exit 1
-fi
diff --git a/bin/test b/bin/test
deleted file mode 100755
index 60ede7a8..00000000
--- a/bin/test
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env bash
-
-bin/check-test-server && rye run pytest "$@"
diff --git a/pyproject.toml b/pyproject.toml
index e3159c4e..d9df4e66 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -68,7 +68,7 @@ format = { chain = [
"fix:ruff",
]}
"format:black" = "black ."
-"format:docs" = "python bin/ruffen-docs.py README.md api.md"
+"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md"
"format:ruff" = "ruff format"
"format:isort" = "isort ."
@@ -191,5 +191,6 @@ known-first-party = ["maisa", "tests"]
[tool.ruff.per-file-ignores]
"bin/**.py" = ["T201", "T203"]
+"scripts/**.py" = ["T201", "T203"]
"tests/**.py" = ["T201", "T203"]
"examples/**.py" = ["T201", "T203"]
diff --git a/scripts/bootstrap b/scripts/bootstrap
new file mode 100755
index 00000000..29df07e7
--- /dev/null
+++ b/scripts/bootstrap
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then
+ brew bundle check >/dev/null 2>&1 || {
+ echo "==> Installing Homebrew dependencies…"
+ brew bundle
+ }
+fi
+
+echo "==> Installing Python dependencies…"
+
+# experimental uv support makes installations significantly faster
+rye config --set-bool behavior.use-uv=true
+
+rye sync
diff --git a/scripts/format b/scripts/format
new file mode 100755
index 00000000..2a9ea466
--- /dev/null
+++ b/scripts/format
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+rye run format
+
diff --git a/scripts/lint b/scripts/lint
new file mode 100755
index 00000000..0cc68b51
--- /dev/null
+++ b/scripts/lint
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+rye run lint
+
diff --git a/scripts/mock b/scripts/mock
new file mode 100755
index 00000000..fe89a1d0
--- /dev/null
+++ b/scripts/mock
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+if [[ -n "$1" && "$1" != '--'* ]]; then
+ URL="$1"
+ shift
+else
+ URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)"
+fi
+
+# Check if the URL is empty
+if [ -z "$URL" ]; then
+ echo "Error: No OpenAPI spec path/url provided or found in .stats.yml"
+ exit 1
+fi
+
+echo "==> Starting mock server with URL ${URL}"
+
+# Run prism mock on the given spec
+if [ "$1" == "--daemon" ]; then
+ npm exec --package=@stoplight/prism-cli@~5.8 -- prism mock "$URL" &> .prism.log &
+
+ # Wait for server to come online
+ echo -n "Waiting for server"
+ while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do
+ echo -n "."
+ sleep 0.1
+ done
+
+ if grep -q "✖ fatal" ".prism.log"; then
+ cat .prism.log
+ exit 1
+ fi
+
+ echo
+else
+ npm exec --package=@stoplight/prism-cli@~5.8 -- prism mock "$URL"
+fi
diff --git a/scripts/test b/scripts/test
new file mode 100755
index 00000000..be01d044
--- /dev/null
+++ b/scripts/test
@@ -0,0 +1,57 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[0;33m'
+NC='\033[0m' # No Color
+
+function prism_is_running() {
+ curl --silent "http://localhost:4010" >/dev/null 2>&1
+}
+
+kill_server_on_port() {
+ pids=$(lsof -t -i tcp:"$1" || echo "")
+ if [ "$pids" != "" ]; then
+ kill "$pids"
+ echo "Stopped $pids."
+ fi
+}
+
+function is_overriding_api_base_url() {
+ [ -n "$TEST_API_BASE_URL" ]
+}
+
+if ! is_overriding_api_base_url && ! prism_is_running ; then
+ # When we exit this script, make sure to kill the background mock server process
+ trap 'kill_server_on_port 4010' EXIT
+
+ # Start the dev server
+ ./scripts/mock --daemon
+fi
+
+if is_overriding_api_base_url ; then
+ echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}"
+ echo
+elif ! prism_is_running ; then
+ echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server"
+ echo -e "running against your OpenAPI spec."
+ echo
+ echo -e "To run the server, pass in the path or url of your OpenAPI"
+ echo -e "spec to the prism command:"
+ echo
+ echo -e " \$ ${YELLOW}npm exec --package=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}"
+ echo
+
+ exit 1
+else
+ echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}"
+ echo
+fi
+
+# Run tests
+echo "==> Running tests"
+rye run pytest "$@"
diff --git a/bin/ruffen-docs.py b/scripts/utils/ruffen-docs.py
similarity index 100%
rename from bin/ruffen-docs.py
rename to scripts/utils/ruffen-docs.py
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index e8f6709d..338b5a46 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -945,6 +945,8 @@ def _request(
if self.custom_auth is not None:
kwargs["auth"] = self.custom_auth
+ log.debug("Sending HTTP Request: %s %s", request.method, request.url)
+
try:
response = self._client.send(
request,
@@ -983,7 +985,12 @@ def _request(
raise APIConnectionError(request=request) from err
log.debug(
- 'HTTP Request: %s %s "%i %s"', request.method, request.url, response.status_code, response.reason_phrase
+ 'HTTP Response: %s %s "%i %s" %s',
+ request.method,
+ request.url,
+ response.status_code,
+ response.reason_phrase,
+ response.headers,
)
try:
diff --git a/src/maisa/_client.py b/src/maisa/_client.py
index f7c6e9e5..5ed1eceb 100644
--- a/src/maisa/_client.py
+++ b/src/maisa/_client.py
@@ -46,10 +46,10 @@
class Maisa(SyncAPIClient):
- capabilities: resources.Capabilities
- models: resources.Models
- kpu: resources.Kpu
- file_interpreter: resources.FileInterpreter
+ capabilities: resources.CapabilitiesResource
+ models: resources.ModelsResource
+ kpu: resources.KpuResource
+ file_interpreter: resources.FileInterpreterResource
with_raw_response: MaisaWithRawResponse
with_streaming_response: MaisaWithStreamedResponse
@@ -107,10 +107,10 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.capabilities = resources.Capabilities(self)
- self.models = resources.Models(self)
- self.kpu = resources.Kpu(self)
- self.file_interpreter = resources.FileInterpreter(self)
+ self.capabilities = resources.CapabilitiesResource(self)
+ self.models = resources.ModelsResource(self)
+ self.kpu = resources.KpuResource(self)
+ self.file_interpreter = resources.FileInterpreterResource(self)
self.with_raw_response = MaisaWithRawResponse(self)
self.with_streaming_response = MaisaWithStreamedResponse(self)
@@ -220,10 +220,10 @@ def _make_status_error(
class AsyncMaisa(AsyncAPIClient):
- capabilities: resources.AsyncCapabilities
- models: resources.AsyncModels
- kpu: resources.AsyncKpu
- file_interpreter: resources.AsyncFileInterpreter
+ capabilities: resources.AsyncCapabilitiesResource
+ models: resources.AsyncModelsResource
+ kpu: resources.AsyncKpuResource
+ file_interpreter: resources.AsyncFileInterpreterResource
with_raw_response: AsyncMaisaWithRawResponse
with_streaming_response: AsyncMaisaWithStreamedResponse
@@ -281,10 +281,10 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.capabilities = resources.AsyncCapabilities(self)
- self.models = resources.AsyncModels(self)
- self.kpu = resources.AsyncKpu(self)
- self.file_interpreter = resources.AsyncFileInterpreter(self)
+ self.capabilities = resources.AsyncCapabilitiesResource(self)
+ self.models = resources.AsyncModelsResource(self)
+ self.kpu = resources.AsyncKpuResource(self)
+ self.file_interpreter = resources.AsyncFileInterpreterResource(self)
self.with_raw_response = AsyncMaisaWithRawResponse(self)
self.with_streaming_response = AsyncMaisaWithStreamedResponse(self)
@@ -395,34 +395,34 @@ def _make_status_error(
class MaisaWithRawResponse:
def __init__(self, client: Maisa) -> None:
- self.capabilities = resources.CapabilitiesWithRawResponse(client.capabilities)
- self.models = resources.ModelsWithRawResponse(client.models)
- self.kpu = resources.KpuWithRawResponse(client.kpu)
- self.file_interpreter = resources.FileInterpreterWithRawResponse(client.file_interpreter)
+ self.capabilities = resources.CapabilitiesResourceWithRawResponse(client.capabilities)
+ self.models = resources.ModelsResourceWithRawResponse(client.models)
+ self.kpu = resources.KpuResourceWithRawResponse(client.kpu)
+ self.file_interpreter = resources.FileInterpreterResourceWithRawResponse(client.file_interpreter)
class AsyncMaisaWithRawResponse:
def __init__(self, client: AsyncMaisa) -> None:
- self.capabilities = resources.AsyncCapabilitiesWithRawResponse(client.capabilities)
- self.models = resources.AsyncModelsWithRawResponse(client.models)
- self.kpu = resources.AsyncKpuWithRawResponse(client.kpu)
- self.file_interpreter = resources.AsyncFileInterpreterWithRawResponse(client.file_interpreter)
+ self.capabilities = resources.AsyncCapabilitiesResourceWithRawResponse(client.capabilities)
+ self.models = resources.AsyncModelsResourceWithRawResponse(client.models)
+ self.kpu = resources.AsyncKpuResourceWithRawResponse(client.kpu)
+ self.file_interpreter = resources.AsyncFileInterpreterResourceWithRawResponse(client.file_interpreter)
class MaisaWithStreamedResponse:
def __init__(self, client: Maisa) -> None:
- self.capabilities = resources.CapabilitiesWithStreamingResponse(client.capabilities)
- self.models = resources.ModelsWithStreamingResponse(client.models)
- self.kpu = resources.KpuWithStreamingResponse(client.kpu)
- self.file_interpreter = resources.FileInterpreterWithStreamingResponse(client.file_interpreter)
+ self.capabilities = resources.CapabilitiesResourceWithStreamingResponse(client.capabilities)
+ self.models = resources.ModelsResourceWithStreamingResponse(client.models)
+ self.kpu = resources.KpuResourceWithStreamingResponse(client.kpu)
+ self.file_interpreter = resources.FileInterpreterResourceWithStreamingResponse(client.file_interpreter)
class AsyncMaisaWithStreamedResponse:
def __init__(self, client: AsyncMaisa) -> None:
- self.capabilities = resources.AsyncCapabilitiesWithStreamingResponse(client.capabilities)
- self.models = resources.AsyncModelsWithStreamingResponse(client.models)
- self.kpu = resources.AsyncKpuWithStreamingResponse(client.kpu)
- self.file_interpreter = resources.AsyncFileInterpreterWithStreamingResponse(client.file_interpreter)
+ self.capabilities = resources.AsyncCapabilitiesResourceWithStreamingResponse(client.capabilities)
+ self.models = resources.AsyncModelsResourceWithStreamingResponse(client.models)
+ self.kpu = resources.AsyncKpuResourceWithStreamingResponse(client.kpu)
+ self.file_interpreter = resources.AsyncFileInterpreterResourceWithStreamingResponse(client.file_interpreter)
Client = Maisa
diff --git a/src/maisa/resources/__init__.py b/src/maisa/resources/__init__.py
index 89db5456..bb8e26cd 100644
--- a/src/maisa/resources/__init__.py
+++ b/src/maisa/resources/__init__.py
@@ -1,61 +1,61 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from .kpu import (
- Kpu,
- AsyncKpu,
- KpuWithRawResponse,
- AsyncKpuWithRawResponse,
- KpuWithStreamingResponse,
- AsyncKpuWithStreamingResponse,
+ KpuResource,
+ AsyncKpuResource,
+ KpuResourceWithRawResponse,
+ AsyncKpuResourceWithRawResponse,
+ KpuResourceWithStreamingResponse,
+ AsyncKpuResourceWithStreamingResponse,
)
from .models import (
- Models,
- AsyncModels,
- ModelsWithRawResponse,
- AsyncModelsWithRawResponse,
- ModelsWithStreamingResponse,
- AsyncModelsWithStreamingResponse,
+ ModelsResource,
+ AsyncModelsResource,
+ ModelsResourceWithRawResponse,
+ AsyncModelsResourceWithRawResponse,
+ ModelsResourceWithStreamingResponse,
+ AsyncModelsResourceWithStreamingResponse,
)
from .capabilities import (
- Capabilities,
- AsyncCapabilities,
- CapabilitiesWithRawResponse,
- AsyncCapabilitiesWithRawResponse,
- CapabilitiesWithStreamingResponse,
- AsyncCapabilitiesWithStreamingResponse,
+ CapabilitiesResource,
+ AsyncCapabilitiesResource,
+ CapabilitiesResourceWithRawResponse,
+ AsyncCapabilitiesResourceWithRawResponse,
+ CapabilitiesResourceWithStreamingResponse,
+ AsyncCapabilitiesResourceWithStreamingResponse,
)
from .file_interpreter import (
- FileInterpreter,
- AsyncFileInterpreter,
- FileInterpreterWithRawResponse,
- AsyncFileInterpreterWithRawResponse,
- FileInterpreterWithStreamingResponse,
- AsyncFileInterpreterWithStreamingResponse,
+ FileInterpreterResource,
+ AsyncFileInterpreterResource,
+ FileInterpreterResourceWithRawResponse,
+ AsyncFileInterpreterResourceWithRawResponse,
+ FileInterpreterResourceWithStreamingResponse,
+ AsyncFileInterpreterResourceWithStreamingResponse,
)
__all__ = [
- "Capabilities",
- "AsyncCapabilities",
- "CapabilitiesWithRawResponse",
- "AsyncCapabilitiesWithRawResponse",
- "CapabilitiesWithStreamingResponse",
- "AsyncCapabilitiesWithStreamingResponse",
- "Models",
- "AsyncModels",
- "ModelsWithRawResponse",
- "AsyncModelsWithRawResponse",
- "ModelsWithStreamingResponse",
- "AsyncModelsWithStreamingResponse",
- "Kpu",
- "AsyncKpu",
- "KpuWithRawResponse",
- "AsyncKpuWithRawResponse",
- "KpuWithStreamingResponse",
- "AsyncKpuWithStreamingResponse",
- "FileInterpreter",
- "AsyncFileInterpreter",
- "FileInterpreterWithRawResponse",
- "AsyncFileInterpreterWithRawResponse",
- "FileInterpreterWithStreamingResponse",
- "AsyncFileInterpreterWithStreamingResponse",
+ "CapabilitiesResource",
+ "AsyncCapabilitiesResource",
+ "CapabilitiesResourceWithRawResponse",
+ "AsyncCapabilitiesResourceWithRawResponse",
+ "CapabilitiesResourceWithStreamingResponse",
+ "AsyncCapabilitiesResourceWithStreamingResponse",
+ "ModelsResource",
+ "AsyncModelsResource",
+ "ModelsResourceWithRawResponse",
+ "AsyncModelsResourceWithRawResponse",
+ "ModelsResourceWithStreamingResponse",
+ "AsyncModelsResourceWithStreamingResponse",
+ "KpuResource",
+ "AsyncKpuResource",
+ "KpuResourceWithRawResponse",
+ "AsyncKpuResourceWithRawResponse",
+ "KpuResourceWithStreamingResponse",
+ "AsyncKpuResourceWithStreamingResponse",
+ "FileInterpreterResource",
+ "AsyncFileInterpreterResource",
+ "FileInterpreterResourceWithRawResponse",
+ "AsyncFileInterpreterResourceWithRawResponse",
+ "FileInterpreterResourceWithStreamingResponse",
+ "AsyncFileInterpreterResourceWithStreamingResponse",
]
diff --git a/src/maisa/resources/capabilities/__init__.py b/src/maisa/resources/capabilities/__init__.py
index d2288b1c..3c8b0dac 100644
--- a/src/maisa/resources/capabilities/__init__.py
+++ b/src/maisa/resources/capabilities/__init__.py
@@ -1,33 +1,33 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from .media import (
- Media,
- AsyncMedia,
- MediaWithRawResponse,
- AsyncMediaWithRawResponse,
- MediaWithStreamingResponse,
- AsyncMediaWithStreamingResponse,
+ MediaResource,
+ AsyncMediaResource,
+ MediaResourceWithRawResponse,
+ AsyncMediaResourceWithRawResponse,
+ MediaResourceWithStreamingResponse,
+ AsyncMediaResourceWithStreamingResponse,
)
from .capabilities import (
- Capabilities,
- AsyncCapabilities,
- CapabilitiesWithRawResponse,
- AsyncCapabilitiesWithRawResponse,
- CapabilitiesWithStreamingResponse,
- AsyncCapabilitiesWithStreamingResponse,
+ CapabilitiesResource,
+ AsyncCapabilitiesResource,
+ CapabilitiesResourceWithRawResponse,
+ AsyncCapabilitiesResourceWithRawResponse,
+ CapabilitiesResourceWithStreamingResponse,
+ AsyncCapabilitiesResourceWithStreamingResponse,
)
__all__ = [
- "Media",
- "AsyncMedia",
- "MediaWithRawResponse",
- "AsyncMediaWithRawResponse",
- "MediaWithStreamingResponse",
- "AsyncMediaWithStreamingResponse",
- "Capabilities",
- "AsyncCapabilities",
- "CapabilitiesWithRawResponse",
- "AsyncCapabilitiesWithRawResponse",
- "CapabilitiesWithStreamingResponse",
- "AsyncCapabilitiesWithStreamingResponse",
+ "MediaResource",
+ "AsyncMediaResource",
+ "MediaResourceWithRawResponse",
+ "AsyncMediaResourceWithRawResponse",
+ "MediaResourceWithStreamingResponse",
+ "AsyncMediaResourceWithStreamingResponse",
+ "CapabilitiesResource",
+ "AsyncCapabilitiesResource",
+ "CapabilitiesResourceWithRawResponse",
+ "AsyncCapabilitiesResourceWithRawResponse",
+ "CapabilitiesResourceWithStreamingResponse",
+ "AsyncCapabilitiesResourceWithStreamingResponse",
]
diff --git a/src/maisa/resources/capabilities/capabilities.py b/src/maisa/resources/capabilities/capabilities.py
index e8ce4551..e8989be5 100644
--- a/src/maisa/resources/capabilities/capabilities.py
+++ b/src/maisa/resources/capabilities/capabilities.py
@@ -8,12 +8,12 @@
import httpx
from .media import (
- Media,
- AsyncMedia,
- MediaWithRawResponse,
- AsyncMediaWithRawResponse,
- MediaWithStreamingResponse,
- AsyncMediaWithStreamingResponse,
+ MediaResource,
+ AsyncMediaResource,
+ MediaResourceWithRawResponse,
+ AsyncMediaResourceWithRawResponse,
+ MediaResourceWithStreamingResponse,
+ AsyncMediaResourceWithStreamingResponse,
)
from ...types import capability_compare_params, capability_extract_params, capability_summarize_params
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
@@ -32,23 +32,25 @@
from ..._base_client import (
make_request_options,
)
-from ...types.shared import TextSummary, TextExtractor, TextComparator
+from ...types.shared.text_summary import TextSummary
+from ...types.shared.text_extractor import TextExtractor
+from ...types.shared.text_comparator import TextComparator
-__all__ = ["Capabilities", "AsyncCapabilities"]
+__all__ = ["CapabilitiesResource", "AsyncCapabilitiesResource"]
-class Capabilities(SyncAPIResource):
+class CapabilitiesResource(SyncAPIResource):
@cached_property
- def media(self) -> Media:
- return Media(self._client)
+ def media(self) -> MediaResource:
+ return MediaResource(self._client)
@cached_property
- def with_raw_response(self) -> CapabilitiesWithRawResponse:
- return CapabilitiesWithRawResponse(self)
+ def with_raw_response(self) -> CapabilitiesResourceWithRawResponse:
+ return CapabilitiesResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> CapabilitiesWithStreamingResponse:
- return CapabilitiesWithStreamingResponse(self)
+ def with_streaming_response(self) -> CapabilitiesResourceWithStreamingResponse:
+ return CapabilitiesResourceWithStreamingResponse(self)
def compare(
self,
@@ -215,18 +217,18 @@ def summarize(
)
-class AsyncCapabilities(AsyncAPIResource):
+class AsyncCapabilitiesResource(AsyncAPIResource):
@cached_property
- def media(self) -> AsyncMedia:
- return AsyncMedia(self._client)
+ def media(self) -> AsyncMediaResource:
+ return AsyncMediaResource(self._client)
@cached_property
- def with_raw_response(self) -> AsyncCapabilitiesWithRawResponse:
- return AsyncCapabilitiesWithRawResponse(self)
+ def with_raw_response(self) -> AsyncCapabilitiesResourceWithRawResponse:
+ return AsyncCapabilitiesResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> AsyncCapabilitiesWithStreamingResponse:
- return AsyncCapabilitiesWithStreamingResponse(self)
+ def with_streaming_response(self) -> AsyncCapabilitiesResourceWithStreamingResponse:
+ return AsyncCapabilitiesResourceWithStreamingResponse(self)
async def compare(
self,
@@ -393,8 +395,8 @@ async def summarize(
)
-class CapabilitiesWithRawResponse:
- def __init__(self, capabilities: Capabilities) -> None:
+class CapabilitiesResourceWithRawResponse:
+ def __init__(self, capabilities: CapabilitiesResource) -> None:
self._capabilities = capabilities
self.compare = to_raw_response_wrapper(
@@ -408,12 +410,12 @@ def __init__(self, capabilities: Capabilities) -> None:
)
@cached_property
- def media(self) -> MediaWithRawResponse:
- return MediaWithRawResponse(self._capabilities.media)
+ def media(self) -> MediaResourceWithRawResponse:
+ return MediaResourceWithRawResponse(self._capabilities.media)
-class AsyncCapabilitiesWithRawResponse:
- def __init__(self, capabilities: AsyncCapabilities) -> None:
+class AsyncCapabilitiesResourceWithRawResponse:
+ def __init__(self, capabilities: AsyncCapabilitiesResource) -> None:
self._capabilities = capabilities
self.compare = async_to_raw_response_wrapper(
@@ -427,12 +429,12 @@ def __init__(self, capabilities: AsyncCapabilities) -> None:
)
@cached_property
- def media(self) -> AsyncMediaWithRawResponse:
- return AsyncMediaWithRawResponse(self._capabilities.media)
+ def media(self) -> AsyncMediaResourceWithRawResponse:
+ return AsyncMediaResourceWithRawResponse(self._capabilities.media)
-class CapabilitiesWithStreamingResponse:
- def __init__(self, capabilities: Capabilities) -> None:
+class CapabilitiesResourceWithStreamingResponse:
+ def __init__(self, capabilities: CapabilitiesResource) -> None:
self._capabilities = capabilities
self.compare = to_streamed_response_wrapper(
@@ -446,12 +448,12 @@ def __init__(self, capabilities: Capabilities) -> None:
)
@cached_property
- def media(self) -> MediaWithStreamingResponse:
- return MediaWithStreamingResponse(self._capabilities.media)
+ def media(self) -> MediaResourceWithStreamingResponse:
+ return MediaResourceWithStreamingResponse(self._capabilities.media)
-class AsyncCapabilitiesWithStreamingResponse:
- def __init__(self, capabilities: AsyncCapabilities) -> None:
+class AsyncCapabilitiesResourceWithStreamingResponse:
+ def __init__(self, capabilities: AsyncCapabilitiesResource) -> None:
self._capabilities = capabilities
self.compare = async_to_streamed_response_wrapper(
@@ -465,5 +467,5 @@ def __init__(self, capabilities: AsyncCapabilities) -> None:
)
@cached_property
- def media(self) -> AsyncMediaWithStreamingResponse:
- return AsyncMediaWithStreamingResponse(self._capabilities.media)
+ def media(self) -> AsyncMediaResourceWithStreamingResponse:
+ return AsyncMediaResourceWithStreamingResponse(self._capabilities.media)
diff --git a/src/maisa/resources/capabilities/media.py b/src/maisa/resources/capabilities/media.py
index 1dcd2f56..04e41a0e 100644
--- a/src/maisa/resources/capabilities/media.py
+++ b/src/maisa/resources/capabilities/media.py
@@ -25,20 +25,22 @@
from ..._base_client import (
make_request_options,
)
-from ...types.shared import TextSummary, TextExtractor, TextComparator
from ...types.capabilities import media_compare_params, media_extract_params, media_summarize_params
+from ...types.shared.text_summary import TextSummary
+from ...types.shared.text_extractor import TextExtractor
+from ...types.shared.text_comparator import TextComparator
-__all__ = ["Media", "AsyncMedia"]
+__all__ = ["MediaResource", "AsyncMediaResource"]
-class Media(SyncAPIResource):
+class MediaResource(SyncAPIResource):
@cached_property
- def with_raw_response(self) -> MediaWithRawResponse:
- return MediaWithRawResponse(self)
+ def with_raw_response(self) -> MediaResourceWithRawResponse:
+ return MediaResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> MediaWithStreamingResponse:
- return MediaWithStreamingResponse(self)
+ def with_streaming_response(self) -> MediaResourceWithStreamingResponse:
+ return MediaResourceWithStreamingResponse(self)
def compare(
self,
@@ -311,14 +313,14 @@ def summarize(
)
-class AsyncMedia(AsyncAPIResource):
+class AsyncMediaResource(AsyncAPIResource):
@cached_property
- def with_raw_response(self) -> AsyncMediaWithRawResponse:
- return AsyncMediaWithRawResponse(self)
+ def with_raw_response(self) -> AsyncMediaResourceWithRawResponse:
+ return AsyncMediaResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> AsyncMediaWithStreamingResponse:
- return AsyncMediaWithStreamingResponse(self)
+ def with_streaming_response(self) -> AsyncMediaResourceWithStreamingResponse:
+ return AsyncMediaResourceWithStreamingResponse(self)
async def compare(
self,
@@ -591,8 +593,8 @@ async def summarize(
)
-class MediaWithRawResponse:
- def __init__(self, media: Media) -> None:
+class MediaResourceWithRawResponse:
+ def __init__(self, media: MediaResource) -> None:
self._media = media
self.compare = to_raw_response_wrapper(
@@ -606,8 +608,8 @@ def __init__(self, media: Media) -> None:
)
-class AsyncMediaWithRawResponse:
- def __init__(self, media: AsyncMedia) -> None:
+class AsyncMediaResourceWithRawResponse:
+ def __init__(self, media: AsyncMediaResource) -> None:
self._media = media
self.compare = async_to_raw_response_wrapper(
@@ -621,8 +623,8 @@ def __init__(self, media: AsyncMedia) -> None:
)
-class MediaWithStreamingResponse:
- def __init__(self, media: Media) -> None:
+class MediaResourceWithStreamingResponse:
+ def __init__(self, media: MediaResource) -> None:
self._media = media
self.compare = to_streamed_response_wrapper(
@@ -636,8 +638,8 @@ def __init__(self, media: Media) -> None:
)
-class AsyncMediaWithStreamingResponse:
- def __init__(self, media: AsyncMedia) -> None:
+class AsyncMediaResourceWithStreamingResponse:
+ def __init__(self, media: AsyncMediaResource) -> None:
self._media = media
self.compare = async_to_streamed_response_wrapper(
diff --git a/src/maisa/resources/file_interpreter/__init__.py b/src/maisa/resources/file_interpreter/__init__.py
index 59aecbec..f0210991 100644
--- a/src/maisa/resources/file_interpreter/__init__.py
+++ b/src/maisa/resources/file_interpreter/__init__.py
@@ -1,103 +1,89 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from .from_pdf import (
- FromPdf,
- AsyncFromPdf,
- FromPdfWithRawResponse,
- AsyncFromPdfWithRawResponse,
- FromPdfWithStreamingResponse,
- AsyncFromPdfWithStreamingResponse,
+ FromPdfResource,
+ AsyncFromPdfResource,
+ FromPdfResourceWithRawResponse,
+ AsyncFromPdfResourceWithRawResponse,
+ FromPdfResourceWithStreamingResponse,
+ AsyncFromPdfResourceWithStreamingResponse,
)
from .from_docx import (
- FromDocx,
- AsyncFromDocx,
- FromDocxWithRawResponse,
- AsyncFromDocxWithRawResponse,
- FromDocxWithStreamingResponse,
- AsyncFromDocxWithStreamingResponse,
+ FromDocxResource,
+ AsyncFromDocxResource,
+ FromDocxResourceWithRawResponse,
+ AsyncFromDocxResourceWithRawResponse,
+ FromDocxResourceWithStreamingResponse,
+ AsyncFromDocxResourceWithStreamingResponse,
)
from .from_html import (
- FromHTML,
- AsyncFromHTML,
- FromHTMLWithRawResponse,
- AsyncFromHTMLWithRawResponse,
- FromHTMLWithStreamingResponse,
- AsyncFromHTMLWithStreamingResponse,
+ FromHTMLResource,
+ AsyncFromHTMLResource,
+ FromHTMLResourceWithRawResponse,
+ AsyncFromHTMLResourceWithRawResponse,
+ FromHTMLResourceWithStreamingResponse,
+ AsyncFromHTMLResourceWithStreamingResponse,
)
from .from_audio import (
- FromAudio,
- AsyncFromAudio,
- FromAudioWithRawResponse,
- AsyncFromAudioWithRawResponse,
- FromAudioWithStreamingResponse,
- AsyncFromAudioWithStreamingResponse,
+ FromAudioResource,
+ AsyncFromAudioResource,
+ FromAudioResourceWithRawResponse,
+ AsyncFromAudioResourceWithRawResponse,
+ FromAudioResourceWithStreamingResponse,
+ AsyncFromAudioResourceWithStreamingResponse,
)
from .from_image import (
- FromImage,
- AsyncFromImage,
- FromImageWithRawResponse,
- AsyncFromImageWithRawResponse,
- FromImageWithStreamingResponse,
- AsyncFromImageWithStreamingResponse,
+ FromImageResource,
+ AsyncFromImageResource,
+ FromImageResourceWithRawResponse,
+ AsyncFromImageResourceWithRawResponse,
+ FromImageResourceWithStreamingResponse,
+ AsyncFromImageResourceWithStreamingResponse,
)
from .file_interpreter import (
- FileInterpreter,
- AsyncFileInterpreter,
- FileInterpreterWithRawResponse,
- AsyncFileInterpreterWithRawResponse,
- FileInterpreterWithStreamingResponse,
- AsyncFileInterpreterWithStreamingResponse,
-)
-from .from_pdf_scanned import (
- FromPdfScanned,
- AsyncFromPdfScanned,
- FromPdfScannedWithRawResponse,
- AsyncFromPdfScannedWithRawResponse,
- FromPdfScannedWithStreamingResponse,
- AsyncFromPdfScannedWithStreamingResponse,
+ FileInterpreterResource,
+ AsyncFileInterpreterResource,
+ FileInterpreterResourceWithRawResponse,
+ AsyncFileInterpreterResourceWithRawResponse,
+ FileInterpreterResourceWithStreamingResponse,
+ AsyncFileInterpreterResourceWithStreamingResponse,
)
__all__ = [
- "FromPdf",
- "AsyncFromPdf",
- "FromPdfWithRawResponse",
- "AsyncFromPdfWithRawResponse",
- "FromPdfWithStreamingResponse",
- "AsyncFromPdfWithStreamingResponse",
- "FromPdfScanned",
- "AsyncFromPdfScanned",
- "FromPdfScannedWithRawResponse",
- "AsyncFromPdfScannedWithRawResponse",
- "FromPdfScannedWithStreamingResponse",
- "AsyncFromPdfScannedWithStreamingResponse",
- "FromDocx",
- "AsyncFromDocx",
- "FromDocxWithRawResponse",
- "AsyncFromDocxWithRawResponse",
- "FromDocxWithStreamingResponse",
- "AsyncFromDocxWithStreamingResponse",
- "FromHTML",
- "AsyncFromHTML",
- "FromHTMLWithRawResponse",
- "AsyncFromHTMLWithRawResponse",
- "FromHTMLWithStreamingResponse",
- "AsyncFromHTMLWithStreamingResponse",
- "FromImage",
- "AsyncFromImage",
- "FromImageWithRawResponse",
- "AsyncFromImageWithRawResponse",
- "FromImageWithStreamingResponse",
- "AsyncFromImageWithStreamingResponse",
- "FromAudio",
- "AsyncFromAudio",
- "FromAudioWithRawResponse",
- "AsyncFromAudioWithRawResponse",
- "FromAudioWithStreamingResponse",
- "AsyncFromAudioWithStreamingResponse",
- "FileInterpreter",
- "AsyncFileInterpreter",
- "FileInterpreterWithRawResponse",
- "AsyncFileInterpreterWithRawResponse",
- "FileInterpreterWithStreamingResponse",
- "AsyncFileInterpreterWithStreamingResponse",
+ "FromPdfResource",
+ "AsyncFromPdfResource",
+ "FromPdfResourceWithRawResponse",
+ "AsyncFromPdfResourceWithRawResponse",
+ "FromPdfResourceWithStreamingResponse",
+ "AsyncFromPdfResourceWithStreamingResponse",
+ "FromDocxResource",
+ "AsyncFromDocxResource",
+ "FromDocxResourceWithRawResponse",
+ "AsyncFromDocxResourceWithRawResponse",
+ "FromDocxResourceWithStreamingResponse",
+ "AsyncFromDocxResourceWithStreamingResponse",
+ "FromHTMLResource",
+ "AsyncFromHTMLResource",
+ "FromHTMLResourceWithRawResponse",
+ "AsyncFromHTMLResourceWithRawResponse",
+ "FromHTMLResourceWithStreamingResponse",
+ "AsyncFromHTMLResourceWithStreamingResponse",
+ "FromImageResource",
+ "AsyncFromImageResource",
+ "FromImageResourceWithRawResponse",
+ "AsyncFromImageResourceWithRawResponse",
+ "FromImageResourceWithStreamingResponse",
+ "AsyncFromImageResourceWithStreamingResponse",
+ "FromAudioResource",
+ "AsyncFromAudioResource",
+ "FromAudioResourceWithRawResponse",
+ "AsyncFromAudioResourceWithRawResponse",
+ "FromAudioResourceWithStreamingResponse",
+ "AsyncFromAudioResourceWithStreamingResponse",
+ "FileInterpreterResource",
+ "AsyncFileInterpreterResource",
+ "FileInterpreterResourceWithRawResponse",
+ "AsyncFileInterpreterResourceWithRawResponse",
+ "FileInterpreterResourceWithStreamingResponse",
+ "AsyncFileInterpreterResourceWithStreamingResponse",
]
diff --git a/src/maisa/resources/file_interpreter/file_interpreter.py b/src/maisa/resources/file_interpreter/file_interpreter.py
index 30b61e8f..f94c2a6e 100644
--- a/src/maisa/resources/file_interpreter/file_interpreter.py
+++ b/src/maisa/resources/file_interpreter/file_interpreter.py
@@ -3,238 +3,206 @@
from __future__ import annotations
from .from_pdf import (
- FromPdf,
- AsyncFromPdf,
- FromPdfWithRawResponse,
- AsyncFromPdfWithRawResponse,
- FromPdfWithStreamingResponse,
- AsyncFromPdfWithStreamingResponse,
+ FromPdfResource,
+ AsyncFromPdfResource,
+ FromPdfResourceWithRawResponse,
+ AsyncFromPdfResourceWithRawResponse,
+ FromPdfResourceWithStreamingResponse,
+ AsyncFromPdfResourceWithStreamingResponse,
)
from ..._compat import cached_property
from .from_docx import (
- FromDocx,
- AsyncFromDocx,
- FromDocxWithRawResponse,
- AsyncFromDocxWithRawResponse,
- FromDocxWithStreamingResponse,
- AsyncFromDocxWithStreamingResponse,
+ FromDocxResource,
+ AsyncFromDocxResource,
+ FromDocxResourceWithRawResponse,
+ AsyncFromDocxResourceWithRawResponse,
+ FromDocxResourceWithStreamingResponse,
+ AsyncFromDocxResourceWithStreamingResponse,
)
from .from_html import (
- FromHTML,
- AsyncFromHTML,
- FromHTMLWithRawResponse,
- AsyncFromHTMLWithRawResponse,
- FromHTMLWithStreamingResponse,
- AsyncFromHTMLWithStreamingResponse,
+ FromHTMLResource,
+ AsyncFromHTMLResource,
+ FromHTMLResourceWithRawResponse,
+ AsyncFromHTMLResourceWithRawResponse,
+ FromHTMLResourceWithStreamingResponse,
+ AsyncFromHTMLResourceWithStreamingResponse,
)
from .from_audio import (
- FromAudio,
- AsyncFromAudio,
- FromAudioWithRawResponse,
- AsyncFromAudioWithRawResponse,
- FromAudioWithStreamingResponse,
- AsyncFromAudioWithStreamingResponse,
+ FromAudioResource,
+ AsyncFromAudioResource,
+ FromAudioResourceWithRawResponse,
+ AsyncFromAudioResourceWithRawResponse,
+ FromAudioResourceWithStreamingResponse,
+ AsyncFromAudioResourceWithStreamingResponse,
)
from .from_image import (
- FromImage,
- AsyncFromImage,
- FromImageWithRawResponse,
- AsyncFromImageWithRawResponse,
- FromImageWithStreamingResponse,
- AsyncFromImageWithStreamingResponse,
+ FromImageResource,
+ AsyncFromImageResource,
+ FromImageResourceWithRawResponse,
+ AsyncFromImageResourceWithRawResponse,
+ FromImageResourceWithStreamingResponse,
+ AsyncFromImageResourceWithStreamingResponse,
)
from ..._resource import SyncAPIResource, AsyncAPIResource
-from .from_pdf_scanned import (
- FromPdfScanned,
- AsyncFromPdfScanned,
- FromPdfScannedWithRawResponse,
- AsyncFromPdfScannedWithRawResponse,
- FromPdfScannedWithStreamingResponse,
- AsyncFromPdfScannedWithStreamingResponse,
-)
-
-__all__ = ["FileInterpreter", "AsyncFileInterpreter"]
+__all__ = ["FileInterpreterResource", "AsyncFileInterpreterResource"]
-class FileInterpreter(SyncAPIResource):
- @cached_property
- def from_pdf(self) -> FromPdf:
- return FromPdf(self._client)
+class FileInterpreterResource(SyncAPIResource):
@cached_property
- def from_pdf_scanned(self) -> FromPdfScanned:
- return FromPdfScanned(self._client)
+ def from_pdf(self) -> FromPdfResource:
+ return FromPdfResource(self._client)
@cached_property
- def from_docx(self) -> FromDocx:
- return FromDocx(self._client)
+ def from_docx(self) -> FromDocxResource:
+ return FromDocxResource(self._client)
@cached_property
- def from_html(self) -> FromHTML:
- return FromHTML(self._client)
+ def from_html(self) -> FromHTMLResource:
+ return FromHTMLResource(self._client)
@cached_property
- def from_image(self) -> FromImage:
- return FromImage(self._client)
+ def from_image(self) -> FromImageResource:
+ return FromImageResource(self._client)
@cached_property
- def from_audio(self) -> FromAudio:
- return FromAudio(self._client)
+ def from_audio(self) -> FromAudioResource:
+ return FromAudioResource(self._client)
@cached_property
- def with_raw_response(self) -> FileInterpreterWithRawResponse:
- return FileInterpreterWithRawResponse(self)
+ def with_raw_response(self) -> FileInterpreterResourceWithRawResponse:
+ return FileInterpreterResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> FileInterpreterWithStreamingResponse:
- return FileInterpreterWithStreamingResponse(self)
+ def with_streaming_response(self) -> FileInterpreterResourceWithStreamingResponse:
+ return FileInterpreterResourceWithStreamingResponse(self)
-class AsyncFileInterpreter(AsyncAPIResource):
+class AsyncFileInterpreterResource(AsyncAPIResource):
@cached_property
- def from_pdf(self) -> AsyncFromPdf:
- return AsyncFromPdf(self._client)
+ def from_pdf(self) -> AsyncFromPdfResource:
+ return AsyncFromPdfResource(self._client)
@cached_property
- def from_pdf_scanned(self) -> AsyncFromPdfScanned:
- return AsyncFromPdfScanned(self._client)
+ def from_docx(self) -> AsyncFromDocxResource:
+ return AsyncFromDocxResource(self._client)
@cached_property
- def from_docx(self) -> AsyncFromDocx:
- return AsyncFromDocx(self._client)
+ def from_html(self) -> AsyncFromHTMLResource:
+ return AsyncFromHTMLResource(self._client)
@cached_property
- def from_html(self) -> AsyncFromHTML:
- return AsyncFromHTML(self._client)
+ def from_image(self) -> AsyncFromImageResource:
+ return AsyncFromImageResource(self._client)
@cached_property
- def from_image(self) -> AsyncFromImage:
- return AsyncFromImage(self._client)
+ def from_audio(self) -> AsyncFromAudioResource:
+ return AsyncFromAudioResource(self._client)
@cached_property
- def from_audio(self) -> AsyncFromAudio:
- return AsyncFromAudio(self._client)
+ def with_raw_response(self) -> AsyncFileInterpreterResourceWithRawResponse:
+ return AsyncFileInterpreterResourceWithRawResponse(self)
@cached_property
- def with_raw_response(self) -> AsyncFileInterpreterWithRawResponse:
- return AsyncFileInterpreterWithRawResponse(self)
+ def with_streaming_response(self) -> AsyncFileInterpreterResourceWithStreamingResponse:
+ return AsyncFileInterpreterResourceWithStreamingResponse(self)
- @cached_property
- def with_streaming_response(self) -> AsyncFileInterpreterWithStreamingResponse:
- return AsyncFileInterpreterWithStreamingResponse(self)
-
-class FileInterpreterWithRawResponse:
- def __init__(self, file_interpreter: FileInterpreter) -> None:
+class FileInterpreterResourceWithRawResponse:
+ def __init__(self, file_interpreter: FileInterpreterResource) -> None:
self._file_interpreter = file_interpreter
@cached_property
- def from_pdf(self) -> FromPdfWithRawResponse:
- return FromPdfWithRawResponse(self._file_interpreter.from_pdf)
-
- @cached_property
- def from_pdf_scanned(self) -> FromPdfScannedWithRawResponse:
- return FromPdfScannedWithRawResponse(self._file_interpreter.from_pdf_scanned)
+ def from_pdf(self) -> FromPdfResourceWithRawResponse:
+ return FromPdfResourceWithRawResponse(self._file_interpreter.from_pdf)
@cached_property
- def from_docx(self) -> FromDocxWithRawResponse:
- return FromDocxWithRawResponse(self._file_interpreter.from_docx)
+ def from_docx(self) -> FromDocxResourceWithRawResponse:
+ return FromDocxResourceWithRawResponse(self._file_interpreter.from_docx)
@cached_property
- def from_html(self) -> FromHTMLWithRawResponse:
- return FromHTMLWithRawResponse(self._file_interpreter.from_html)
+ def from_html(self) -> FromHTMLResourceWithRawResponse:
+ return FromHTMLResourceWithRawResponse(self._file_interpreter.from_html)
@cached_property
- def from_image(self) -> FromImageWithRawResponse:
- return FromImageWithRawResponse(self._file_interpreter.from_image)
+ def from_image(self) -> FromImageResourceWithRawResponse:
+ return FromImageResourceWithRawResponse(self._file_interpreter.from_image)
@cached_property
- def from_audio(self) -> FromAudioWithRawResponse:
- return FromAudioWithRawResponse(self._file_interpreter.from_audio)
+ def from_audio(self) -> FromAudioResourceWithRawResponse:
+ return FromAudioResourceWithRawResponse(self._file_interpreter.from_audio)
-class AsyncFileInterpreterWithRawResponse:
- def __init__(self, file_interpreter: AsyncFileInterpreter) -> None:
+class AsyncFileInterpreterResourceWithRawResponse:
+ def __init__(self, file_interpreter: AsyncFileInterpreterResource) -> None:
self._file_interpreter = file_interpreter
@cached_property
- def from_pdf(self) -> AsyncFromPdfWithRawResponse:
- return AsyncFromPdfWithRawResponse(self._file_interpreter.from_pdf)
+ def from_pdf(self) -> AsyncFromPdfResourceWithRawResponse:
+ return AsyncFromPdfResourceWithRawResponse(self._file_interpreter.from_pdf)
@cached_property
- def from_pdf_scanned(self) -> AsyncFromPdfScannedWithRawResponse:
- return AsyncFromPdfScannedWithRawResponse(self._file_interpreter.from_pdf_scanned)
+ def from_docx(self) -> AsyncFromDocxResourceWithRawResponse:
+ return AsyncFromDocxResourceWithRawResponse(self._file_interpreter.from_docx)
@cached_property
- def from_docx(self) -> AsyncFromDocxWithRawResponse:
- return AsyncFromDocxWithRawResponse(self._file_interpreter.from_docx)
+ def from_html(self) -> AsyncFromHTMLResourceWithRawResponse:
+ return AsyncFromHTMLResourceWithRawResponse(self._file_interpreter.from_html)
@cached_property
- def from_html(self) -> AsyncFromHTMLWithRawResponse:
- return AsyncFromHTMLWithRawResponse(self._file_interpreter.from_html)
+ def from_image(self) -> AsyncFromImageResourceWithRawResponse:
+ return AsyncFromImageResourceWithRawResponse(self._file_interpreter.from_image)
@cached_property
- def from_image(self) -> AsyncFromImageWithRawResponse:
- return AsyncFromImageWithRawResponse(self._file_interpreter.from_image)
-
- @cached_property
- def from_audio(self) -> AsyncFromAudioWithRawResponse:
- return AsyncFromAudioWithRawResponse(self._file_interpreter.from_audio)
+ def from_audio(self) -> AsyncFromAudioResourceWithRawResponse:
+ return AsyncFromAudioResourceWithRawResponse(self._file_interpreter.from_audio)
-class FileInterpreterWithStreamingResponse:
- def __init__(self, file_interpreter: FileInterpreter) -> None:
+class FileInterpreterResourceWithStreamingResponse:
+ def __init__(self, file_interpreter: FileInterpreterResource) -> None:
self._file_interpreter = file_interpreter
@cached_property
- def from_pdf(self) -> FromPdfWithStreamingResponse:
- return FromPdfWithStreamingResponse(self._file_interpreter.from_pdf)
-
- @cached_property
- def from_pdf_scanned(self) -> FromPdfScannedWithStreamingResponse:
- return FromPdfScannedWithStreamingResponse(self._file_interpreter.from_pdf_scanned)
+ def from_pdf(self) -> FromPdfResourceWithStreamingResponse:
+ return FromPdfResourceWithStreamingResponse(self._file_interpreter.from_pdf)
@cached_property
- def from_docx(self) -> FromDocxWithStreamingResponse:
- return FromDocxWithStreamingResponse(self._file_interpreter.from_docx)
+ def from_docx(self) -> FromDocxResourceWithStreamingResponse:
+ return FromDocxResourceWithStreamingResponse(self._file_interpreter.from_docx)
@cached_property
- def from_html(self) -> FromHTMLWithStreamingResponse:
- return FromHTMLWithStreamingResponse(self._file_interpreter.from_html)
+ def from_html(self) -> FromHTMLResourceWithStreamingResponse:
+ return FromHTMLResourceWithStreamingResponse(self._file_interpreter.from_html)
@cached_property
- def from_image(self) -> FromImageWithStreamingResponse:
- return FromImageWithStreamingResponse(self._file_interpreter.from_image)
+ def from_image(self) -> FromImageResourceWithStreamingResponse:
+ return FromImageResourceWithStreamingResponse(self._file_interpreter.from_image)
@cached_property
- def from_audio(self) -> FromAudioWithStreamingResponse:
- return FromAudioWithStreamingResponse(self._file_interpreter.from_audio)
+ def from_audio(self) -> FromAudioResourceWithStreamingResponse:
+ return FromAudioResourceWithStreamingResponse(self._file_interpreter.from_audio)
-class AsyncFileInterpreterWithStreamingResponse:
- def __init__(self, file_interpreter: AsyncFileInterpreter) -> None:
+class AsyncFileInterpreterResourceWithStreamingResponse:
+ def __init__(self, file_interpreter: AsyncFileInterpreterResource) -> None:
self._file_interpreter = file_interpreter
@cached_property
- def from_pdf(self) -> AsyncFromPdfWithStreamingResponse:
- return AsyncFromPdfWithStreamingResponse(self._file_interpreter.from_pdf)
-
- @cached_property
- def from_pdf_scanned(self) -> AsyncFromPdfScannedWithStreamingResponse:
- return AsyncFromPdfScannedWithStreamingResponse(self._file_interpreter.from_pdf_scanned)
+ def from_pdf(self) -> AsyncFromPdfResourceWithStreamingResponse:
+ return AsyncFromPdfResourceWithStreamingResponse(self._file_interpreter.from_pdf)
@cached_property
- def from_docx(self) -> AsyncFromDocxWithStreamingResponse:
- return AsyncFromDocxWithStreamingResponse(self._file_interpreter.from_docx)
+ def from_docx(self) -> AsyncFromDocxResourceWithStreamingResponse:
+ return AsyncFromDocxResourceWithStreamingResponse(self._file_interpreter.from_docx)
@cached_property
- def from_html(self) -> AsyncFromHTMLWithStreamingResponse:
- return AsyncFromHTMLWithStreamingResponse(self._file_interpreter.from_html)
+ def from_html(self) -> AsyncFromHTMLResourceWithStreamingResponse:
+ return AsyncFromHTMLResourceWithStreamingResponse(self._file_interpreter.from_html)
@cached_property
- def from_image(self) -> AsyncFromImageWithStreamingResponse:
- return AsyncFromImageWithStreamingResponse(self._file_interpreter.from_image)
+ def from_image(self) -> AsyncFromImageResourceWithStreamingResponse:
+ return AsyncFromImageResourceWithStreamingResponse(self._file_interpreter.from_image)
@cached_property
- def from_audio(self) -> AsyncFromAudioWithStreamingResponse:
- return AsyncFromAudioWithStreamingResponse(self._file_interpreter.from_audio)
+ def from_audio(self) -> AsyncFromAudioResourceWithStreamingResponse:
+ return AsyncFromAudioResourceWithStreamingResponse(self._file_interpreter.from_audio)
diff --git a/src/maisa/resources/file_interpreter/from_audio.py b/src/maisa/resources/file_interpreter/from_audio.py
index f73df7e8..96399bcd 100644
--- a/src/maisa/resources/file_interpreter/from_audio.py
+++ b/src/maisa/resources/file_interpreter/from_audio.py
@@ -26,17 +26,17 @@
)
from ...types.file_interpreter import from_audio_create_params
-__all__ = ["FromAudio", "AsyncFromAudio"]
+__all__ = ["FromAudioResource", "AsyncFromAudioResource"]
-class FromAudio(SyncAPIResource):
+class FromAudioResource(SyncAPIResource):
@cached_property
- def with_raw_response(self) -> FromAudioWithRawResponse:
- return FromAudioWithRawResponse(self)
+ def with_raw_response(self) -> FromAudioResourceWithRawResponse:
+ return FromAudioResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> FromAudioWithStreamingResponse:
- return FromAudioWithStreamingResponse(self)
+ def with_streaming_response(self) -> FromAudioResourceWithStreamingResponse:
+ return FromAudioResourceWithStreamingResponse(self)
def create(
self,
@@ -79,14 +79,14 @@ def create(
)
-class AsyncFromAudio(AsyncAPIResource):
+class AsyncFromAudioResource(AsyncAPIResource):
@cached_property
- def with_raw_response(self) -> AsyncFromAudioWithRawResponse:
- return AsyncFromAudioWithRawResponse(self)
+ def with_raw_response(self) -> AsyncFromAudioResourceWithRawResponse:
+ return AsyncFromAudioResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> AsyncFromAudioWithStreamingResponse:
- return AsyncFromAudioWithStreamingResponse(self)
+ def with_streaming_response(self) -> AsyncFromAudioResourceWithStreamingResponse:
+ return AsyncFromAudioResourceWithStreamingResponse(self)
async def create(
self,
@@ -129,8 +129,8 @@ async def create(
)
-class FromAudioWithRawResponse:
- def __init__(self, from_audio: FromAudio) -> None:
+class FromAudioResourceWithRawResponse:
+ def __init__(self, from_audio: FromAudioResource) -> None:
self._from_audio = from_audio
self.create = to_raw_response_wrapper(
@@ -138,8 +138,8 @@ def __init__(self, from_audio: FromAudio) -> None:
)
-class AsyncFromAudioWithRawResponse:
- def __init__(self, from_audio: AsyncFromAudio) -> None:
+class AsyncFromAudioResourceWithRawResponse:
+ def __init__(self, from_audio: AsyncFromAudioResource) -> None:
self._from_audio = from_audio
self.create = async_to_raw_response_wrapper(
@@ -147,8 +147,8 @@ def __init__(self, from_audio: AsyncFromAudio) -> None:
)
-class FromAudioWithStreamingResponse:
- def __init__(self, from_audio: FromAudio) -> None:
+class FromAudioResourceWithStreamingResponse:
+ def __init__(self, from_audio: FromAudioResource) -> None:
self._from_audio = from_audio
self.create = to_streamed_response_wrapper(
@@ -156,8 +156,8 @@ def __init__(self, from_audio: FromAudio) -> None:
)
-class AsyncFromAudioWithStreamingResponse:
- def __init__(self, from_audio: AsyncFromAudio) -> None:
+class AsyncFromAudioResourceWithStreamingResponse:
+ def __init__(self, from_audio: AsyncFromAudioResource) -> None:
self._from_audio = from_audio
self.create = async_to_streamed_response_wrapper(
diff --git a/src/maisa/resources/file_interpreter/from_docx.py b/src/maisa/resources/file_interpreter/from_docx.py
index 016a0a8e..eec6adaf 100644
--- a/src/maisa/resources/file_interpreter/from_docx.py
+++ b/src/maisa/resources/file_interpreter/from_docx.py
@@ -26,17 +26,17 @@
)
from ...types.file_interpreter import from_docx_create_params
-__all__ = ["FromDocx", "AsyncFromDocx"]
+__all__ = ["FromDocxResource", "AsyncFromDocxResource"]
-class FromDocx(SyncAPIResource):
+class FromDocxResource(SyncAPIResource):
@cached_property
- def with_raw_response(self) -> FromDocxWithRawResponse:
- return FromDocxWithRawResponse(self)
+ def with_raw_response(self) -> FromDocxResourceWithRawResponse:
+ return FromDocxResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> FromDocxWithStreamingResponse:
- return FromDocxWithStreamingResponse(self)
+ def with_streaming_response(self) -> FromDocxResourceWithStreamingResponse:
+ return FromDocxResourceWithStreamingResponse(self)
def create(
self,
@@ -79,14 +79,14 @@ def create(
)
-class AsyncFromDocx(AsyncAPIResource):
+class AsyncFromDocxResource(AsyncAPIResource):
@cached_property
- def with_raw_response(self) -> AsyncFromDocxWithRawResponse:
- return AsyncFromDocxWithRawResponse(self)
+ def with_raw_response(self) -> AsyncFromDocxResourceWithRawResponse:
+ return AsyncFromDocxResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> AsyncFromDocxWithStreamingResponse:
- return AsyncFromDocxWithStreamingResponse(self)
+ def with_streaming_response(self) -> AsyncFromDocxResourceWithStreamingResponse:
+ return AsyncFromDocxResourceWithStreamingResponse(self)
async def create(
self,
@@ -129,8 +129,8 @@ async def create(
)
-class FromDocxWithRawResponse:
- def __init__(self, from_docx: FromDocx) -> None:
+class FromDocxResourceWithRawResponse:
+ def __init__(self, from_docx: FromDocxResource) -> None:
self._from_docx = from_docx
self.create = to_raw_response_wrapper(
@@ -138,8 +138,8 @@ def __init__(self, from_docx: FromDocx) -> None:
)
-class AsyncFromDocxWithRawResponse:
- def __init__(self, from_docx: AsyncFromDocx) -> None:
+class AsyncFromDocxResourceWithRawResponse:
+ def __init__(self, from_docx: AsyncFromDocxResource) -> None:
self._from_docx = from_docx
self.create = async_to_raw_response_wrapper(
@@ -147,8 +147,8 @@ def __init__(self, from_docx: AsyncFromDocx) -> None:
)
-class FromDocxWithStreamingResponse:
- def __init__(self, from_docx: FromDocx) -> None:
+class FromDocxResourceWithStreamingResponse:
+ def __init__(self, from_docx: FromDocxResource) -> None:
self._from_docx = from_docx
self.create = to_streamed_response_wrapper(
@@ -156,8 +156,8 @@ def __init__(self, from_docx: FromDocx) -> None:
)
-class AsyncFromDocxWithStreamingResponse:
- def __init__(self, from_docx: AsyncFromDocx) -> None:
+class AsyncFromDocxResourceWithStreamingResponse:
+ def __init__(self, from_docx: AsyncFromDocxResource) -> None:
self._from_docx = from_docx
self.create = async_to_streamed_response_wrapper(
diff --git a/src/maisa/resources/file_interpreter/from_html.py b/src/maisa/resources/file_interpreter/from_html.py
index 22afd81e..166bb9a6 100644
--- a/src/maisa/resources/file_interpreter/from_html.py
+++ b/src/maisa/resources/file_interpreter/from_html.py
@@ -26,17 +26,17 @@
)
from ...types.file_interpreter import from_html_create_params
-__all__ = ["FromHTML", "AsyncFromHTML"]
+__all__ = ["FromHTMLResource", "AsyncFromHTMLResource"]
-class FromHTML(SyncAPIResource):
+class FromHTMLResource(SyncAPIResource):
@cached_property
- def with_raw_response(self) -> FromHTMLWithRawResponse:
- return FromHTMLWithRawResponse(self)
+ def with_raw_response(self) -> FromHTMLResourceWithRawResponse:
+ return FromHTMLResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> FromHTMLWithStreamingResponse:
- return FromHTMLWithStreamingResponse(self)
+ def with_streaming_response(self) -> FromHTMLResourceWithStreamingResponse:
+ return FromHTMLResourceWithStreamingResponse(self)
def create(
self,
@@ -79,14 +79,14 @@ def create(
)
-class AsyncFromHTML(AsyncAPIResource):
+class AsyncFromHTMLResource(AsyncAPIResource):
@cached_property
- def with_raw_response(self) -> AsyncFromHTMLWithRawResponse:
- return AsyncFromHTMLWithRawResponse(self)
+ def with_raw_response(self) -> AsyncFromHTMLResourceWithRawResponse:
+ return AsyncFromHTMLResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> AsyncFromHTMLWithStreamingResponse:
- return AsyncFromHTMLWithStreamingResponse(self)
+ def with_streaming_response(self) -> AsyncFromHTMLResourceWithStreamingResponse:
+ return AsyncFromHTMLResourceWithStreamingResponse(self)
async def create(
self,
@@ -129,8 +129,8 @@ async def create(
)
-class FromHTMLWithRawResponse:
- def __init__(self, from_html: FromHTML) -> None:
+class FromHTMLResourceWithRawResponse:
+ def __init__(self, from_html: FromHTMLResource) -> None:
self._from_html = from_html
self.create = to_raw_response_wrapper(
@@ -138,8 +138,8 @@ def __init__(self, from_html: FromHTML) -> None:
)
-class AsyncFromHTMLWithRawResponse:
- def __init__(self, from_html: AsyncFromHTML) -> None:
+class AsyncFromHTMLResourceWithRawResponse:
+ def __init__(self, from_html: AsyncFromHTMLResource) -> None:
self._from_html = from_html
self.create = async_to_raw_response_wrapper(
@@ -147,8 +147,8 @@ def __init__(self, from_html: AsyncFromHTML) -> None:
)
-class FromHTMLWithStreamingResponse:
- def __init__(self, from_html: FromHTML) -> None:
+class FromHTMLResourceWithStreamingResponse:
+ def __init__(self, from_html: FromHTMLResource) -> None:
self._from_html = from_html
self.create = to_streamed_response_wrapper(
@@ -156,8 +156,8 @@ def __init__(self, from_html: FromHTML) -> None:
)
-class AsyncFromHTMLWithStreamingResponse:
- def __init__(self, from_html: AsyncFromHTML) -> None:
+class AsyncFromHTMLResourceWithStreamingResponse:
+ def __init__(self, from_html: AsyncFromHTMLResource) -> None:
self._from_html = from_html
self.create = async_to_streamed_response_wrapper(
diff --git a/src/maisa/resources/file_interpreter/from_image.py b/src/maisa/resources/file_interpreter/from_image.py
index 842ee12b..a35ab876 100644
--- a/src/maisa/resources/file_interpreter/from_image.py
+++ b/src/maisa/resources/file_interpreter/from_image.py
@@ -26,17 +26,17 @@
)
from ...types.file_interpreter import from_image_create_params
-__all__ = ["FromImage", "AsyncFromImage"]
+__all__ = ["FromImageResource", "AsyncFromImageResource"]
-class FromImage(SyncAPIResource):
+class FromImageResource(SyncAPIResource):
@cached_property
- def with_raw_response(self) -> FromImageWithRawResponse:
- return FromImageWithRawResponse(self)
+ def with_raw_response(self) -> FromImageResourceWithRawResponse:
+ return FromImageResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> FromImageWithStreamingResponse:
- return FromImageWithStreamingResponse(self)
+ def with_streaming_response(self) -> FromImageResourceWithStreamingResponse:
+ return FromImageResourceWithStreamingResponse(self)
def create(
self,
@@ -79,14 +79,14 @@ def create(
)
-class AsyncFromImage(AsyncAPIResource):
+class AsyncFromImageResource(AsyncAPIResource):
@cached_property
- def with_raw_response(self) -> AsyncFromImageWithRawResponse:
- return AsyncFromImageWithRawResponse(self)
+ def with_raw_response(self) -> AsyncFromImageResourceWithRawResponse:
+ return AsyncFromImageResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> AsyncFromImageWithStreamingResponse:
- return AsyncFromImageWithStreamingResponse(self)
+ def with_streaming_response(self) -> AsyncFromImageResourceWithStreamingResponse:
+ return AsyncFromImageResourceWithStreamingResponse(self)
async def create(
self,
@@ -129,8 +129,8 @@ async def create(
)
-class FromImageWithRawResponse:
- def __init__(self, from_image: FromImage) -> None:
+class FromImageResourceWithRawResponse:
+ def __init__(self, from_image: FromImageResource) -> None:
self._from_image = from_image
self.create = to_raw_response_wrapper(
@@ -138,8 +138,8 @@ def __init__(self, from_image: FromImage) -> None:
)
-class AsyncFromImageWithRawResponse:
- def __init__(self, from_image: AsyncFromImage) -> None:
+class AsyncFromImageResourceWithRawResponse:
+ def __init__(self, from_image: AsyncFromImageResource) -> None:
self._from_image = from_image
self.create = async_to_raw_response_wrapper(
@@ -147,8 +147,8 @@ def __init__(self, from_image: AsyncFromImage) -> None:
)
-class FromImageWithStreamingResponse:
- def __init__(self, from_image: FromImage) -> None:
+class FromImageResourceWithStreamingResponse:
+ def __init__(self, from_image: FromImageResource) -> None:
self._from_image = from_image
self.create = to_streamed_response_wrapper(
@@ -156,8 +156,8 @@ def __init__(self, from_image: FromImage) -> None:
)
-class AsyncFromImageWithStreamingResponse:
- def __init__(self, from_image: AsyncFromImage) -> None:
+class AsyncFromImageResourceWithStreamingResponse:
+ def __init__(self, from_image: AsyncFromImageResource) -> None:
self._from_image = from_image
self.create = async_to_streamed_response_wrapper(
diff --git a/src/maisa/resources/file_interpreter/from_pdf.py b/src/maisa/resources/file_interpreter/from_pdf.py
index 03a9ad1c..9889625a 100644
--- a/src/maisa/resources/file_interpreter/from_pdf.py
+++ b/src/maisa/resources/file_interpreter/from_pdf.py
@@ -26,17 +26,17 @@
)
from ...types.file_interpreter import from_pdf_create_params
-__all__ = ["FromPdf", "AsyncFromPdf"]
+__all__ = ["FromPdfResource", "AsyncFromPdfResource"]
-class FromPdf(SyncAPIResource):
+class FromPdfResource(SyncAPIResource):
@cached_property
- def with_raw_response(self) -> FromPdfWithRawResponse:
- return FromPdfWithRawResponse(self)
+ def with_raw_response(self) -> FromPdfResourceWithRawResponse:
+ return FromPdfResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> FromPdfWithStreamingResponse:
- return FromPdfWithStreamingResponse(self)
+ def with_streaming_response(self) -> FromPdfResourceWithStreamingResponse:
+ return FromPdfResourceWithStreamingResponse(self)
def create(
self,
@@ -84,14 +84,14 @@ def create(
)
-class AsyncFromPdf(AsyncAPIResource):
+class AsyncFromPdfResource(AsyncAPIResource):
@cached_property
- def with_raw_response(self) -> AsyncFromPdfWithRawResponse:
- return AsyncFromPdfWithRawResponse(self)
+ def with_raw_response(self) -> AsyncFromPdfResourceWithRawResponse:
+ return AsyncFromPdfResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> AsyncFromPdfWithStreamingResponse:
- return AsyncFromPdfWithStreamingResponse(self)
+ def with_streaming_response(self) -> AsyncFromPdfResourceWithStreamingResponse:
+ return AsyncFromPdfResourceWithStreamingResponse(self)
async def create(
self,
@@ -139,8 +139,8 @@ async def create(
)
-class FromPdfWithRawResponse:
- def __init__(self, from_pdf: FromPdf) -> None:
+class FromPdfResourceWithRawResponse:
+ def __init__(self, from_pdf: FromPdfResource) -> None:
self._from_pdf = from_pdf
self.create = to_raw_response_wrapper(
@@ -148,8 +148,8 @@ def __init__(self, from_pdf: FromPdf) -> None:
)
-class AsyncFromPdfWithRawResponse:
- def __init__(self, from_pdf: AsyncFromPdf) -> None:
+class AsyncFromPdfResourceWithRawResponse:
+ def __init__(self, from_pdf: AsyncFromPdfResource) -> None:
self._from_pdf = from_pdf
self.create = async_to_raw_response_wrapper(
@@ -157,8 +157,8 @@ def __init__(self, from_pdf: AsyncFromPdf) -> None:
)
-class FromPdfWithStreamingResponse:
- def __init__(self, from_pdf: FromPdf) -> None:
+class FromPdfResourceWithStreamingResponse:
+ def __init__(self, from_pdf: FromPdfResource) -> None:
self._from_pdf = from_pdf
self.create = to_streamed_response_wrapper(
@@ -166,8 +166,8 @@ def __init__(self, from_pdf: FromPdf) -> None:
)
-class AsyncFromPdfWithStreamingResponse:
- def __init__(self, from_pdf: AsyncFromPdf) -> None:
+class AsyncFromPdfResourceWithStreamingResponse:
+ def __init__(self, from_pdf: AsyncFromPdfResource) -> None:
self._from_pdf = from_pdf
self.create = async_to_streamed_response_wrapper(
diff --git a/src/maisa/resources/file_interpreter/from_pdf_scanned.py b/src/maisa/resources/file_interpreter/from_pdf_scanned.py
deleted file mode 100644
index db292da9..00000000
--- a/src/maisa/resources/file_interpreter/from_pdf_scanned.py
+++ /dev/null
@@ -1,288 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing import Mapping, cast
-from typing_extensions import Literal
-
-import httpx
-
-from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
-from ..._utils import (
- extract_files,
- maybe_transform,
- deepcopy_minimal,
- async_maybe_transform,
-)
-from ..._compat import cached_property
-from ..._resource import SyncAPIResource, AsyncAPIResource
-from ..._response import (
- to_raw_response_wrapper,
- to_streamed_response_wrapper,
- async_to_raw_response_wrapper,
- async_to_streamed_response_wrapper,
-)
-from ..._base_client import (
- make_request_options,
-)
-from ...types.file_interpreter import from_pdf_scanned_create_params
-
-__all__ = ["FromPdfScanned", "AsyncFromPdfScanned"]
-
-
-class FromPdfScanned(SyncAPIResource):
- @cached_property
- def with_raw_response(self) -> FromPdfScannedWithRawResponse:
- return FromPdfScannedWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> FromPdfScannedWithStreamingResponse:
- return FromPdfScannedWithStreamingResponse(self)
-
- def create(
- self,
- *,
- file: FileTypes,
- lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
- max_pages: int | NotGiven = NOT_GIVEN,
- variable1_description: str | NotGiven = NOT_GIVEN,
- variable1_name: str | NotGiven = NOT_GIVEN,
- variable1_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable2_description: str | NotGiven = NOT_GIVEN,
- variable2_name: str | NotGiven = NOT_GIVEN,
- variable2_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable3_description: str | NotGiven = NOT_GIVEN,
- variable3_name: str | NotGiven = NOT_GIVEN,
- variable3_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable4_description: str | NotGiven = NOT_GIVEN,
- variable4_name: str | NotGiven = NOT_GIVEN,
- variable4_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- ) -> object:
- """
- Interprets pdf file and returns some extracted variables.
-
- Args:
- lang: The language of the output. If not provided, the language used will be the same
- as the language of the text provided.
-
- max_pages: The maximum number of pages to be extracted.
-
- variable1_description: The description of the variable.
-
- variable1_name: The name of the variable to be extracted.
-
- variable1_type: Text Extraction Request Variable Type.
-
- variable2_description: The description of the variable.
-
- variable2_name: The name of the variable to be extracted.
-
- variable2_type: Text Extraction Request Variable Type.
-
- variable3_description: The description of the variable.
-
- variable3_name: The name of the variable to be extracted.
-
- variable3_type: Text Extraction Request Variable Type.
-
- variable4_description: The description of the variable.
-
- variable4_name: The name of the variable to be extracted.
-
- variable4_type: Text Extraction Request Variable Type.
-
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- body = deepcopy_minimal(
- {
- "file": file,
- "lang": lang,
- "max_pages": max_pages,
- "variable1_description": variable1_description,
- "variable1_name": variable1_name,
- "variable1_type": variable1_type,
- "variable2_description": variable2_description,
- "variable2_name": variable2_name,
- "variable2_type": variable2_type,
- "variable3_description": variable3_description,
- "variable3_name": variable3_name,
- "variable3_type": variable3_type,
- "variable4_description": variable4_description,
- "variable4_name": variable4_name,
- "variable4_type": variable4_type,
- }
- )
- files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
- return self._post(
- "/v1/file-interpreter/from-pdf-scanned",
- body=maybe_transform(body, from_pdf_scanned_create_params.FromPdfScannedCreateParams),
- files=files,
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=object,
- )
-
-
-class AsyncFromPdfScanned(AsyncAPIResource):
- @cached_property
- def with_raw_response(self) -> AsyncFromPdfScannedWithRawResponse:
- return AsyncFromPdfScannedWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> AsyncFromPdfScannedWithStreamingResponse:
- return AsyncFromPdfScannedWithStreamingResponse(self)
-
- async def create(
- self,
- *,
- file: FileTypes,
- lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
- max_pages: int | NotGiven = NOT_GIVEN,
- variable1_description: str | NotGiven = NOT_GIVEN,
- variable1_name: str | NotGiven = NOT_GIVEN,
- variable1_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable2_description: str | NotGiven = NOT_GIVEN,
- variable2_name: str | NotGiven = NOT_GIVEN,
- variable2_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable3_description: str | NotGiven = NOT_GIVEN,
- variable3_name: str | NotGiven = NOT_GIVEN,
- variable3_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable4_description: str | NotGiven = NOT_GIVEN,
- variable4_name: str | NotGiven = NOT_GIVEN,
- variable4_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- ) -> object:
- """
- Interprets pdf file and returns some extracted variables.
-
- Args:
- lang: The language of the output. If not provided, the language used will be the same
- as the language of the text provided.
-
- max_pages: The maximum number of pages to be extracted.
-
- variable1_description: The description of the variable.
-
- variable1_name: The name of the variable to be extracted.
-
- variable1_type: Text Extraction Request Variable Type.
-
- variable2_description: The description of the variable.
-
- variable2_name: The name of the variable to be extracted.
-
- variable2_type: Text Extraction Request Variable Type.
-
- variable3_description: The description of the variable.
-
- variable3_name: The name of the variable to be extracted.
-
- variable3_type: Text Extraction Request Variable Type.
-
- variable4_description: The description of the variable.
-
- variable4_name: The name of the variable to be extracted.
-
- variable4_type: Text Extraction Request Variable Type.
-
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- body = deepcopy_minimal(
- {
- "file": file,
- "lang": lang,
- "max_pages": max_pages,
- "variable1_description": variable1_description,
- "variable1_name": variable1_name,
- "variable1_type": variable1_type,
- "variable2_description": variable2_description,
- "variable2_name": variable2_name,
- "variable2_type": variable2_type,
- "variable3_description": variable3_description,
- "variable3_name": variable3_name,
- "variable3_type": variable3_type,
- "variable4_description": variable4_description,
- "variable4_name": variable4_name,
- "variable4_type": variable4_type,
- }
- )
- files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
- return await self._post(
- "/v1/file-interpreter/from-pdf-scanned",
- body=await async_maybe_transform(body, from_pdf_scanned_create_params.FromPdfScannedCreateParams),
- files=files,
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=object,
- )
-
-
-class FromPdfScannedWithRawResponse:
- def __init__(self, from_pdf_scanned: FromPdfScanned) -> None:
- self._from_pdf_scanned = from_pdf_scanned
-
- self.create = to_raw_response_wrapper(
- from_pdf_scanned.create,
- )
-
-
-class AsyncFromPdfScannedWithRawResponse:
- def __init__(self, from_pdf_scanned: AsyncFromPdfScanned) -> None:
- self._from_pdf_scanned = from_pdf_scanned
-
- self.create = async_to_raw_response_wrapper(
- from_pdf_scanned.create,
- )
-
-
-class FromPdfScannedWithStreamingResponse:
- def __init__(self, from_pdf_scanned: FromPdfScanned) -> None:
- self._from_pdf_scanned = from_pdf_scanned
-
- self.create = to_streamed_response_wrapper(
- from_pdf_scanned.create,
- )
-
-
-class AsyncFromPdfScannedWithStreamingResponse:
- def __init__(self, from_pdf_scanned: AsyncFromPdfScanned) -> None:
- self._from_pdf_scanned = from_pdf_scanned
-
- self.create = async_to_streamed_response_wrapper(
- from_pdf_scanned.create,
- )
diff --git a/src/maisa/resources/kpu.py b/src/maisa/resources/kpu.py
index eaee89a9..de7e243f 100644
--- a/src/maisa/resources/kpu.py
+++ b/src/maisa/resources/kpu.py
@@ -27,17 +27,17 @@
make_request_options,
)
-__all__ = ["Kpu", "AsyncKpu"]
+__all__ = ["KpuResource", "AsyncKpuResource"]
-class Kpu(SyncAPIResource):
+class KpuResource(SyncAPIResource):
@cached_property
- def with_raw_response(self) -> KpuWithRawResponse:
- return KpuWithRawResponse(self)
+ def with_raw_response(self) -> KpuResourceWithRawResponse:
+ return KpuResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> KpuWithStreamingResponse:
- return KpuWithStreamingResponse(self)
+ def with_streaming_response(self) -> KpuResourceWithStreamingResponse:
+ return KpuResourceWithStreamingResponse(self)
def run(
self,
@@ -127,14 +127,14 @@ def run(
)
-class AsyncKpu(AsyncAPIResource):
+class AsyncKpuResource(AsyncAPIResource):
@cached_property
- def with_raw_response(self) -> AsyncKpuWithRawResponse:
- return AsyncKpuWithRawResponse(self)
+ def with_raw_response(self) -> AsyncKpuResourceWithRawResponse:
+ return AsyncKpuResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> AsyncKpuWithStreamingResponse:
- return AsyncKpuWithStreamingResponse(self)
+ def with_streaming_response(self) -> AsyncKpuResourceWithStreamingResponse:
+ return AsyncKpuResourceWithStreamingResponse(self)
async def run(
self,
@@ -224,8 +224,8 @@ async def run(
)
-class KpuWithRawResponse:
- def __init__(self, kpu: Kpu) -> None:
+class KpuResourceWithRawResponse:
+ def __init__(self, kpu: KpuResource) -> None:
self._kpu = kpu
self.run = to_raw_response_wrapper(
@@ -233,8 +233,8 @@ def __init__(self, kpu: Kpu) -> None:
)
-class AsyncKpuWithRawResponse:
- def __init__(self, kpu: AsyncKpu) -> None:
+class AsyncKpuResourceWithRawResponse:
+ def __init__(self, kpu: AsyncKpuResource) -> None:
self._kpu = kpu
self.run = async_to_raw_response_wrapper(
@@ -242,8 +242,8 @@ def __init__(self, kpu: AsyncKpu) -> None:
)
-class KpuWithStreamingResponse:
- def __init__(self, kpu: Kpu) -> None:
+class KpuResourceWithStreamingResponse:
+ def __init__(self, kpu: KpuResource) -> None:
self._kpu = kpu
self.run = to_streamed_response_wrapper(
@@ -251,8 +251,8 @@ def __init__(self, kpu: Kpu) -> None:
)
-class AsyncKpuWithStreamingResponse:
- def __init__(self, kpu: AsyncKpu) -> None:
+class AsyncKpuResourceWithStreamingResponse:
+ def __init__(self, kpu: AsyncKpuResource) -> None:
self._kpu = kpu
self.run = async_to_streamed_response_wrapper(
diff --git a/src/maisa/resources/models/__init__.py b/src/maisa/resources/models/__init__.py
index 7b09253c..db0b8077 100644
--- a/src/maisa/resources/models/__init__.py
+++ b/src/maisa/resources/models/__init__.py
@@ -1,33 +1,33 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from .models import (
- Models,
- AsyncModels,
- ModelsWithRawResponse,
- AsyncModelsWithRawResponse,
- ModelsWithStreamingResponse,
- AsyncModelsWithStreamingResponse,
+ ModelsResource,
+ AsyncModelsResource,
+ ModelsResourceWithRawResponse,
+ AsyncModelsResourceWithRawResponse,
+ ModelsResourceWithStreamingResponse,
+ AsyncModelsResourceWithStreamingResponse,
)
from .embeddings import (
- Embeddings,
- AsyncEmbeddings,
- EmbeddingsWithRawResponse,
- AsyncEmbeddingsWithRawResponse,
- EmbeddingsWithStreamingResponse,
- AsyncEmbeddingsWithStreamingResponse,
+ EmbeddingsResource,
+ AsyncEmbeddingsResource,
+ EmbeddingsResourceWithRawResponse,
+ AsyncEmbeddingsResourceWithRawResponse,
+ EmbeddingsResourceWithStreamingResponse,
+ AsyncEmbeddingsResourceWithStreamingResponse,
)
__all__ = [
- "Embeddings",
- "AsyncEmbeddings",
- "EmbeddingsWithRawResponse",
- "AsyncEmbeddingsWithRawResponse",
- "EmbeddingsWithStreamingResponse",
- "AsyncEmbeddingsWithStreamingResponse",
- "Models",
- "AsyncModels",
- "ModelsWithRawResponse",
- "AsyncModelsWithRawResponse",
- "ModelsWithStreamingResponse",
- "AsyncModelsWithStreamingResponse",
+ "EmbeddingsResource",
+ "AsyncEmbeddingsResource",
+ "EmbeddingsResourceWithRawResponse",
+ "AsyncEmbeddingsResourceWithRawResponse",
+ "EmbeddingsResourceWithStreamingResponse",
+ "AsyncEmbeddingsResourceWithStreamingResponse",
+ "ModelsResource",
+ "AsyncModelsResource",
+ "ModelsResourceWithRawResponse",
+ "AsyncModelsResourceWithRawResponse",
+ "ModelsResourceWithStreamingResponse",
+ "AsyncModelsResourceWithStreamingResponse",
]
diff --git a/src/maisa/resources/models/embeddings.py b/src/maisa/resources/models/embeddings.py
index 82cfffcc..427afddd 100644
--- a/src/maisa/resources/models/embeddings.py
+++ b/src/maisa/resources/models/embeddings.py
@@ -22,19 +22,20 @@
from ..._base_client import (
make_request_options,
)
-from ...types.models import embeddings, embedding_create_params
+from ...types.models import embedding_create_params
+from ...types.models.embeddings import Embeddings
-__all__ = ["Embeddings", "AsyncEmbeddings"]
+__all__ = ["EmbeddingsResource", "AsyncEmbeddingsResource"]
-class Embeddings(SyncAPIResource):
+class EmbeddingsResource(SyncAPIResource):
@cached_property
- def with_raw_response(self) -> EmbeddingsWithRawResponse:
- return EmbeddingsWithRawResponse(self)
+ def with_raw_response(self) -> EmbeddingsResourceWithRawResponse:
+ return EmbeddingsResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> EmbeddingsWithStreamingResponse:
- return EmbeddingsWithStreamingResponse(self)
+ def with_streaming_response(self) -> EmbeddingsResourceWithStreamingResponse:
+ return EmbeddingsResourceWithStreamingResponse(self)
def create(
self,
@@ -46,7 +47,7 @@ def create(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- ) -> embeddings.Embeddings:
+ ) -> Embeddings:
"""
Creates embeddings from pieces of text.
@@ -67,18 +68,18 @@ def create(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=embeddings.Embeddings,
+ cast_to=Embeddings,
)
-class AsyncEmbeddings(AsyncAPIResource):
+class AsyncEmbeddingsResource(AsyncAPIResource):
@cached_property
- def with_raw_response(self) -> AsyncEmbeddingsWithRawResponse:
- return AsyncEmbeddingsWithRawResponse(self)
+ def with_raw_response(self) -> AsyncEmbeddingsResourceWithRawResponse:
+ return AsyncEmbeddingsResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> AsyncEmbeddingsWithStreamingResponse:
- return AsyncEmbeddingsWithStreamingResponse(self)
+ def with_streaming_response(self) -> AsyncEmbeddingsResourceWithStreamingResponse:
+ return AsyncEmbeddingsResourceWithStreamingResponse(self)
async def create(
self,
@@ -90,7 +91,7 @@ async def create(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- ) -> embeddings.Embeddings:
+ ) -> Embeddings:
"""
Creates embeddings from pieces of text.
@@ -111,12 +112,12 @@ async def create(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=embeddings.Embeddings,
+ cast_to=Embeddings,
)
-class EmbeddingsWithRawResponse:
- def __init__(self, embeddings: Embeddings) -> None:
+class EmbeddingsResourceWithRawResponse:
+ def __init__(self, embeddings: EmbeddingsResource) -> None:
self._embeddings = embeddings
self.create = to_raw_response_wrapper(
@@ -124,8 +125,8 @@ def __init__(self, embeddings: Embeddings) -> None:
)
-class AsyncEmbeddingsWithRawResponse:
- def __init__(self, embeddings: AsyncEmbeddings) -> None:
+class AsyncEmbeddingsResourceWithRawResponse:
+ def __init__(self, embeddings: AsyncEmbeddingsResource) -> None:
self._embeddings = embeddings
self.create = async_to_raw_response_wrapper(
@@ -133,8 +134,8 @@ def __init__(self, embeddings: AsyncEmbeddings) -> None:
)
-class EmbeddingsWithStreamingResponse:
- def __init__(self, embeddings: Embeddings) -> None:
+class EmbeddingsResourceWithStreamingResponse:
+ def __init__(self, embeddings: EmbeddingsResource) -> None:
self._embeddings = embeddings
self.create = to_streamed_response_wrapper(
@@ -142,8 +143,8 @@ def __init__(self, embeddings: Embeddings) -> None:
)
-class AsyncEmbeddingsWithStreamingResponse:
- def __init__(self, embeddings: AsyncEmbeddings) -> None:
+class AsyncEmbeddingsResourceWithStreamingResponse:
+ def __init__(self, embeddings: AsyncEmbeddingsResource) -> None:
self._embeddings = embeddings
self.create = async_to_streamed_response_wrapper(
diff --git a/src/maisa/resources/models/models.py b/src/maisa/resources/models/models.py
index b67597bd..c03c3690 100644
--- a/src/maisa/resources/models/models.py
+++ b/src/maisa/resources/models/models.py
@@ -4,77 +4,77 @@
from ..._compat import cached_property
from .embeddings import (
- Embeddings,
- AsyncEmbeddings,
- EmbeddingsWithRawResponse,
- AsyncEmbeddingsWithRawResponse,
- EmbeddingsWithStreamingResponse,
- AsyncEmbeddingsWithStreamingResponse,
+ EmbeddingsResource,
+ AsyncEmbeddingsResource,
+ EmbeddingsResourceWithRawResponse,
+ AsyncEmbeddingsResourceWithRawResponse,
+ EmbeddingsResourceWithStreamingResponse,
+ AsyncEmbeddingsResourceWithStreamingResponse,
)
from ..._resource import SyncAPIResource, AsyncAPIResource
-__all__ = ["Models", "AsyncModels"]
+__all__ = ["ModelsResource", "AsyncModelsResource"]
-class Models(SyncAPIResource):
+class ModelsResource(SyncAPIResource):
@cached_property
- def embeddings(self) -> Embeddings:
- return Embeddings(self._client)
+ def embeddings(self) -> EmbeddingsResource:
+ return EmbeddingsResource(self._client)
@cached_property
- def with_raw_response(self) -> ModelsWithRawResponse:
- return ModelsWithRawResponse(self)
+ def with_raw_response(self) -> ModelsResourceWithRawResponse:
+ return ModelsResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> ModelsWithStreamingResponse:
- return ModelsWithStreamingResponse(self)
+ def with_streaming_response(self) -> ModelsResourceWithStreamingResponse:
+ return ModelsResourceWithStreamingResponse(self)
-class AsyncModels(AsyncAPIResource):
+class AsyncModelsResource(AsyncAPIResource):
@cached_property
- def embeddings(self) -> AsyncEmbeddings:
- return AsyncEmbeddings(self._client)
+ def embeddings(self) -> AsyncEmbeddingsResource:
+ return AsyncEmbeddingsResource(self._client)
@cached_property
- def with_raw_response(self) -> AsyncModelsWithRawResponse:
- return AsyncModelsWithRawResponse(self)
+ def with_raw_response(self) -> AsyncModelsResourceWithRawResponse:
+ return AsyncModelsResourceWithRawResponse(self)
@cached_property
- def with_streaming_response(self) -> AsyncModelsWithStreamingResponse:
- return AsyncModelsWithStreamingResponse(self)
+ def with_streaming_response(self) -> AsyncModelsResourceWithStreamingResponse:
+ return AsyncModelsResourceWithStreamingResponse(self)
-class ModelsWithRawResponse:
- def __init__(self, models: Models) -> None:
+class ModelsResourceWithRawResponse:
+ def __init__(self, models: ModelsResource) -> None:
self._models = models
@cached_property
- def embeddings(self) -> EmbeddingsWithRawResponse:
- return EmbeddingsWithRawResponse(self._models.embeddings)
+ def embeddings(self) -> EmbeddingsResourceWithRawResponse:
+ return EmbeddingsResourceWithRawResponse(self._models.embeddings)
-class AsyncModelsWithRawResponse:
- def __init__(self, models: AsyncModels) -> None:
+class AsyncModelsResourceWithRawResponse:
+ def __init__(self, models: AsyncModelsResource) -> None:
self._models = models
@cached_property
- def embeddings(self) -> AsyncEmbeddingsWithRawResponse:
- return AsyncEmbeddingsWithRawResponse(self._models.embeddings)
+ def embeddings(self) -> AsyncEmbeddingsResourceWithRawResponse:
+ return AsyncEmbeddingsResourceWithRawResponse(self._models.embeddings)
-class ModelsWithStreamingResponse:
- def __init__(self, models: Models) -> None:
+class ModelsResourceWithStreamingResponse:
+ def __init__(self, models: ModelsResource) -> None:
self._models = models
@cached_property
- def embeddings(self) -> EmbeddingsWithStreamingResponse:
- return EmbeddingsWithStreamingResponse(self._models.embeddings)
+ def embeddings(self) -> EmbeddingsResourceWithStreamingResponse:
+ return EmbeddingsResourceWithStreamingResponse(self._models.embeddings)
-class AsyncModelsWithStreamingResponse:
- def __init__(self, models: AsyncModels) -> None:
+class AsyncModelsResourceWithStreamingResponse:
+ def __init__(self, models: AsyncModelsResource) -> None:
self._models = models
@cached_property
- def embeddings(self) -> AsyncEmbeddingsWithStreamingResponse:
- return AsyncEmbeddingsWithStreamingResponse(self._models.embeddings)
+ def embeddings(self) -> AsyncEmbeddingsResourceWithStreamingResponse:
+ return AsyncEmbeddingsResourceWithStreamingResponse(self._models.embeddings)
diff --git a/src/maisa/types/file_interpreter/__init__.py b/src/maisa/types/file_interpreter/__init__.py
index 778c0a5f..2848c33a 100644
--- a/src/maisa/types/file_interpreter/__init__.py
+++ b/src/maisa/types/file_interpreter/__init__.py
@@ -7,4 +7,3 @@
from .from_html_create_params import FromHTMLCreateParams as FromHTMLCreateParams
from .from_audio_create_params import FromAudioCreateParams as FromAudioCreateParams
from .from_image_create_params import FromImageCreateParams as FromImageCreateParams
-from .from_pdf_scanned_create_params import FromPdfScannedCreateParams as FromPdfScannedCreateParams
diff --git a/src/maisa/types/file_interpreter/from_pdf_scanned_create_params.py b/src/maisa/types/file_interpreter/from_pdf_scanned_create_params.py
deleted file mode 100644
index 2f64c3d2..00000000
--- a/src/maisa/types/file_interpreter/from_pdf_scanned_create_params.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing_extensions import Literal, Required, TypedDict
-
-from ..._types import FileTypes
-
-__all__ = ["FromPdfScannedCreateParams"]
-
-
-class FromPdfScannedCreateParams(TypedDict, total=False):
- file: Required[FileTypes]
-
- lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"]
- """The language of the output.
-
- If not provided, the language used will be the same as the language of the text
- provided.
- """
-
- max_pages: int
- """The maximum number of pages to be extracted."""
-
- variable1_description: str
- """The description of the variable."""
-
- variable1_name: str
- """The name of the variable to be extracted."""
-
- variable1_type: Literal["string", "number", "date", "boolean"]
- """Text Extraction Request Variable Type."""
-
- variable2_description: str
- """The description of the variable."""
-
- variable2_name: str
- """The name of the variable to be extracted."""
-
- variable2_type: Literal["string", "number", "date", "boolean"]
- """Text Extraction Request Variable Type."""
-
- variable3_description: str
- """The description of the variable."""
-
- variable3_name: str
- """The name of the variable to be extracted."""
-
- variable3_type: Literal["string", "number", "date", "boolean"]
- """Text Extraction Request Variable Type."""
-
- variable4_description: str
- """The description of the variable."""
-
- variable4_name: str
- """The name of the variable to be extracted."""
-
- variable4_type: Literal["string", "number", "date", "boolean"]
- """Text Extraction Request Variable Type."""
diff --git a/tests/api_resources/file_interpreter/test_from_pdf_scanned.py b/tests/api_resources/file_interpreter/test_from_pdf_scanned.py
deleted file mode 100644
index 9a1dc1d7..00000000
--- a/tests/api_resources/file_interpreter/test_from_pdf_scanned.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-import os
-from typing import Any, cast
-
-import pytest
-
-from maisa import Maisa, AsyncMaisa
-from tests.utils import assert_matches_type
-
-base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
-
-
-class TestFromPdfScanned:
- parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
-
- @parametrize
- def test_method_create(self, client: Maisa) -> None:
- from_pdf_scanned = client.file_interpreter.from_pdf_scanned.create(
- file=b"raw file contents",
- )
- assert_matches_type(object, from_pdf_scanned, path=["response"])
-
- @parametrize
- def test_method_create_with_all_params(self, client: Maisa) -> None:
- from_pdf_scanned = client.file_interpreter.from_pdf_scanned.create(
- file=b"raw file contents",
- lang="en",
- max_pages=0,
- variable1_description="The name of the person.",
- variable1_name="Name",
- variable1_type="string",
- variable2_description="The name of the person.",
- variable2_name="Name",
- variable2_type="string",
- variable3_description="The name of the person.",
- variable3_name="Name",
- variable3_type="string",
- variable4_description="The name of the person.",
- variable4_name="Name",
- variable4_type="string",
- )
- assert_matches_type(object, from_pdf_scanned, path=["response"])
-
- @parametrize
- def test_raw_response_create(self, client: Maisa) -> None:
- response = client.file_interpreter.from_pdf_scanned.with_raw_response.create(
- file=b"raw file contents",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- from_pdf_scanned = response.parse()
- assert_matches_type(object, from_pdf_scanned, path=["response"])
-
- @parametrize
- def test_streaming_response_create(self, client: Maisa) -> None:
- with client.file_interpreter.from_pdf_scanned.with_streaming_response.create(
- file=b"raw file contents",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- from_pdf_scanned = response.parse()
- assert_matches_type(object, from_pdf_scanned, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
-
-class TestAsyncFromPdfScanned:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
-
- @parametrize
- async def test_method_create(self, async_client: AsyncMaisa) -> None:
- from_pdf_scanned = await async_client.file_interpreter.from_pdf_scanned.create(
- file=b"raw file contents",
- )
- assert_matches_type(object, from_pdf_scanned, path=["response"])
-
- @parametrize
- async def test_method_create_with_all_params(self, async_client: AsyncMaisa) -> None:
- from_pdf_scanned = await async_client.file_interpreter.from_pdf_scanned.create(
- file=b"raw file contents",
- lang="en",
- max_pages=0,
- variable1_description="The name of the person.",
- variable1_name="Name",
- variable1_type="string",
- variable2_description="The name of the person.",
- variable2_name="Name",
- variable2_type="string",
- variable3_description="The name of the person.",
- variable3_name="Name",
- variable3_type="string",
- variable4_description="The name of the person.",
- variable4_name="Name",
- variable4_type="string",
- )
- assert_matches_type(object, from_pdf_scanned, path=["response"])
-
- @parametrize
- async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
- response = await async_client.file_interpreter.from_pdf_scanned.with_raw_response.create(
- file=b"raw file contents",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- from_pdf_scanned = await response.parse()
- assert_matches_type(object, from_pdf_scanned, path=["response"])
-
- @parametrize
- async def test_streaming_response_create(self, async_client: AsyncMaisa) -> None:
- async with async_client.file_interpreter.from_pdf_scanned.with_streaming_response.create(
- file=b"raw file contents",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- from_pdf_scanned = await response.parse()
- assert_matches_type(object, from_pdf_scanned, path=["response"])
-
- assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/mainet/__init__.py b/tests/api_resources/mainet/__init__.py
deleted file mode 100644
index fd8019a9..00000000
--- a/tests/api_resources/mainet/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/test_client.py b/tests/test_client.py
index 5f8ba8e7..aa708a4c 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -17,7 +17,6 @@
from pydantic import ValidationError
from maisa import Maisa, AsyncMaisa, APIResponseValidationError
-from maisa._client import Maisa, AsyncMaisa
from maisa._models import BaseModel, FinalRequestOptions
from maisa._constants import RAW_RESPONSE_HEADER
from maisa._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError
diff --git a/tests/utils.py b/tests/utils.py
index be2c2445..9fa6ba7a 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -97,7 +97,22 @@ def assert_matches_type(
assert_matches_type(key_type, key, path=[*path, ""])
assert_matches_type(items_type, item, path=[*path, ""])
elif is_union_type(type_):
- for i, variant in enumerate(get_args(type_)):
+ variants = get_args(type_)
+
+ try:
+ none_index = variants.index(type(None))
+ except ValueError:
+ pass
+ else:
+ # special case Optional[T] for better error messages
+ if len(variants) == 2:
+ if value is None:
+ # valid
+ return
+
+ return assert_matches_type(type_=variants[not none_index], value=value, path=path)
+
+ for i, variant in enumerate(variants):
try:
assert_matches_type(variant, value, path=[*path, f"variant {i}"])
return
From 4b94f710dfcf6e0a3155ee1328d96dafe7b26efa Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 May 2024 11:54:47 +0000
Subject: [PATCH 003/200] feat(api): OpenAPI spec update via Stainless API
(#21)
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 4076c464..9128fdb4 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,2 +1,2 @@
configured_endpoints: 13
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/maisa%2FMaisa-c82e0a80b379d33d11af783865ec15844bcd750043bf0f1543ebdf72bda79f3a.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/maisa%2Fmaisa-c82e0a80b379d33d11af783865ec15844bcd750043bf0f1543ebdf72bda79f3a.yml
From 29c8c9e95ae569de0a785722d0773198b96912bc Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 4 Nov 2024 19:42:47 +0000
Subject: [PATCH 004/200] chore: rebuild project due to codegen change (#22)
---
.devcontainer/Dockerfile | 2 +-
.github/workflows/ci.yml | 25 +-
.github/workflows/publish-pypi.yml | 6 +-
.github/workflows/release-doctor.yml | 2 +
.gitignore | 1 +
.stats.yml | 2 +-
CONTRIBUTING.md | 52 ++--
README.md | 29 +-
SECURITY.md | 27 ++
bin/check-release-environment | 13 +-
bin/publish-pypi | 3 +
pyproject.toml | 51 ++--
requirements-dev.lock | 38 +--
requirements.lock | 12 +-
scripts/bootstrap | 2 +-
scripts/format | 2 +-
scripts/lint | 4 +
scripts/mock | 4 +-
scripts/test | 4 +-
src/maisa/_base_client.py | 270 +++++++++++-------
src/maisa/_compat.py | 43 ++-
src/maisa/_files.py | 12 +-
src/maisa/_models.py | 85 +++++-
src/maisa/_response.py | 20 +-
src/maisa/_types.py | 15 +-
src/maisa/_utils/__init__.py | 5 +
src/maisa/_utils/_proxy.py | 3 +-
src/maisa/_utils/_reflection.py | 42 +++
src/maisa/_utils/_sync.py | 19 +-
src/maisa/_utils/_transform.py | 9 +-
src/maisa/_utils/_utils.py | 45 +--
.../resources/capabilities/capabilities.py | 26 +-
src/maisa/resources/capabilities/media.py | 80 +++---
.../file_interpreter/file_interpreter.py | 22 ++
.../resources/file_interpreter/from_audio.py | 44 ++-
.../resources/file_interpreter/from_docx.py | 44 ++-
.../resources/file_interpreter/from_html.py | 44 ++-
.../resources/file_interpreter/from_image.py | 44 ++-
.../resources/file_interpreter/from_pdf.py | 44 ++-
src/maisa/resources/kpu.py | 44 ++-
src/maisa/resources/models/embeddings.py | 26 +-
src/maisa/resources/models/models.py | 22 ++
src/maisa/types/shared/text_comparator.py | 1 -
src/maisa/types/shared/text_extractor.py | 1 -
src/maisa/types/shared/text_summary.py | 1 -
tests/api_resources/test_capabilities.py | 36 +--
tests/api_resources/test_kpu.py | 20 +-
tests/conftest.py | 14 +-
tests/test_client.py | 167 +++++++++++
tests/test_deepcopy.py | 3 +-
tests/test_models.py | 31 +-
tests/test_response.py | 101 ++++++-
tests/test_transform.py | 37 ++-
tests/test_utils/test_typing.py | 15 +-
tests/utils.py | 10 +-
55 files changed, 1219 insertions(+), 505 deletions(-)
create mode 100644 SECURITY.md
create mode 100644 src/maisa/_utils/_reflection.py
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index dd939620..ac9a2e75 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -3,7 +3,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
USER vscode
-RUN curl -sSf https://rye-up.com/get | RYE_VERSION="0.24.0" RYE_INSTALL_OPTION="--yes" bash
+RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.35.0" RYE_INSTALL_OPTION="--yes" bash
ENV PATH=/home/vscode/.rye/shims:$PATH
RUN echo "[[ -d .venv ]] && source .venv/bin/activate" >> /home/vscode/.bashrc
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6ce2f72b..40293964 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,6 +6,7 @@ on:
pull_request:
branches:
- main
+ - next
jobs:
lint:
@@ -18,27 +19,17 @@ jobs:
- name: Install Rye
run: |
- curl -sSf https://rye-up.com/get | bash
+ curl -sSf https://rye.astral.sh/get | bash
echo "$HOME/.rye/shims" >> $GITHUB_PATH
env:
- RYE_VERSION: 0.24.0
+ RYE_VERSION: '0.35.0'
RYE_INSTALL_OPTION: '--yes'
- name: Install dependencies
- run: |
- rye sync --all-features
-
- - name: Run ruff
- run: |
- rye run check:ruff
+ run: rye sync --all-features
- - name: Run type checking
- run: |
- rye run typecheck
-
- - name: Ensure importable
- run: |
- rye run python -c 'import maisa'
+ - name: Run lints
+ run: ./scripts/lint
test:
name: test
runs-on: ubuntu-latest
@@ -48,10 +39,10 @@ jobs:
- name: Install Rye
run: |
- curl -sSf https://rye-up.com/get | bash
+ curl -sSf https://rye.astral.sh/get | bash
echo "$HOME/.rye/shims" >> $GITHUB_PATH
env:
- RYE_VERSION: 0.24.0
+ RYE_VERSION: '0.35.0'
RYE_INSTALL_OPTION: '--yes'
- name: Bootstrap
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index 3a18e4be..a251ab22 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -18,11 +18,11 @@ jobs:
- name: Install Rye
run: |
- curl -sSf https://rye-up.com/get | bash
+ curl -sSf https://rye.astral.sh/get | bash
echo "$HOME/.rye/shims" >> $GITHUB_PATH
env:
- RYE_VERSION: 0.24.0
- RYE_INSTALL_OPTION: "--yes"
+ RYE_VERSION: '0.35.0'
+ RYE_INSTALL_OPTION: '--yes'
- name: Publish to PyPI
run: |
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index 588669b5..e2332a84 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -1,6 +1,8 @@
name: Release Doctor
on:
pull_request:
+ branches:
+ - main
workflow_dispatch:
jobs:
diff --git a/.gitignore b/.gitignore
index 0f9a66a9..87797408 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+.prism.log
.vscode
_dev
diff --git a/.stats.yml b/.stats.yml
index 9128fdb4..4076c464 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,2 +1,2 @@
configured_endpoints: 13
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/maisa%2Fmaisa-c82e0a80b379d33d11af783865ec15844bcd750043bf0f1543ebdf72bda79f3a.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/maisa%2FMaisa-c82e0a80b379d33d11af783865ec15844bcd750043bf0f1543ebdf72bda79f3a.yml
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index cf8d4723..a2ddcfb0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,9 +2,13 @@
### With Rye
-We use [Rye](https://rye-up.com/) to manage dependencies so we highly recommend [installing it](https://rye-up.com/guide/installation/) as it will automatically provision a Python environment with the expected Python version.
+We use [Rye](https://rye.astral.sh/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run:
-After installing Rye, you'll just have to run this command:
+```sh
+$ ./scripts/bootstrap
+```
+
+Or [install Rye manually](https://rye.astral.sh/guide/installation/) and run:
```sh
$ rye sync --all-features
@@ -31,25 +35,25 @@ $ pip install -r requirements-dev.lock
## Modifying/Adding code
-Most of the SDK is generated code, and any modified code will be overridden on the next generation. The
-`src/maisa/lib/` and `examples/` directories are exceptions and will never be overridden.
+Most of the SDK is generated code. Modifications to code will be persisted between generations, but may
+result in merge conflicts between manual patches and changes from the generator. The generator will never
+modify the contents of the `src/maisa/lib/` and `examples/` directories.
## Adding and running examples
-All files in the `examples/` directory are not modified by the Stainless generator and can be freely edited or
-added to.
+All files in the `examples/` directory are not modified by the generator and can be freely edited or added to.
-```bash
+```py
# add an example to examples/.py
#!/usr/bin/env -S rye run python
…
```
-```
-chmod +x examples/.py
+```sh
+$ chmod +x examples/.py
# run the example against your api
-./examples/.py
+$ ./examples/.py
```
## Using the repository from source
@@ -58,8 +62,8 @@ If you’d like to use the repository from source, you can either install from g
To install via git:
-```bash
-pip install git+ssh://git@github.com/maisaai/python-sdk.git
+```sh
+$ pip install git+ssh://git@github.com/maisaai/python-sdk.git
```
Alternatively, you can build from source and install the wheel file:
@@ -68,29 +72,29 @@ Building this package will create two files in the `dist/` directory, a `.tar.gz
To create a distributable version of the library, all you have to do is run this command:
-```bash
-rye build
+```sh
+$ rye build
# or
-python -m build
+$ python -m build
```
Then to install:
```sh
-pip install ./path-to-wheel-file.whl
+$ pip install ./path-to-wheel-file.whl
```
## Running tests
Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests.
-```bash
+```sh
# you will need npm installed
-npx prism mock path/to/your/openapi.yml
+$ npx prism mock path/to/your/openapi.yml
```
-```bash
-rye run pytest
+```sh
+$ ./scripts/test
```
## Linting and formatting
@@ -100,14 +104,14 @@ This repository uses [ruff](https://github.com/astral-sh/ruff) and
To lint:
-```bash
-rye run lint
+```sh
+$ ./scripts/lint
```
To format and fix all ruff issues automatically:
-```bash
-rye run format
+```sh
+$ ./scripts/format
```
## Publishing and releases
diff --git a/README.md b/README.md
index 5d5c0a00..c82f836f 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](https://pypi.org/project/maisa/)
-The Maisa Python library provides convenient access to the Maisa REST API from any Python 3.7+
+The Maisa Python library provides convenient access to the Maisa REST API from any Python 3.8+
application. The library includes type definitions for all request params and response fields,
and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).
@@ -10,7 +10,7 @@ It is generated with [Stainless](https://www.stainlessapi.com/).
## Documentation
-The REST API documentation can be found [on docs.maisa.ai](https://docs.maisa.ai/). The full API of this library can be found in [api.md](api.md).
+The REST API documentation can be found on [docs.maisa.ai](https://docs.maisa.ai/). The full API of this library can be found in [api.md](api.md).
## Installation
@@ -270,7 +270,7 @@ You can directly override the [httpx client](https://www.python-httpx.org/api/#c
- Support for proxies
- Custom transports
-- Additional [advanced](https://www.python-httpx.org/advanced/#client-instances) functionality
+- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality
```python
from maisa import Maisa, DefaultHttpxClient
@@ -285,6 +285,12 @@ client = Maisa(
)
```
+You can also customize the client on a per-request basis by using `with_options()`:
+
+```python
+client.with_options(http_client=DefaultHttpxClient(...))
+```
+
### Managing HTTP resources
By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting.
@@ -301,6 +307,21 @@ We take backwards-compatibility seriously and work hard to ensure you can rely o
We are keen for your feedback; please open an [issue](https://www.github.com/maisaai/python-sdk/issues) with questions, bugs, or suggestions.
+### Determining the installed version
+
+If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version.
+
+You can determine the version that is being used at runtime with:
+
+```py
+import maisa
+print(maisa.__version__)
+```
+
## Requirements
-Python 3.7 or higher.
+Python 3.8 or higher.
+
+## Contributing
+
+See [the contributing documentation](./CONTRIBUTING.md).
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..459e7314
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,27 @@
+# Security Policy
+
+## Reporting Security Issues
+
+This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
+
+To report a security issue, please contact the Stainless team at security@stainlessapi.com.
+
+## Responsible Disclosure
+
+We appreciate the efforts of security researchers and individuals who help us maintain the security of
+SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible
+disclosure practices by allowing us a reasonable amount of time to investigate and address the issue
+before making any information public.
+
+## Reporting Non-SDK Related Security Issues
+
+If you encounter security issues that are not directly related to SDKs but pertain to the services
+or products provided by Maisa please follow the respective company's security reporting guidelines.
+
+### Maisa Terms and Policies
+
+Please contact support@maisa.ai for any questions or concerns regarding security of our services.
+
+---
+
+Thank you for helping us keep the SDKs and systems they interact with secure.
diff --git a/bin/check-release-environment b/bin/check-release-environment
index dff35355..5af9c3e6 100644
--- a/bin/check-release-environment
+++ b/bin/check-release-environment
@@ -1,20 +1,9 @@
#!/usr/bin/env bash
-warnings=()
errors=()
if [ -z "${PYPI_TOKEN}" ]; then
- warnings+=("The MAISA_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.")
-fi
-
-lenWarnings=${#warnings[@]}
-
-if [[ lenWarnings -gt 0 ]]; then
- echo -e "Found the following warnings in the release environment:\n"
-
- for warning in "${warnings[@]}"; do
- echo -e "- $warning\n"
- done
+ errors+=("The MAISA_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.")
fi
lenErrors=${#errors[@]}
diff --git a/bin/publish-pypi b/bin/publish-pypi
index 826054e9..05bfccbb 100644
--- a/bin/publish-pypi
+++ b/bin/publish-pypi
@@ -3,4 +3,7 @@
set -eux
mkdir -p dist
rye build --clean
+# Patching importlib-metadata version until upstream library version is updated
+# https://github.com/pypa/twine/issues/977#issuecomment-2189800841
+"$HOME/.rye/self/bin/python3" -m pip install 'importlib-metadata==7.2.1'
rye publish --yes --token=$PYPI_TOKEN
diff --git a/pyproject.toml b/pyproject.toml
index d9df4e66..1720adab 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,13 +15,11 @@ dependencies = [
"distro>=1.7.0, <2",
"sniffio",
"cached-property; python_version < '3.8'",
-
]
-requires-python = ">= 3.7"
+requires-python = ">= 3.8"
classifiers = [
"Typing :: Typed",
"Intended Audience :: Developers",
- "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
@@ -36,8 +34,6 @@ classifiers = [
"License :: OSI Approved :: Apache Software License"
]
-
-
[project.urls]
Homepage = "https://github.com/maisaai/python-sdk"
Repository = "https://github.com/maisaai/python-sdk"
@@ -58,7 +54,7 @@ dev-dependencies = [
"nox",
"dirty-equals>=0.6.0",
"importlib-metadata>=6.7.0",
-
+ "rich>=13.7.1",
]
[tool.rye.scripts]
@@ -66,18 +62,21 @@ format = { chain = [
"format:ruff",
"format:docs",
"fix:ruff",
+ # run formatting again to fix any inconsistencies when imports are stripped
+ "format:ruff",
]}
-"format:black" = "black ."
"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md"
"format:ruff" = "ruff format"
-"format:isort" = "isort ."
"lint" = { chain = [
"check:ruff",
"typecheck",
+ "check:importable",
]}
-"check:ruff" = "ruff ."
-"fix:ruff" = "ruff --fix ."
+"check:ruff" = "ruff check ."
+"fix:ruff" = "ruff check --fix ."
+
+"check:importable" = "python -c 'import maisa'"
typecheck = { chain = [
"typecheck:pyright",
@@ -99,6 +98,21 @@ include = [
[tool.hatch.build.targets.wheel]
packages = ["src/maisa"]
+[tool.hatch.build.targets.sdist]
+# Basically everything except hidden files/directories (such as .github, .devcontainers, .python-version, etc)
+include = [
+ "/*.toml",
+ "/*.json",
+ "/*.lock",
+ "/*.md",
+ "/mypy.ini",
+ "/noxfile.py",
+ "bin/*",
+ "examples/*",
+ "src/*",
+ "tests/*",
+]
+
[tool.hatch.metadata.hooks.fancy-pypi-readme]
content-type = "text/markdown"
@@ -110,10 +124,6 @@ path = "README.md"
pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)'
replacement = '[\1](https://github.com/maisaai/python-sdk/tree/main/\g<2>)'
-[tool.black]
-line-length = 120
-target-version = ["py37"]
-
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--tb=short"
@@ -128,7 +138,7 @@ filterwarnings = [
# there are a couple of flags that are still disabled by
# default in strict mode as they are experimental and niche.
typeCheckingMode = "strict"
-pythonVersion = "3.7"
+pythonVersion = "3.8"
exclude = [
"_dev",
@@ -146,6 +156,11 @@ reportPrivateUsage = false
line-length = 120
output-format = "grouped"
target-version = "py37"
+
+[tool.ruff.format]
+docstring-code-format = true
+
+[tool.ruff.lint]
select = [
# isort
"I",
@@ -174,10 +189,6 @@ unfixable = [
"T201",
"T203",
]
-ignore-init-module-imports = true
-
-[tool.ruff.format]
-docstring-code-format = true
[tool.ruff.lint.flake8-tidy-imports.banned-api]
"functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead"
@@ -189,7 +200,7 @@ combine-as-imports = true
extra-standard-library = ["typing_extensions"]
known-first-party = ["maisa", "tests"]
-[tool.ruff.per-file-ignores]
+[tool.ruff.lint.per-file-ignores]
"bin/**.py" = ["T201", "T203"]
"scripts/**.py" = ["T201", "T203"]
"tests/**.py" = ["T201", "T203"]
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 7a6ac29b..aab8a26d 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -6,17 +6,16 @@
# features: []
# all-features: true
# with-sources: false
+# generate-hashes: false
-e file:.
annotated-types==0.6.0
# via pydantic
-anyio==4.1.0
+anyio==4.4.0
# via httpx
# via maisa
argcomplete==3.1.2
# via nox
-attrs==23.1.0
- # via pytest
certifi==2023.7.22
# via httpcore
# via httpx
@@ -27,8 +26,9 @@ distlib==0.3.7
# via virtualenv
distro==1.8.0
# via maisa
-exceptiongroup==1.1.3
+exceptiongroup==1.2.2
# via anyio
+ # via pytest
filelock==3.12.4
# via virtualenv
h11==0.14.0
@@ -44,7 +44,11 @@ idna==3.4
importlib-metadata==7.0.0
iniconfig==2.0.0
# via pytest
-mypy==1.7.1
+markdown-it-py==3.0.0
+ # via rich
+mdurl==0.1.2
+ # via markdown-it-py
+mypy==1.13.0
mypy-extensions==1.0.0
# via mypy
nodeenv==1.8.0
@@ -55,24 +59,25 @@ packaging==23.2
# via pytest
platformdirs==3.11.0
# via virtualenv
-pluggy==1.3.0
+pluggy==1.5.0
# via pytest
-py==1.11.0
- # via pytest
-pydantic==2.4.2
+pydantic==2.9.2
# via maisa
-pydantic-core==2.10.1
+pydantic-core==2.23.4
# via pydantic
-pyright==1.1.359
-pytest==7.1.1
+pygments==2.18.0
+ # via rich
+pyright==1.1.380
+pytest==8.3.3
# via pytest-asyncio
-pytest-asyncio==0.21.1
+pytest-asyncio==0.24.0
python-dateutil==2.8.2
# via time-machine
pytz==2023.3.post1
# via dirty-equals
respx==0.20.2
-ruff==0.1.9
+rich==13.7.1
+ruff==0.6.9
setuptools==68.2.2
# via nodeenv
six==1.16.0
@@ -82,10 +87,11 @@ sniffio==1.3.0
# via httpx
# via maisa
time-machine==2.9.0
-tomli==2.0.1
+tomli==2.0.2
# via mypy
# via pytest
-typing-extensions==4.8.0
+typing-extensions==4.12.2
+ # via anyio
# via maisa
# via mypy
# via pydantic
diff --git a/requirements.lock b/requirements.lock
index 6a42654c..46d20886 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -6,11 +6,12 @@
# features: []
# all-features: true
# with-sources: false
+# generate-hashes: false
-e file:.
annotated-types==0.6.0
# via pydantic
-anyio==4.1.0
+anyio==4.4.0
# via httpx
# via maisa
certifi==2023.7.22
@@ -18,7 +19,7 @@ certifi==2023.7.22
# via httpx
distro==1.8.0
# via maisa
-exceptiongroup==1.1.3
+exceptiongroup==1.2.2
# via anyio
h11==0.14.0
# via httpcore
@@ -29,15 +30,16 @@ httpx==0.25.2
idna==3.4
# via anyio
# via httpx
-pydantic==2.4.2
+pydantic==2.9.2
# via maisa
-pydantic-core==2.10.1
+pydantic-core==2.23.4
# via pydantic
sniffio==1.3.0
# via anyio
# via httpx
# via maisa
-typing-extensions==4.8.0
+typing-extensions==4.12.2
+ # via anyio
# via maisa
# via pydantic
# via pydantic-core
diff --git a/scripts/bootstrap b/scripts/bootstrap
index 29df07e7..8c5c60eb 100755
--- a/scripts/bootstrap
+++ b/scripts/bootstrap
@@ -16,4 +16,4 @@ echo "==> Installing Python dependencies…"
# experimental uv support makes installations significantly faster
rye config --set-bool behavior.use-uv=true
-rye sync
+rye sync --all-features
diff --git a/scripts/format b/scripts/format
index 2a9ea466..667ec2d7 100755
--- a/scripts/format
+++ b/scripts/format
@@ -4,5 +4,5 @@ set -e
cd "$(dirname "$0")/.."
+echo "==> Running formatters"
rye run format
-
diff --git a/scripts/lint b/scripts/lint
index 0cc68b51..b9127eda 100755
--- a/scripts/lint
+++ b/scripts/lint
@@ -4,5 +4,9 @@ set -e
cd "$(dirname "$0")/.."
+echo "==> Running lints"
rye run lint
+echo "==> Making sure it imports"
+rye run python -c 'import maisa'
+
diff --git a/scripts/mock b/scripts/mock
index fe89a1d0..d2814ae6 100755
--- a/scripts/mock
+++ b/scripts/mock
@@ -21,7 +21,7 @@ echo "==> Starting mock server with URL ${URL}"
# Run prism mock on the given spec
if [ "$1" == "--daemon" ]; then
- npm exec --package=@stoplight/prism-cli@~5.8 -- prism mock "$URL" &> .prism.log &
+ npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" &> .prism.log &
# Wait for server to come online
echo -n "Waiting for server"
@@ -37,5 +37,5 @@ if [ "$1" == "--daemon" ]; then
echo
else
- npm exec --package=@stoplight/prism-cli@~5.8 -- prism mock "$URL"
+ npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL"
fi
diff --git a/scripts/test b/scripts/test
index be01d044..4fa5698b 100755
--- a/scripts/test
+++ b/scripts/test
@@ -52,6 +52,8 @@ else
echo
fi
-# Run tests
echo "==> Running tests"
rye run pytest "$@"
+
+echo "==> Running Pydantic v1 tests"
+rye run nox -s test-pydantic-v1 -- "$@"
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index 338b5a46..c5a8b7d2 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import sys
import json
import time
import uuid
@@ -58,9 +59,10 @@
HttpxSendArgs,
AsyncTransport,
RequestOptions,
+ HttpxRequestFiles,
ModelBuilderProtocol,
)
-from ._utils import is_dict, is_list, is_given, lru_cache, is_mapping
+from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
from ._compat import model_copy, model_dump
from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type
from ._response import (
@@ -123,16 +125,14 @@ def __init__(
self,
*,
url: URL,
- ) -> None:
- ...
+ ) -> None: ...
@overload
def __init__(
self,
*,
params: Query,
- ) -> None:
- ...
+ ) -> None: ...
def __init__(
self,
@@ -143,6 +143,12 @@ def __init__(
self.url = url
self.params = params
+ @override
+ def __repr__(self) -> str:
+ if self.url:
+ return f"{self.__class__.__name__}(url={self.url})"
+ return f"{self.__class__.__name__}(params={self.params})"
+
class BasePage(GenericModel, Generic[_T]):
"""
@@ -165,8 +171,7 @@ def has_next_page(self) -> bool:
return False
return self.next_page_info() is not None
- def next_page_info(self) -> Optional[PageInfo]:
- ...
+ def next_page_info(self) -> Optional[PageInfo]: ...
def _get_page_items(self) -> Iterable[_T]: # type: ignore[empty-body]
...
@@ -358,6 +363,7 @@ def __init__(
self._custom_query = custom_query or {}
self._strict_response_validation = _strict_response_validation
self._idempotency_header = None
+ self._platform: Platform | None = None
if max_retries is None: # pyright: ignore[reportUnnecessaryComparison]
raise TypeError(
@@ -400,14 +406,7 @@ def _make_status_error(
) -> _exceptions.APIStatusError:
raise NotImplementedError()
- def _remaining_retries(
- self,
- remaining_retries: Optional[int],
- options: FinalRequestOptions,
- ) -> int:
- return remaining_retries if remaining_retries is not None else options.get_max_retries(self.max_retries)
-
- def _build_headers(self, options: FinalRequestOptions) -> httpx.Headers:
+ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers:
custom_headers = options.headers or {}
headers_dict = _merge_mappings(self.default_headers, custom_headers)
self._validate_headers(headers_dict, custom_headers)
@@ -419,6 +418,11 @@ def _build_headers(self, options: FinalRequestOptions) -> httpx.Headers:
if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers:
headers[idempotency_header] = options.idempotency_key or self._idempotency_key()
+ # Don't set the retry count header if it was already set or removed by the caller. We check
+ # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case.
+ if "x-stainless-retry-count" not in (header.lower() for header in custom_headers):
+ headers["x-stainless-retry-count"] = str(retries_taken)
+
return headers
def _prepare_url(self, url: str) -> URL:
@@ -440,6 +444,8 @@ def _make_sse_decoder(self) -> SSEDecoder | SSEBytesDecoder:
def _build_request(
self,
options: FinalRequestOptions,
+ *,
+ retries_taken: int = 0,
) -> httpx.Request:
if log.isEnabledFor(logging.DEBUG):
log.debug("Request options: %s", model_dump(options, exclude_unset=True))
@@ -455,9 +461,10 @@ def _build_request(
else:
raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`")
- headers = self._build_headers(options)
- params = _merge_mappings(self._custom_query, options.params)
+ headers = self._build_headers(options, retries_taken=retries_taken)
+ params = _merge_mappings(self.default_query, options.params)
content_type = headers.get("Content-Type")
+ files = options.files
# If the given Content-Type header is multipart/form-data then it
# has to be removed so that httpx can generate the header with
@@ -471,7 +478,7 @@ def _build_request(
headers.pop("Content-Type")
# As we are now sending multipart/form-data instead of application/json
- # we need to tell httpx to use it, https://www.python-httpx.org/advanced/#multipart-file-encoding
+ # we need to tell httpx to use it, https://www.python-httpx.org/advanced/clients/#multipart-file-encoding
if json_data:
if not is_dict(json_data):
raise TypeError(
@@ -479,19 +486,33 @@ def _build_request(
)
kwargs["data"] = self._serialize_multipartform(json_data)
+ # httpx determines whether or not to send a "multipart/form-data"
+ # request based on the truthiness of the "files" argument.
+ # This gets around that issue by generating a dict value that
+ # evaluates to true.
+ #
+ # https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186
+ if not files:
+ files = cast(HttpxRequestFiles, ForceMultipartDict())
+
+ prepared_url = self._prepare_url(options.url)
+ if "_" in prepared_url.host:
+ # work around https://github.com/encode/httpx/discussions/2880
+ kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")}
+
# TODO: report this error to httpx
return self._client.build_request( # pyright: ignore[reportUnknownMemberType]
headers=headers,
timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout,
method=options.method,
- url=self._prepare_url(options.url),
+ url=prepared_url,
# the `Query` type that we use is incompatible with qs'
# `Params` type as it needs to be typed as `Mapping[str, object]`
# so that passing a `TypedDict` doesn't cause an error.
# https://github.com/microsoft/pyright/issues/3526#event-6715453066
params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None,
json=json_data,
- files=options.files,
+ files=files,
**kwargs,
)
@@ -592,6 +613,12 @@ def default_headers(self) -> dict[str, str | Omit]:
**self._custom_headers,
}
+ @property
+ def default_query(self) -> dict[str, object]:
+ return {
+ **self._custom_query,
+ }
+
def _validate_headers(
self,
headers: Headers, # noqa: ARG002
@@ -616,7 +643,10 @@ def base_url(self, url: URL | str) -> None:
self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url))
def platform_headers(self) -> Dict[str, str]:
- return platform_headers(self._version)
+ # the actual implementation is in a separate `lru_cache` decorated
+ # function because adding `lru_cache` to methods will leak memory
+ # https://github.com/python/cpython/issues/88476
+ return platform_headers(self._version, platform=self._platform)
def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None:
"""Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified.
@@ -665,7 +695,8 @@ def _calculate_retry_timeout(
if retry_after is not None and 0 < retry_after <= 60:
return retry_after
- nb_retries = max_retries - remaining_retries
+ # Also cap retry count to 1000 to avoid any potential overflows with `pow`
+ nb_retries = min(max_retries - remaining_retries, 1000)
# Apply exponential backoff, but not more than the max.
sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY)
@@ -858,9 +889,9 @@ def __exit__(
def _prepare_options(
self,
options: FinalRequestOptions, # noqa: ARG002
- ) -> None:
+ ) -> FinalRequestOptions:
"""Hook for mutating the given options"""
- return None
+ return options
def _prepare_request(
self,
@@ -882,8 +913,7 @@ def request(
*,
stream: Literal[True],
stream_cls: Type[_StreamT],
- ) -> _StreamT:
- ...
+ ) -> _StreamT: ...
@overload
def request(
@@ -893,8 +923,7 @@ def request(
remaining_retries: Optional[int] = None,
*,
stream: Literal[False] = False,
- ) -> ResponseT:
- ...
+ ) -> ResponseT: ...
@overload
def request(
@@ -905,8 +934,7 @@ def request(
*,
stream: bool = False,
stream_cls: Type[_StreamT] | None = None,
- ) -> ResponseT | _StreamT:
- ...
+ ) -> ResponseT | _StreamT: ...
def request(
self,
@@ -917,12 +945,17 @@ def request(
stream: bool = False,
stream_cls: type[_StreamT] | None = None,
) -> ResponseT | _StreamT:
+ if remaining_retries is not None:
+ retries_taken = options.get_max_retries(self.max_retries) - remaining_retries
+ else:
+ retries_taken = 0
+
return self._request(
cast_to=cast_to,
options=options,
stream=stream,
stream_cls=stream_cls,
- remaining_retries=remaining_retries,
+ retries_taken=retries_taken,
)
def _request(
@@ -930,15 +963,20 @@ def _request(
*,
cast_to: Type[ResponseT],
options: FinalRequestOptions,
- remaining_retries: int | None,
+ retries_taken: int,
stream: bool,
stream_cls: type[_StreamT] | None,
) -> ResponseT | _StreamT:
+ # create a copy of the options we were given so that if the
+ # options are mutated later & we then retry, the retries are
+ # given the original options
+ input_options = model_copy(options)
+
cast_to = self._maybe_override_cast_to(cast_to, options)
- self._prepare_options(options)
+ options = self._prepare_options(options)
- retries = self._remaining_retries(remaining_retries, options)
- request = self._build_request(options)
+ remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
+ request = self._build_request(options, retries_taken=retries_taken)
self._prepare_request(request)
kwargs: HttpxSendArgs = {}
@@ -956,11 +994,11 @@ def _request(
except httpx.TimeoutException as err:
log.debug("Encountered httpx.TimeoutException", exc_info=True)
- if retries > 0:
+ if remaining_retries > 0:
return self._retry_request(
- options,
+ input_options,
cast_to,
- retries,
+ retries_taken=retries_taken,
stream=stream,
stream_cls=stream_cls,
response_headers=None,
@@ -971,11 +1009,11 @@ def _request(
except Exception as err:
log.debug("Encountered Exception", exc_info=True)
- if retries > 0:
+ if remaining_retries > 0:
return self._retry_request(
- options,
+ input_options,
cast_to,
- retries,
+ retries_taken=retries_taken,
stream=stream,
stream_cls=stream_cls,
response_headers=None,
@@ -998,13 +1036,13 @@ def _request(
except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
- if retries > 0 and self._should_retry(err.response):
+ if remaining_retries > 0 and self._should_retry(err.response):
err.response.close()
return self._retry_request(
- options,
+ input_options,
cast_to,
- retries,
- err.response.headers,
+ retries_taken=retries_taken,
+ response_headers=err.response.headers,
stream=stream,
stream_cls=stream_cls,
)
@@ -1023,25 +1061,26 @@ def _request(
response=response,
stream=stream,
stream_cls=stream_cls,
+ retries_taken=retries_taken,
)
def _retry_request(
self,
options: FinalRequestOptions,
cast_to: Type[ResponseT],
- remaining_retries: int,
- response_headers: httpx.Headers | None,
*,
+ retries_taken: int,
+ response_headers: httpx.Headers | None,
stream: bool,
stream_cls: type[_StreamT] | None,
) -> ResponseT | _StreamT:
- remaining = remaining_retries - 1
- if remaining == 1:
+ remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
+ if remaining_retries == 1:
log.debug("1 retry left")
else:
- log.debug("%i retries left", remaining)
+ log.debug("%i retries left", remaining_retries)
- timeout = self._calculate_retry_timeout(remaining, options, response_headers)
+ timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers)
log.info("Retrying request to %s in %f seconds", options.url, timeout)
# In a synchronous context we are blocking the entire thread. Up to the library user to run the client in a
@@ -1051,7 +1090,7 @@ def _retry_request(
return self._request(
options=options,
cast_to=cast_to,
- remaining_retries=remaining,
+ retries_taken=retries_taken + 1,
stream=stream,
stream_cls=stream_cls,
)
@@ -1064,6 +1103,7 @@ def _process_response(
response: httpx.Response,
stream: bool,
stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None,
+ retries_taken: int = 0,
) -> ResponseT:
origin = get_origin(cast_to) or cast_to
@@ -1081,6 +1121,7 @@ def _process_response(
stream=stream,
stream_cls=stream_cls,
options=options,
+ retries_taken=retries_taken,
),
)
@@ -1094,6 +1135,7 @@ def _process_response(
stream=stream,
stream_cls=stream_cls,
options=options,
+ retries_taken=retries_taken,
)
if bool(response.request.headers.get(RAW_RESPONSE_HEADER)):
return cast(ResponseT, api_response)
@@ -1126,8 +1168,7 @@ def get(
cast_to: Type[ResponseT],
options: RequestOptions = {},
stream: Literal[False] = False,
- ) -> ResponseT:
- ...
+ ) -> ResponseT: ...
@overload
def get(
@@ -1138,8 +1179,7 @@ def get(
options: RequestOptions = {},
stream: Literal[True],
stream_cls: type[_StreamT],
- ) -> _StreamT:
- ...
+ ) -> _StreamT: ...
@overload
def get(
@@ -1150,8 +1190,7 @@ def get(
options: RequestOptions = {},
stream: bool,
stream_cls: type[_StreamT] | None = None,
- ) -> ResponseT | _StreamT:
- ...
+ ) -> ResponseT | _StreamT: ...
def get(
self,
@@ -1177,8 +1216,7 @@ def post(
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: Literal[False] = False,
- ) -> ResponseT:
- ...
+ ) -> ResponseT: ...
@overload
def post(
@@ -1191,8 +1229,7 @@ def post(
files: RequestFiles | None = None,
stream: Literal[True],
stream_cls: type[_StreamT],
- ) -> _StreamT:
- ...
+ ) -> _StreamT: ...
@overload
def post(
@@ -1205,8 +1242,7 @@ def post(
files: RequestFiles | None = None,
stream: bool,
stream_cls: type[_StreamT] | None = None,
- ) -> ResponseT | _StreamT:
- ...
+ ) -> ResponseT | _StreamT: ...
def post(
self,
@@ -1416,9 +1452,9 @@ async def __aexit__(
async def _prepare_options(
self,
options: FinalRequestOptions, # noqa: ARG002
- ) -> None:
+ ) -> FinalRequestOptions:
"""Hook for mutating the given options"""
- return None
+ return options
async def _prepare_request(
self,
@@ -1439,8 +1475,7 @@ async def request(
*,
stream: Literal[False] = False,
remaining_retries: Optional[int] = None,
- ) -> ResponseT:
- ...
+ ) -> ResponseT: ...
@overload
async def request(
@@ -1451,8 +1486,7 @@ async def request(
stream: Literal[True],
stream_cls: type[_AsyncStreamT],
remaining_retries: Optional[int] = None,
- ) -> _AsyncStreamT:
- ...
+ ) -> _AsyncStreamT: ...
@overload
async def request(
@@ -1463,8 +1497,7 @@ async def request(
stream: bool,
stream_cls: type[_AsyncStreamT] | None = None,
remaining_retries: Optional[int] = None,
- ) -> ResponseT | _AsyncStreamT:
- ...
+ ) -> ResponseT | _AsyncStreamT: ...
async def request(
self,
@@ -1475,12 +1508,17 @@ async def request(
stream_cls: type[_AsyncStreamT] | None = None,
remaining_retries: Optional[int] = None,
) -> ResponseT | _AsyncStreamT:
+ if remaining_retries is not None:
+ retries_taken = options.get_max_retries(self.max_retries) - remaining_retries
+ else:
+ retries_taken = 0
+
return await self._request(
cast_to=cast_to,
options=options,
stream=stream,
stream_cls=stream_cls,
- remaining_retries=remaining_retries,
+ retries_taken=retries_taken,
)
async def _request(
@@ -1490,13 +1528,23 @@ async def _request(
*,
stream: bool,
stream_cls: type[_AsyncStreamT] | None,
- remaining_retries: int | None,
+ retries_taken: int,
) -> ResponseT | _AsyncStreamT:
+ if self._platform is None:
+ # `get_platform` can make blocking IO calls so we
+ # execute it earlier while we are in an async context
+ self._platform = await asyncify(get_platform)()
+
+ # create a copy of the options we were given so that if the
+ # options are mutated later & we then retry, the retries are
+ # given the original options
+ input_options = model_copy(options)
+
cast_to = self._maybe_override_cast_to(cast_to, options)
- await self._prepare_options(options)
+ options = await self._prepare_options(options)
- retries = self._remaining_retries(remaining_retries, options)
- request = self._build_request(options)
+ remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
+ request = self._build_request(options, retries_taken=retries_taken)
await self._prepare_request(request)
kwargs: HttpxSendArgs = {}
@@ -1512,11 +1560,11 @@ async def _request(
except httpx.TimeoutException as err:
log.debug("Encountered httpx.TimeoutException", exc_info=True)
- if retries > 0:
+ if remaining_retries > 0:
return await self._retry_request(
- options,
+ input_options,
cast_to,
- retries,
+ retries_taken=retries_taken,
stream=stream,
stream_cls=stream_cls,
response_headers=None,
@@ -1527,11 +1575,11 @@ async def _request(
except Exception as err:
log.debug("Encountered Exception", exc_info=True)
- if retries > 0:
+ if remaining_retries > 0:
return await self._retry_request(
- options,
+ input_options,
cast_to,
- retries,
+ retries_taken=retries_taken,
stream=stream,
stream_cls=stream_cls,
response_headers=None,
@@ -1549,13 +1597,13 @@ async def _request(
except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
- if retries > 0 and self._should_retry(err.response):
+ if remaining_retries > 0 and self._should_retry(err.response):
await err.response.aclose()
return await self._retry_request(
- options,
+ input_options,
cast_to,
- retries,
- err.response.headers,
+ retries_taken=retries_taken,
+ response_headers=err.response.headers,
stream=stream,
stream_cls=stream_cls,
)
@@ -1574,25 +1622,26 @@ async def _request(
response=response,
stream=stream,
stream_cls=stream_cls,
+ retries_taken=retries_taken,
)
async def _retry_request(
self,
options: FinalRequestOptions,
cast_to: Type[ResponseT],
- remaining_retries: int,
- response_headers: httpx.Headers | None,
*,
+ retries_taken: int,
+ response_headers: httpx.Headers | None,
stream: bool,
stream_cls: type[_AsyncStreamT] | None,
) -> ResponseT | _AsyncStreamT:
- remaining = remaining_retries - 1
- if remaining == 1:
+ remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
+ if remaining_retries == 1:
log.debug("1 retry left")
else:
- log.debug("%i retries left", remaining)
+ log.debug("%i retries left", remaining_retries)
- timeout = self._calculate_retry_timeout(remaining, options, response_headers)
+ timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers)
log.info("Retrying request to %s in %f seconds", options.url, timeout)
await anyio.sleep(timeout)
@@ -1600,7 +1649,7 @@ async def _retry_request(
return await self._request(
options=options,
cast_to=cast_to,
- remaining_retries=remaining,
+ retries_taken=retries_taken + 1,
stream=stream,
stream_cls=stream_cls,
)
@@ -1613,6 +1662,7 @@ async def _process_response(
response: httpx.Response,
stream: bool,
stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None,
+ retries_taken: int = 0,
) -> ResponseT:
origin = get_origin(cast_to) or cast_to
@@ -1630,6 +1680,7 @@ async def _process_response(
stream=stream,
stream_cls=stream_cls,
options=options,
+ retries_taken=retries_taken,
),
)
@@ -1643,6 +1694,7 @@ async def _process_response(
stream=stream,
stream_cls=stream_cls,
options=options,
+ retries_taken=retries_taken,
)
if bool(response.request.headers.get(RAW_RESPONSE_HEADER)):
return cast(ResponseT, api_response)
@@ -1665,8 +1717,7 @@ async def get(
cast_to: Type[ResponseT],
options: RequestOptions = {},
stream: Literal[False] = False,
- ) -> ResponseT:
- ...
+ ) -> ResponseT: ...
@overload
async def get(
@@ -1677,8 +1728,7 @@ async def get(
options: RequestOptions = {},
stream: Literal[True],
stream_cls: type[_AsyncStreamT],
- ) -> _AsyncStreamT:
- ...
+ ) -> _AsyncStreamT: ...
@overload
async def get(
@@ -1689,8 +1739,7 @@ async def get(
options: RequestOptions = {},
stream: bool,
stream_cls: type[_AsyncStreamT] | None = None,
- ) -> ResponseT | _AsyncStreamT:
- ...
+ ) -> ResponseT | _AsyncStreamT: ...
async def get(
self,
@@ -1714,8 +1763,7 @@ async def post(
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: Literal[False] = False,
- ) -> ResponseT:
- ...
+ ) -> ResponseT: ...
@overload
async def post(
@@ -1728,8 +1776,7 @@ async def post(
options: RequestOptions = {},
stream: Literal[True],
stream_cls: type[_AsyncStreamT],
- ) -> _AsyncStreamT:
- ...
+ ) -> _AsyncStreamT: ...
@overload
async def post(
@@ -1742,8 +1789,7 @@ async def post(
options: RequestOptions = {},
stream: bool,
stream_cls: type[_AsyncStreamT] | None = None,
- ) -> ResponseT | _AsyncStreamT:
- ...
+ ) -> ResponseT | _AsyncStreamT: ...
async def post(
self,
@@ -1848,6 +1894,11 @@ def make_request_options(
return options
+class ForceMultipartDict(Dict[str, None]):
+ def __bool__(self) -> bool:
+ return True
+
+
class OtherPlatform:
def __init__(self, name: str) -> None:
self.name = name
@@ -1915,11 +1966,11 @@ def get_platform() -> Platform:
@lru_cache(maxsize=None)
-def platform_headers(version: str) -> Dict[str, str]:
+def platform_headers(version: str, *, platform: Platform | None) -> Dict[str, str]:
return {
"X-Stainless-Lang": "python",
"X-Stainless-Package-Version": version,
- "X-Stainless-OS": str(get_platform()),
+ "X-Stainless-OS": str(platform or get_platform()),
"X-Stainless-Arch": str(get_architecture()),
"X-Stainless-Runtime": get_python_runtime(),
"X-Stainless-Runtime-Version": get_python_version(),
@@ -1954,7 +2005,6 @@ def get_python_version() -> str:
def get_architecture() -> Arch:
try:
- python_bitness, _ = platform.architecture()
machine = platform.machine().lower()
except Exception:
return "unknown"
@@ -1970,7 +2020,7 @@ def get_architecture() -> Arch:
return "x64"
# TODO: untested
- if python_bitness == "32bit":
+ if sys.maxsize <= 2**32:
return "x32"
if machine:
diff --git a/src/maisa/_compat.py b/src/maisa/_compat.py
index 74c7639b..4794129c 100644
--- a/src/maisa/_compat.py
+++ b/src/maisa/_compat.py
@@ -2,12 +2,12 @@
from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload
from datetime import date, datetime
-from typing_extensions import Self
+from typing_extensions import Self, Literal
import pydantic
from pydantic.fields import FieldInfo
-from ._types import StrBytesIntFloat
+from ._types import IncEx, StrBytesIntFloat
_T = TypeVar("_T")
_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel)
@@ -118,10 +118,10 @@ def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]:
return model.__fields__ # type: ignore
-def model_copy(model: _ModelT) -> _ModelT:
+def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT:
if PYDANTIC_V2:
- return model.model_copy()
- return model.copy() # type: ignore
+ return model.model_copy(deep=deep)
+ return model.copy(deep=deep) # type: ignore
def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str:
@@ -133,17 +133,24 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str:
def model_dump(
model: pydantic.BaseModel,
*,
+ exclude: IncEx | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
+ warnings: bool = True,
+ mode: Literal["json", "python"] = "python",
) -> dict[str, Any]:
- if PYDANTIC_V2:
+ if PYDANTIC_V2 or hasattr(model, "model_dump"):
return model.model_dump(
+ mode=mode,
+ exclude=exclude,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
+ warnings=warnings,
)
return cast(
"dict[str, Any]",
model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
+ exclude=exclude,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
),
@@ -159,22 +166,19 @@ def model_parse(model: type[_ModelT], data: Any) -> _ModelT:
# generic models
if TYPE_CHECKING:
- class GenericModel(pydantic.BaseModel):
- ...
+ class GenericModel(pydantic.BaseModel): ...
else:
if PYDANTIC_V2:
# there no longer needs to be a distinction in v2 but
# we still have to create our own subclass to avoid
# inconsistent MRO ordering errors
- class GenericModel(pydantic.BaseModel):
- ...
+ class GenericModel(pydantic.BaseModel): ...
else:
import pydantic.generics
- class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel):
- ...
+ class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ...
# cached properties
@@ -193,26 +197,21 @@ class typed_cached_property(Generic[_T]):
func: Callable[[Any], _T]
attrname: str | None
- def __init__(self, func: Callable[[Any], _T]) -> None:
- ...
+ def __init__(self, func: Callable[[Any], _T]) -> None: ...
@overload
- def __get__(self, instance: None, owner: type[Any] | None = None) -> Self:
- ...
+ def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ...
@overload
- def __get__(self, instance: object, owner: type[Any] | None = None) -> _T:
- ...
+ def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: ...
def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self:
raise NotImplementedError()
- def __set_name__(self, owner: type[Any], name: str) -> None:
- ...
+ def __set_name__(self, owner: type[Any], name: str) -> None: ...
# __set__ is not defined at runtime, but @cached_property is designed to be settable
- def __set__(self, instance: object, value: _T) -> None:
- ...
+ def __set__(self, instance: object, value: _T) -> None: ...
else:
try:
from functools import cached_property as cached_property
diff --git a/src/maisa/_files.py b/src/maisa/_files.py
index 0d2022ae..715cc207 100644
--- a/src/maisa/_files.py
+++ b/src/maisa/_files.py
@@ -39,13 +39,11 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None:
@overload
-def to_httpx_files(files: None) -> None:
- ...
+def to_httpx_files(files: None) -> None: ...
@overload
-def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles:
- ...
+def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ...
def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None:
@@ -83,13 +81,11 @@ def _read_file_content(file: FileContent) -> HttpxFileContent:
@overload
-async def async_to_httpx_files(files: None) -> None:
- ...
+async def async_to_httpx_files(files: None) -> None: ...
@overload
-async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles:
- ...
+async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ...
async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None:
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index ff3f54e2..6cb469e2 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -10,6 +10,7 @@
ClassVar,
Protocol,
Required,
+ ParamSpec,
TypedDict,
TypeGuard,
final,
@@ -36,6 +37,7 @@
PropertyInfo,
is_list,
is_given,
+ json_safe,
lru_cache,
is_mapping,
parse_date,
@@ -62,11 +64,14 @@
from ._constants import RAW_RESPONSE_HEADER
if TYPE_CHECKING:
- from pydantic_core.core_schema import ModelField, ModelFieldsSchema
+ from pydantic_core.core_schema import ModelField, LiteralSchema, ModelFieldsSchema
__all__ = ["BaseModel", "GenericModel"]
_T = TypeVar("_T")
+_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel")
+
+P = ParamSpec("P")
@runtime_checkable
@@ -172,7 +177,7 @@ def __str__(self) -> str:
# Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836.
@classmethod
@override
- def construct(
+ def construct( # pyright: ignore[reportIncompatibleMethodOverride]
cls: Type[ModelT],
_fields_set: set[str] | None = None,
**values: object,
@@ -244,14 +249,16 @@ def model_dump(
self,
*,
mode: Literal["json", "python"] | str = "python",
- include: IncEx = None,
- exclude: IncEx = None,
+ include: IncEx | None = None,
+ exclude: IncEx | None = None,
by_alias: bool = False,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
round_trip: bool = False,
- warnings: bool = True,
+ warnings: bool | Literal["none", "warn", "error"] = True,
+ context: dict[str, Any] | None = None,
+ serialize_as_any: bool = False,
) -> dict[str, Any]:
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump
@@ -273,13 +280,17 @@ def model_dump(
Returns:
A dictionary representation of the model.
"""
- if mode != "python":
- raise ValueError("mode is only supported in Pydantic v2")
+ if mode not in {"json", "python"}:
+ raise ValueError("mode must be either 'json' or 'python'")
if round_trip != False:
raise ValueError("round_trip is only supported in Pydantic v2")
if warnings != True:
raise ValueError("warnings is only supported in Pydantic v2")
- return super().dict( # pyright: ignore[reportDeprecated]
+ if context is not None:
+ raise ValueError("context is only supported in Pydantic v2")
+ if serialize_as_any != False:
+ raise ValueError("serialize_as_any is only supported in Pydantic v2")
+ dumped = super().dict( # pyright: ignore[reportDeprecated]
include=include,
exclude=exclude,
by_alias=by_alias,
@@ -288,19 +299,23 @@ def model_dump(
exclude_none=exclude_none,
)
+ return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped
+
@override
def model_dump_json(
self,
*,
indent: int | None = None,
- include: IncEx = None,
- exclude: IncEx = None,
+ include: IncEx | None = None,
+ exclude: IncEx | None = None,
by_alias: bool = False,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
round_trip: bool = False,
- warnings: bool = True,
+ warnings: bool | Literal["none", "warn", "error"] = True,
+ context: dict[str, Any] | None = None,
+ serialize_as_any: bool = False,
) -> str:
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json
@@ -324,6 +339,10 @@ def model_dump_json(
raise ValueError("round_trip is only supported in Pydantic v2")
if warnings != True:
raise ValueError("warnings is only supported in Pydantic v2")
+ if context is not None:
+ raise ValueError("context is only supported in Pydantic v2")
+ if serialize_as_any != False:
+ raise ValueError("serialize_as_any is only supported in Pydantic v2")
return super().json( # type: ignore[reportDeprecated]
indent=indent,
include=include,
@@ -364,9 +383,43 @@ def is_basemodel(type_: type) -> bool:
def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]:
origin = get_origin(type_) or type_
+ if not inspect.isclass(origin):
+ return False
return issubclass(origin, BaseModel) or issubclass(origin, GenericModel)
+def build(
+ base_model_cls: Callable[P, _BaseModelT],
+ *args: P.args,
+ **kwargs: P.kwargs,
+) -> _BaseModelT:
+ """Construct a BaseModel class without validation.
+
+ This is useful for cases where you need to instantiate a `BaseModel`
+ from an API response as this provides type-safe params which isn't supported
+ by helpers like `construct_type()`.
+
+ ```py
+ build(MyModel, my_field_a="foo", my_field_b=123)
+ ```
+ """
+ if args:
+ raise TypeError(
+ "Received positional arguments which are not supported; Keyword arguments must be used instead",
+ )
+
+ return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs))
+
+
+def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T:
+ """Loose coercion to the expected type with construction of nested values.
+
+ Note: the returned value from this function is not guaranteed to match the
+ given type.
+ """
+ return cast(_T, construct_type(value=value, type_=type_))
+
+
def construct_type(*, value: object, type_: object) -> object:
"""Loose coercion to the expected type with construction of nested values.
@@ -550,7 +603,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
field_schema = field["schema"]
if field_schema["type"] == "literal":
- for entry in field_schema["expected"]:
+ for entry in cast("LiteralSchema", field_schema)["expected"]:
if isinstance(entry, str):
mapping[entry] = variant
else:
@@ -604,6 +657,14 @@ def validate_type(*, type_: type[_T], value: object) -> _T:
return cast(_T, _validate_non_model_type(type_=type_, value=value))
+def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None:
+ """Add a pydantic config for the given type.
+
+ Note: this is a no-op on Pydantic v1.
+ """
+ setattr(typ, "__pydantic_config__", config) # noqa: B010
+
+
# our use of subclasssing here causes weirdness for type checkers,
# so we just pretend that we don't subclass
if TYPE_CHECKING:
diff --git a/src/maisa/_response.py b/src/maisa/_response.py
index 0a3b276a..48f1a4ab 100644
--- a/src/maisa/_response.py
+++ b/src/maisa/_response.py
@@ -55,6 +55,9 @@ class BaseAPIResponse(Generic[R]):
http_response: httpx.Response
+ retries_taken: int
+ """The number of retries made. If no retries happened this will be `0`"""
+
def __init__(
self,
*,
@@ -64,6 +67,7 @@ def __init__(
stream: bool,
stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None,
options: FinalRequestOptions,
+ retries_taken: int = 0,
) -> None:
self._cast_to = cast_to
self._client = client
@@ -72,6 +76,7 @@ def __init__(
self._stream_cls = stream_cls
self._options = options
self.http_response = raw
+ self.retries_taken = retries_taken
@property
def headers(self) -> httpx.Headers:
@@ -187,6 +192,9 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
if cast_to == float:
return cast(R, float(response.text))
+ if cast_to == bool:
+ return cast(R, response.text.lower() == "true")
+
origin = get_origin(cast_to) or cast_to
if origin == APIResponse:
@@ -255,12 +263,10 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
class APIResponse(BaseAPIResponse[R]):
@overload
- def parse(self, *, to: type[_T]) -> _T:
- ...
+ def parse(self, *, to: type[_T]) -> _T: ...
@overload
- def parse(self) -> R:
- ...
+ def parse(self) -> R: ...
def parse(self, *, to: type[_T] | None = None) -> R | _T:
"""Returns the rich python representation of this response's data.
@@ -359,12 +365,10 @@ def iter_lines(self) -> Iterator[str]:
class AsyncAPIResponse(BaseAPIResponse[R]):
@overload
- async def parse(self, *, to: type[_T]) -> _T:
- ...
+ async def parse(self, *, to: type[_T]) -> _T: ...
@overload
- async def parse(self) -> R:
- ...
+ async def parse(self) -> R: ...
async def parse(self, *, to: type[_T] | None = None) -> R | _T:
"""Returns the rich python representation of this response's data.
diff --git a/src/maisa/_types.py b/src/maisa/_types.py
index 94a83c8a..a865073d 100644
--- a/src/maisa/_types.py
+++ b/src/maisa/_types.py
@@ -16,7 +16,7 @@
Optional,
Sequence,
)
-from typing_extensions import Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable
+from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable
import httpx
import pydantic
@@ -111,8 +111,7 @@ class NotGiven:
For example:
```py
- def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response:
- ...
+ def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ...
get(timeout=1) # 1s timeout
@@ -162,16 +161,14 @@ def build(
*,
response: Response,
data: object,
- ) -> _T:
- ...
+ ) -> _T: ...
Headers = Mapping[str, Union[str, Omit]]
class HeadersLikeProtocol(Protocol):
- def get(self, __key: str) -> str | None:
- ...
+ def get(self, __key: str) -> str | None: ...
HeadersLike = Union[Headers, HeadersLikeProtocol]
@@ -196,7 +193,9 @@ def get(self, __key: str) -> str | None:
# Note: copied from Pydantic
# https://github.com/pydantic/pydantic/blob/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49
-IncEx: TypeAlias = "set[int] | set[str] | dict[int, Any] | dict[str, Any] | None"
+IncEx: TypeAlias = Union[
+ Set[int], Set[str], Mapping[int, Union["IncEx", Literal[True]]], Mapping[str, Union["IncEx", Literal[True]]]
+]
PostParser = Callable[[Any], Any]
diff --git a/src/maisa/_utils/__init__.py b/src/maisa/_utils/__init__.py
index 31b5b227..a7cff3c0 100644
--- a/src/maisa/_utils/__init__.py
+++ b/src/maisa/_utils/__init__.py
@@ -6,6 +6,7 @@
is_list as is_list,
is_given as is_given,
is_tuple as is_tuple,
+ json_safe as json_safe,
lru_cache as lru_cache,
is_mapping as is_mapping,
is_tuple_t as is_tuple_t,
@@ -49,3 +50,7 @@
maybe_transform as maybe_transform,
async_maybe_transform as async_maybe_transform,
)
+from ._reflection import (
+ function_has_argument as function_has_argument,
+ assert_signatures_in_sync as assert_signatures_in_sync,
+)
diff --git a/src/maisa/_utils/_proxy.py b/src/maisa/_utils/_proxy.py
index c46a62a6..ffd883e9 100644
--- a/src/maisa/_utils/_proxy.py
+++ b/src/maisa/_utils/_proxy.py
@@ -59,5 +59,4 @@ def __as_proxied__(self) -> T:
return cast(T, self)
@abstractmethod
- def __load__(self) -> T:
- ...
+ def __load__(self) -> T: ...
diff --git a/src/maisa/_utils/_reflection.py b/src/maisa/_utils/_reflection.py
new file mode 100644
index 00000000..89aa712a
--- /dev/null
+++ b/src/maisa/_utils/_reflection.py
@@ -0,0 +1,42 @@
+from __future__ import annotations
+
+import inspect
+from typing import Any, Callable
+
+
+def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool:
+ """Returns whether or not the given function has a specific parameter"""
+ sig = inspect.signature(func)
+ return arg_name in sig.parameters
+
+
+def assert_signatures_in_sync(
+ source_func: Callable[..., Any],
+ check_func: Callable[..., Any],
+ *,
+ exclude_params: set[str] = set(),
+) -> None:
+ """Ensure that the signature of the second function matches the first."""
+
+ check_sig = inspect.signature(check_func)
+ source_sig = inspect.signature(source_func)
+
+ errors: list[str] = []
+
+ for name, source_param in source_sig.parameters.items():
+ if name in exclude_params:
+ continue
+
+ custom_param = check_sig.parameters.get(name)
+ if not custom_param:
+ errors.append(f"the `{name}` param is missing")
+ continue
+
+ if custom_param.annotation != source_param.annotation:
+ errors.append(
+ f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(custom_param.annotation)}"
+ )
+ continue
+
+ if errors:
+ raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors))
diff --git a/src/maisa/_utils/_sync.py b/src/maisa/_utils/_sync.py
index 595924e5..d0d81033 100644
--- a/src/maisa/_utils/_sync.py
+++ b/src/maisa/_utils/_sync.py
@@ -7,6 +7,8 @@
import anyio
import anyio.to_thread
+from ._reflection import function_has_argument
+
T_Retval = TypeVar("T_Retval")
T_ParamSpec = ParamSpec("T_ParamSpec")
@@ -59,6 +61,21 @@ def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str:
async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval:
partial_f = functools.partial(function, *args, **kwargs)
- return await anyio.to_thread.run_sync(partial_f, cancellable=cancellable, limiter=limiter)
+
+ # In `v4.1.0` anyio added the `abandon_on_cancel` argument and deprecated the old
+ # `cancellable` argument, so we need to use the new `abandon_on_cancel` to avoid
+ # surfacing deprecation warnings.
+ if function_has_argument(anyio.to_thread.run_sync, "abandon_on_cancel"):
+ return await anyio.to_thread.run_sync(
+ partial_f,
+ abandon_on_cancel=cancellable,
+ limiter=limiter,
+ )
+
+ return await anyio.to_thread.run_sync(
+ partial_f,
+ cancellable=cancellable,
+ limiter=limiter,
+ )
return wrapper
diff --git a/src/maisa/_utils/_transform.py b/src/maisa/_utils/_transform.py
index 47e262a5..d7c05345 100644
--- a/src/maisa/_utils/_transform.py
+++ b/src/maisa/_utils/_transform.py
@@ -173,6 +173,11 @@ def _transform_recursive(
# Iterable[T]
or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str))
):
+ # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually
+ # intended as an iterable, so we don't transform it.
+ if isinstance(data, dict):
+ return cast(object, data)
+
inner_type = extract_type_arg(stripped_type, 0)
return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data]
@@ -186,7 +191,7 @@ def _transform_recursive(
return data
if isinstance(data, pydantic.BaseModel):
- return model_dump(data, exclude_unset=True)
+ return model_dump(data, exclude_unset=True, mode="json")
annotated_type = _get_annotated_type(annotation)
if annotated_type is None:
@@ -324,7 +329,7 @@ async def _async_transform_recursive(
return data
if isinstance(data, pydantic.BaseModel):
- return model_dump(data, exclude_unset=True)
+ return model_dump(data, exclude_unset=True, mode="json")
annotated_type = _get_annotated_type(annotation)
if annotated_type is None:
diff --git a/src/maisa/_utils/_utils.py b/src/maisa/_utils/_utils.py
index 17904ce6..e5811bba 100644
--- a/src/maisa/_utils/_utils.py
+++ b/src/maisa/_utils/_utils.py
@@ -16,11 +16,12 @@
overload,
)
from pathlib import Path
+from datetime import date, datetime
from typing_extensions import TypeGuard
import sniffio
-from .._types import Headers, NotGiven, FileTypes, NotGivenOr, HeadersLike
+from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike
from .._compat import parse_date as parse_date, parse_datetime as parse_datetime
_T = TypeVar("_T")
@@ -211,20 +212,17 @@ def required_args(*variants: Sequence[str]) -> Callable[[CallableT], CallableT]:
Example usage:
```py
@overload
- def foo(*, a: str) -> str:
- ...
+ def foo(*, a: str) -> str: ...
@overload
- def foo(*, b: bool) -> str:
- ...
+ def foo(*, b: bool) -> str: ...
# This enforces the same constraints that a static type checker would
# i.e. that either a or b must be passed to the function
@required_args(["a"], ["b"])
- def foo(*, a: str | None = None, b: bool | None = None) -> str:
- ...
+ def foo(*, a: str | None = None, b: bool | None = None) -> str: ...
```
"""
@@ -286,18 +284,15 @@ def wrapper(*args: object, **kwargs: object) -> object:
@overload
-def strip_not_given(obj: None) -> None:
- ...
+def strip_not_given(obj: None) -> None: ...
@overload
-def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]:
- ...
+def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]: ...
@overload
-def strip_not_given(obj: object) -> object:
- ...
+def strip_not_given(obj: object) -> object: ...
def strip_not_given(obj: object | None) -> object:
@@ -369,13 +364,13 @@ def file_from_path(path: str) -> FileTypes:
def get_required_header(headers: HeadersLike, header: str) -> str:
lower_header = header.lower()
- if isinstance(headers, Mapping):
- headers = cast(Headers, headers)
- for k, v in headers.items():
+ if is_mapping_t(headers):
+ # mypy doesn't understand the type narrowing here
+ for k, v in headers.items(): # type: ignore
if k.lower() == lower_header and isinstance(v, str):
return v
- """ to deal with the case where the header looks like Stainless-Event-Id """
+ # to deal with the case where the header looks like Stainless-Event-Id
intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize())
for normalized_header in [header, lower_header, header.upper(), intercaps_header]:
@@ -401,3 +396,19 @@ def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]:
maxsize=maxsize,
)
return cast(Any, wrapper) # type: ignore[no-any-return]
+
+
+def json_safe(data: object) -> object:
+ """Translates a mapping / sequence recursively in the same fashion
+ as `pydantic` v2's `model_dump(mode="json")`.
+ """
+ if is_mapping(data):
+ return {json_safe(key): json_safe(value) for key, value in data.items()}
+
+ if is_iterable(data) and not isinstance(data, (str, bytes, bytearray)):
+ return [json_safe(item) for item in data]
+
+ if isinstance(data, (datetime, date)):
+ return data.isoformat()
+
+ return data
diff --git a/src/maisa/resources/capabilities/capabilities.py b/src/maisa/resources/capabilities/capabilities.py
index e8989be5..7c760b4e 100644
--- a/src/maisa/resources/capabilities/capabilities.py
+++ b/src/maisa/resources/capabilities/capabilities.py
@@ -29,9 +29,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..._base_client import (
- make_request_options,
-)
+from ..._base_client import make_request_options
from ...types.shared.text_summary import TextSummary
from ...types.shared.text_extractor import TextExtractor
from ...types.shared.text_comparator import TextComparator
@@ -46,10 +44,21 @@ def media(self) -> MediaResource:
@cached_property
def with_raw_response(self) -> CapabilitiesResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return CapabilitiesResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> CapabilitiesResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return CapabilitiesResourceWithStreamingResponse(self)
def compare(
@@ -224,10 +233,21 @@ def media(self) -> AsyncMediaResource:
@cached_property
def with_raw_response(self) -> AsyncCapabilitiesResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return AsyncCapabilitiesResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> AsyncCapabilitiesResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return AsyncCapabilitiesResourceWithStreamingResponse(self)
async def compare(
diff --git a/src/maisa/resources/capabilities/media.py b/src/maisa/resources/capabilities/media.py
index 04e41a0e..f9287395 100644
--- a/src/maisa/resources/capabilities/media.py
+++ b/src/maisa/resources/capabilities/media.py
@@ -22,9 +22,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..._base_client import (
- make_request_options,
-)
+from ..._base_client import make_request_options
from ...types.capabilities import media_compare_params, media_extract_params, media_summarize_params
from ...types.shared.text_summary import TextSummary
from ...types.shared.text_extractor import TextExtractor
@@ -36,10 +34,21 @@
class MediaResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> MediaResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return MediaResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> MediaResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return MediaResourceWithStreamingResponse(self)
def compare(
@@ -133,11 +142,10 @@ def compare(
}
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file1"], ["file2"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return self._post(
"/v1/capabilities/compare/media",
body=maybe_transform(body, media_compare_params.MediaCompareParams),
@@ -233,11 +241,10 @@ def extract(
}
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return self._post(
"/v1/capabilities/extract/media",
body=maybe_transform(body, media_extract_params.MediaExtractParams),
@@ -297,11 +304,10 @@ def summarize(
}
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return self._post(
"/v1/capabilities/summarize/media",
body=maybe_transform(body, media_summarize_params.MediaSummarizeParams),
@@ -316,10 +322,21 @@ def summarize(
class AsyncMediaResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncMediaResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return AsyncMediaResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> AsyncMediaResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return AsyncMediaResourceWithStreamingResponse(self)
async def compare(
@@ -413,11 +430,10 @@ async def compare(
}
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file1"], ["file2"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return await self._post(
"/v1/capabilities/compare/media",
body=await async_maybe_transform(body, media_compare_params.MediaCompareParams),
@@ -513,11 +529,10 @@ async def extract(
}
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return await self._post(
"/v1/capabilities/extract/media",
body=await async_maybe_transform(body, media_extract_params.MediaExtractParams),
@@ -577,11 +592,10 @@ async def summarize(
}
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return await self._post(
"/v1/capabilities/summarize/media",
body=await async_maybe_transform(body, media_summarize_params.MediaSummarizeParams),
diff --git a/src/maisa/resources/file_interpreter/file_interpreter.py b/src/maisa/resources/file_interpreter/file_interpreter.py
index f94c2a6e..6519a992 100644
--- a/src/maisa/resources/file_interpreter/file_interpreter.py
+++ b/src/maisa/resources/file_interpreter/file_interpreter.py
@@ -71,10 +71,21 @@ def from_audio(self) -> FromAudioResource:
@cached_property
def with_raw_response(self) -> FileInterpreterResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return FileInterpreterResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> FileInterpreterResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return FileInterpreterResourceWithStreamingResponse(self)
@@ -101,10 +112,21 @@ def from_audio(self) -> AsyncFromAudioResource:
@cached_property
def with_raw_response(self) -> AsyncFileInterpreterResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return AsyncFileInterpreterResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> AsyncFileInterpreterResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return AsyncFileInterpreterResourceWithStreamingResponse(self)
diff --git a/src/maisa/resources/file_interpreter/from_audio.py b/src/maisa/resources/file_interpreter/from_audio.py
index 96399bcd..73c52414 100644
--- a/src/maisa/resources/file_interpreter/from_audio.py
+++ b/src/maisa/resources/file_interpreter/from_audio.py
@@ -21,9 +21,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..._base_client import (
- make_request_options,
-)
+from ..._base_client import make_request_options
from ...types.file_interpreter import from_audio_create_params
__all__ = ["FromAudioResource", "AsyncFromAudioResource"]
@@ -32,10 +30,21 @@
class FromAudioResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> FromAudioResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return FromAudioResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> FromAudioResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return FromAudioResourceWithStreamingResponse(self)
def create(
@@ -63,11 +72,10 @@ def create(
"""
body = deepcopy_minimal({"file": file})
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return self._post(
"/v1/file-interpreter/from-audio",
body=maybe_transform(body, from_audio_create_params.FromAudioCreateParams),
@@ -82,10 +90,21 @@ def create(
class AsyncFromAudioResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncFromAudioResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return AsyncFromAudioResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> AsyncFromAudioResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return AsyncFromAudioResourceWithStreamingResponse(self)
async def create(
@@ -113,11 +132,10 @@ async def create(
"""
body = deepcopy_minimal({"file": file})
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return await self._post(
"/v1/file-interpreter/from-audio",
body=await async_maybe_transform(body, from_audio_create_params.FromAudioCreateParams),
diff --git a/src/maisa/resources/file_interpreter/from_docx.py b/src/maisa/resources/file_interpreter/from_docx.py
index eec6adaf..ca0b6ee0 100644
--- a/src/maisa/resources/file_interpreter/from_docx.py
+++ b/src/maisa/resources/file_interpreter/from_docx.py
@@ -21,9 +21,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..._base_client import (
- make_request_options,
-)
+from ..._base_client import make_request_options
from ...types.file_interpreter import from_docx_create_params
__all__ = ["FromDocxResource", "AsyncFromDocxResource"]
@@ -32,10 +30,21 @@
class FromDocxResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> FromDocxResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return FromDocxResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> FromDocxResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return FromDocxResourceWithStreamingResponse(self)
def create(
@@ -63,11 +72,10 @@ def create(
"""
body = deepcopy_minimal({"file": file})
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return self._post(
"/v1/file-interpreter/from-docx",
body=maybe_transform(body, from_docx_create_params.FromDocxCreateParams),
@@ -82,10 +90,21 @@ def create(
class AsyncFromDocxResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncFromDocxResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return AsyncFromDocxResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> AsyncFromDocxResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return AsyncFromDocxResourceWithStreamingResponse(self)
async def create(
@@ -113,11 +132,10 @@ async def create(
"""
body = deepcopy_minimal({"file": file})
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return await self._post(
"/v1/file-interpreter/from-docx",
body=await async_maybe_transform(body, from_docx_create_params.FromDocxCreateParams),
diff --git a/src/maisa/resources/file_interpreter/from_html.py b/src/maisa/resources/file_interpreter/from_html.py
index 166bb9a6..ba998348 100644
--- a/src/maisa/resources/file_interpreter/from_html.py
+++ b/src/maisa/resources/file_interpreter/from_html.py
@@ -21,9 +21,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..._base_client import (
- make_request_options,
-)
+from ..._base_client import make_request_options
from ...types.file_interpreter import from_html_create_params
__all__ = ["FromHTMLResource", "AsyncFromHTMLResource"]
@@ -32,10 +30,21 @@
class FromHTMLResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> FromHTMLResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return FromHTMLResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> FromHTMLResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return FromHTMLResourceWithStreamingResponse(self)
def create(
@@ -63,11 +72,10 @@ def create(
"""
body = deepcopy_minimal({"file": file})
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return self._post(
"/v1/file-interpreter/from-html",
body=maybe_transform(body, from_html_create_params.FromHTMLCreateParams),
@@ -82,10 +90,21 @@ def create(
class AsyncFromHTMLResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncFromHTMLResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return AsyncFromHTMLResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> AsyncFromHTMLResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return AsyncFromHTMLResourceWithStreamingResponse(self)
async def create(
@@ -113,11 +132,10 @@ async def create(
"""
body = deepcopy_minimal({"file": file})
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return await self._post(
"/v1/file-interpreter/from-html",
body=await async_maybe_transform(body, from_html_create_params.FromHTMLCreateParams),
diff --git a/src/maisa/resources/file_interpreter/from_image.py b/src/maisa/resources/file_interpreter/from_image.py
index a35ab876..bc967a10 100644
--- a/src/maisa/resources/file_interpreter/from_image.py
+++ b/src/maisa/resources/file_interpreter/from_image.py
@@ -21,9 +21,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..._base_client import (
- make_request_options,
-)
+from ..._base_client import make_request_options
from ...types.file_interpreter import from_image_create_params
__all__ = ["FromImageResource", "AsyncFromImageResource"]
@@ -32,10 +30,21 @@
class FromImageResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> FromImageResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return FromImageResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> FromImageResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return FromImageResourceWithStreamingResponse(self)
def create(
@@ -63,11 +72,10 @@ def create(
"""
body = deepcopy_minimal({"file": file})
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return self._post(
"/v1/file-interpreter/from-image",
body=maybe_transform(body, from_image_create_params.FromImageCreateParams),
@@ -82,10 +90,21 @@ def create(
class AsyncFromImageResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncFromImageResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return AsyncFromImageResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> AsyncFromImageResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return AsyncFromImageResourceWithStreamingResponse(self)
async def create(
@@ -113,11 +132,10 @@ async def create(
"""
body = deepcopy_minimal({"file": file})
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return await self._post(
"/v1/file-interpreter/from-image",
body=await async_maybe_transform(body, from_image_create_params.FromImageCreateParams),
diff --git a/src/maisa/resources/file_interpreter/from_pdf.py b/src/maisa/resources/file_interpreter/from_pdf.py
index 9889625a..d8f2a691 100644
--- a/src/maisa/resources/file_interpreter/from_pdf.py
+++ b/src/maisa/resources/file_interpreter/from_pdf.py
@@ -21,9 +21,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..._base_client import (
- make_request_options,
-)
+from ..._base_client import make_request_options
from ...types.file_interpreter import from_pdf_create_params
__all__ = ["FromPdfResource", "AsyncFromPdfResource"]
@@ -32,10 +30,21 @@
class FromPdfResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> FromPdfResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return FromPdfResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> FromPdfResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return FromPdfResourceWithStreamingResponse(self)
def create(
@@ -64,11 +73,10 @@ def create(
"""
body = deepcopy_minimal({"file": file})
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return self._post(
"/v1/file-interpreter/from-pdf",
body=maybe_transform(body, from_pdf_create_params.FromPdfCreateParams),
@@ -87,10 +95,21 @@ def create(
class AsyncFromPdfResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncFromPdfResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return AsyncFromPdfResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> AsyncFromPdfResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return AsyncFromPdfResourceWithStreamingResponse(self)
async def create(
@@ -119,11 +138,10 @@ async def create(
"""
body = deepcopy_minimal({"file": file})
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return await self._post(
"/v1/file-interpreter/from-pdf",
body=await async_maybe_transform(body, from_pdf_create_params.FromPdfCreateParams),
diff --git a/src/maisa/resources/kpu.py b/src/maisa/resources/kpu.py
index de7e243f..c6b22de2 100644
--- a/src/maisa/resources/kpu.py
+++ b/src/maisa/resources/kpu.py
@@ -23,9 +23,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from .._base_client import (
- make_request_options,
-)
+from .._base_client import make_request_options
__all__ = ["KpuResource", "AsyncKpuResource"]
@@ -33,10 +31,21 @@
class KpuResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> KpuResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return KpuResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> KpuResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return KpuResourceWithStreamingResponse(self)
def run(
@@ -101,11 +110,10 @@ def run(
}
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file", ""]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return self._post(
"/v1/kpu/run",
body=maybe_transform(body, kpu_run_params.KpuRunParams),
@@ -130,10 +138,21 @@ def run(
class AsyncKpuResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncKpuResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return AsyncKpuResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> AsyncKpuResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return AsyncKpuResourceWithStreamingResponse(self)
async def run(
@@ -198,11 +217,10 @@ async def run(
}
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file", ""]])
- if files:
- # It should be noted that the actual Content-Type header that will be
- # sent to the server will contain a `boundary` parameter, e.g.
- # multipart/form-data; boundary=---abc--
- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return await self._post(
"/v1/kpu/run",
body=await async_maybe_transform(body, kpu_run_params.KpuRunParams),
diff --git a/src/maisa/resources/models/embeddings.py b/src/maisa/resources/models/embeddings.py
index 427afddd..15f0aa17 100644
--- a/src/maisa/resources/models/embeddings.py
+++ b/src/maisa/resources/models/embeddings.py
@@ -19,9 +19,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..._base_client import (
- make_request_options,
-)
+from ..._base_client import make_request_options
from ...types.models import embedding_create_params
from ...types.models.embeddings import Embeddings
@@ -31,10 +29,21 @@
class EmbeddingsResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> EmbeddingsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return EmbeddingsResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> EmbeddingsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return EmbeddingsResourceWithStreamingResponse(self)
def create(
@@ -75,10 +84,21 @@ def create(
class AsyncEmbeddingsResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncEmbeddingsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return AsyncEmbeddingsResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> AsyncEmbeddingsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return AsyncEmbeddingsResourceWithStreamingResponse(self)
async def create(
diff --git a/src/maisa/resources/models/models.py b/src/maisa/resources/models/models.py
index c03c3690..04e974a9 100644
--- a/src/maisa/resources/models/models.py
+++ b/src/maisa/resources/models/models.py
@@ -23,10 +23,21 @@ def embeddings(self) -> EmbeddingsResource:
@cached_property
def with_raw_response(self) -> ModelsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return ModelsResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> ModelsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return ModelsResourceWithStreamingResponse(self)
@@ -37,10 +48,21 @@ def embeddings(self) -> AsyncEmbeddingsResource:
@cached_property
def with_raw_response(self) -> AsyncModelsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return the
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
+ """
return AsyncModelsResourceWithRawResponse(self)
@cached_property
def with_streaming_response(self) -> AsyncModelsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/maisaai/python-sdk#with_streaming_response
+ """
return AsyncModelsResourceWithStreamingResponse(self)
diff --git a/src/maisa/types/shared/text_comparator.py b/src/maisa/types/shared/text_comparator.py
index 1a052b1d..646f30ad 100644
--- a/src/maisa/types/shared/text_comparator.py
+++ b/src/maisa/types/shared/text_comparator.py
@@ -1,7 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
from ..._models import BaseModel
__all__ = ["TextComparator"]
diff --git a/src/maisa/types/shared/text_extractor.py b/src/maisa/types/shared/text_extractor.py
index 70d694a1..d58d0905 100644
--- a/src/maisa/types/shared/text_extractor.py
+++ b/src/maisa/types/shared/text_extractor.py
@@ -1,7 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
from ..._models import BaseModel
__all__ = ["TextExtractor"]
diff --git a/src/maisa/types/shared/text_summary.py b/src/maisa/types/shared/text_summary.py
index 91e2e375..86d1c11a 100644
--- a/src/maisa/types/shared/text_summary.py
+++ b/src/maisa/types/shared/text_summary.py
@@ -1,7 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
from ..._models import BaseModel
__all__ = ["TextSummary"]
diff --git a/tests/api_resources/test_capabilities.py b/tests/api_resources/test_capabilities.py
index c5df9a99..724f1f6d 100644
--- a/tests/api_resources/test_capabilities.py
+++ b/tests/api_resources/test_capabilities.py
@@ -24,8 +24,8 @@ def test_method_compare(self, client: Maisa) -> None:
text2="Sed ut perspiciatis unde omnis",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
)
@@ -38,8 +38,8 @@ def test_method_compare_with_all_params(self, client: Maisa) -> None:
text2="Sed ut perspiciatis unde omnis",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
lang="en",
@@ -54,8 +54,8 @@ def test_raw_response_compare(self, client: Maisa) -> None:
text2="Sed ut perspiciatis unde omnis",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
)
@@ -72,8 +72,8 @@ def test_streaming_response_compare(self, client: Maisa) -> None:
text2="Sed ut perspiciatis unde omnis",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
) as response:
@@ -91,8 +91,8 @@ def test_method_extract(self, client: Maisa) -> None:
text="Example long text...",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
)
@@ -104,8 +104,8 @@ def test_method_extract_with_all_params(self, client: Maisa) -> None:
text="Example long text...",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
lang="en",
@@ -118,8 +118,8 @@ def test_raw_response_extract(self, client: Maisa) -> None:
text="Example long text...",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
)
@@ -135,8 +135,8 @@ def test_streaming_response_extract(self, client: Maisa) -> None:
text="Example long text...",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
) as response:
@@ -161,7 +161,7 @@ def test_method_summarize_with_all_params(self, client: Maisa) -> None:
text="Example long text...",
format="paragraph",
lang="en",
- length="medium",
+ length="short",
summary_hint="Example summary of the text...",
)
assert_matches_type(TextSummary, capability, path=["response"])
@@ -201,8 +201,8 @@ async def test_method_compare(self, async_client: AsyncMaisa) -> None:
text2="Sed ut perspiciatis unde omnis",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
)
@@ -215,8 +215,8 @@ async def test_method_compare_with_all_params(self, async_client: AsyncMaisa) ->
text2="Sed ut perspiciatis unde omnis",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
lang="en",
@@ -231,8 +231,8 @@ async def test_raw_response_compare(self, async_client: AsyncMaisa) -> None:
text2="Sed ut perspiciatis unde omnis",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
)
@@ -249,8 +249,8 @@ async def test_streaming_response_compare(self, async_client: AsyncMaisa) -> Non
text2="Sed ut perspiciatis unde omnis",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
) as response:
@@ -268,8 +268,8 @@ async def test_method_extract(self, async_client: AsyncMaisa) -> None:
text="Example long text...",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
)
@@ -281,8 +281,8 @@ async def test_method_extract_with_all_params(self, async_client: AsyncMaisa) ->
text="Example long text...",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
lang="en",
@@ -295,8 +295,8 @@ async def test_raw_response_extract(self, async_client: AsyncMaisa) -> None:
text="Example long text...",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
)
@@ -312,8 +312,8 @@ async def test_streaming_response_extract(self, async_client: AsyncMaisa) -> Non
text="Example long text...",
variables={
"name": {
- "type": "string",
"description": "The name of the person.",
+ "type": "string",
}
},
) as response:
@@ -338,7 +338,7 @@ async def test_method_summarize_with_all_params(self, async_client: AsyncMaisa)
text="Example long text...",
format="paragraph",
lang="en",
- length="medium",
+ length="short",
summary_hint="Example summary of the text...",
)
assert_matches_type(TextSummary, capability, path=["response"])
diff --git a/tests/api_resources/test_kpu.py b/tests/api_resources/test_kpu.py
index a9f9f1be..f916db66 100644
--- a/tests/api_resources/test_kpu.py
+++ b/tests/api_resources/test_kpu.py
@@ -19,26 +19,26 @@ class TestKpu:
@parametrize
def test_method_run(self, client: Maisa) -> None:
kpu = client.kpu.run(
- query="string",
+ query="query",
)
assert_matches_type(object, kpu, path=["response"])
@parametrize
def test_method_run_with_all_params(self, client: Maisa) -> None:
kpu = client.kpu.run(
- query="string",
+ query="query",
explain_steps=True,
retries=1,
file=[b"raw file contents", b"raw file contents", b"raw file contents"],
reasoner_model="gpt-4-turbo",
- reasoner_prompt="string",
+ reasoner_prompt="reasoner_prompt",
)
assert_matches_type(object, kpu, path=["response"])
@parametrize
def test_raw_response_run(self, client: Maisa) -> None:
response = client.kpu.with_raw_response.run(
- query="string",
+ query="query",
)
assert response.is_closed is True
@@ -49,7 +49,7 @@ def test_raw_response_run(self, client: Maisa) -> None:
@parametrize
def test_streaming_response_run(self, client: Maisa) -> None:
with client.kpu.with_streaming_response.run(
- query="string",
+ query="query",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -66,26 +66,26 @@ class TestAsyncKpu:
@parametrize
async def test_method_run(self, async_client: AsyncMaisa) -> None:
kpu = await async_client.kpu.run(
- query="string",
+ query="query",
)
assert_matches_type(object, kpu, path=["response"])
@parametrize
async def test_method_run_with_all_params(self, async_client: AsyncMaisa) -> None:
kpu = await async_client.kpu.run(
- query="string",
+ query="query",
explain_steps=True,
retries=1,
file=[b"raw file contents", b"raw file contents", b"raw file contents"],
reasoner_model="gpt-4-turbo",
- reasoner_prompt="string",
+ reasoner_prompt="reasoner_prompt",
)
assert_matches_type(object, kpu, path=["response"])
@parametrize
async def test_raw_response_run(self, async_client: AsyncMaisa) -> None:
response = await async_client.kpu.with_raw_response.run(
- query="string",
+ query="query",
)
assert response.is_closed is True
@@ -96,7 +96,7 @@ async def test_raw_response_run(self, async_client: AsyncMaisa) -> None:
@parametrize
async def test_streaming_response_run(self, async_client: AsyncMaisa) -> None:
async with async_client.kpu.with_streaming_response.run(
- query="string",
+ query="query",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
diff --git a/tests/conftest.py b/tests/conftest.py
index a4bcb401..e4a04e40 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,11 +1,11 @@
from __future__ import annotations
import os
-import asyncio
import logging
from typing import TYPE_CHECKING, Iterator, AsyncIterator
import pytest
+from pytest_asyncio import is_async_test
from maisa import Maisa, AsyncMaisa
@@ -17,11 +17,13 @@
logging.getLogger("maisa").setLevel(logging.DEBUG)
-@pytest.fixture(scope="session")
-def event_loop() -> Iterator[asyncio.AbstractEventLoop]:
- loop = asyncio.new_event_loop()
- yield loop
- loop.close()
+# automatically add `pytest.mark.asyncio()` to all of our async tests
+# so we don't have to add that boilerplate everywhere
+def pytest_collection_modifyitems(items: list[pytest.Function]) -> None:
+ pytest_asyncio_tests = (item for item in items if is_async_test(item))
+ session_scope_marker = pytest.mark.asyncio(loop_scope="session")
+ for async_test in pytest_asyncio_tests:
+ async_test.add_marker(session_scope_marker, append=False)
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
diff --git a/tests/test_client.py b/tests/test_client.py
index aa708a4c..f75c5e2f 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -10,6 +10,7 @@
import tracemalloc
from typing import Any, Union, cast
from unittest import mock
+from typing_extensions import Literal
import httpx
import pytest
@@ -17,6 +18,7 @@
from pydantic import ValidationError
from maisa import Maisa, AsyncMaisa, APIResponseValidationError
+from maisa._types import Omit
from maisa._models import BaseModel, FinalRequestOptions
from maisa._constants import RAW_RESPONSE_HEADER
from maisa._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError
@@ -674,6 +676,7 @@ class Model(BaseModel):
[3, "", 0.5],
[2, "", 0.5 * 2.0],
[1, "", 0.5 * 4.0],
+ [-1100, "", 7.8], # test large number potentially overflowing
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
@@ -715,6 +718,85 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non
assert _get_open_connections(self.client) == 0
+ @pytest.mark.parametrize("failures_before_success", [0, 2, 4])
+ @mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
+ @pytest.mark.respx(base_url=base_url)
+ @pytest.mark.parametrize("failure_mode", ["status", "exception"])
+ def test_retries_taken(
+ self,
+ client: Maisa,
+ failures_before_success: int,
+ failure_mode: Literal["status", "exception"],
+ respx_mock: MockRouter,
+ ) -> None:
+ client = client.with_options(max_retries=4)
+
+ nb_retries = 0
+
+ def retry_handler(_request: httpx.Request) -> httpx.Response:
+ nonlocal nb_retries
+ if nb_retries < failures_before_success:
+ nb_retries += 1
+ if failure_mode == "exception":
+ raise RuntimeError("oops")
+ return httpx.Response(500)
+ return httpx.Response(200)
+
+ respx_mock.post("/v1/capabilities/summarize").mock(side_effect=retry_handler)
+
+ response = client.capabilities.with_raw_response.summarize(text="Example long text...")
+
+ assert response.retries_taken == failures_before_success
+ assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
+
+ @pytest.mark.parametrize("failures_before_success", [0, 2, 4])
+ @mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
+ @pytest.mark.respx(base_url=base_url)
+ def test_omit_retry_count_header(self, client: Maisa, failures_before_success: int, respx_mock: MockRouter) -> None:
+ client = client.with_options(max_retries=4)
+
+ nb_retries = 0
+
+ def retry_handler(_request: httpx.Request) -> httpx.Response:
+ nonlocal nb_retries
+ if nb_retries < failures_before_success:
+ nb_retries += 1
+ return httpx.Response(500)
+ return httpx.Response(200)
+
+ respx_mock.post("/v1/capabilities/summarize").mock(side_effect=retry_handler)
+
+ response = client.capabilities.with_raw_response.summarize(
+ text="Example long text...", extra_headers={"x-stainless-retry-count": Omit()}
+ )
+
+ assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0
+
+ @pytest.mark.parametrize("failures_before_success", [0, 2, 4])
+ @mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
+ @pytest.mark.respx(base_url=base_url)
+ def test_overwrite_retry_count_header(
+ self, client: Maisa, failures_before_success: int, respx_mock: MockRouter
+ ) -> None:
+ client = client.with_options(max_retries=4)
+
+ nb_retries = 0
+
+ def retry_handler(_request: httpx.Request) -> httpx.Response:
+ nonlocal nb_retries
+ if nb_retries < failures_before_success:
+ nb_retries += 1
+ return httpx.Response(500)
+ return httpx.Response(200)
+
+ respx_mock.post("/v1/capabilities/summarize").mock(side_effect=retry_handler)
+
+ response = client.capabilities.with_raw_response.summarize(
+ text="Example long text...", extra_headers={"x-stainless-retry-count": "42"}
+ )
+
+ assert response.http_request.headers.get("x-stainless-retry-count") == "42"
+
class TestAsyncMaisa:
client = AsyncMaisa(base_url=base_url, api_key=api_key, _strict_response_validation=True)
@@ -1359,6 +1441,7 @@ class Model(BaseModel):
[3, "", 0.5],
[2, "", 0.5 * 2.0],
[1, "", 0.5 * 4.0],
+ [-1100, "", 7.8], # test large number potentially overflowing
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
@@ -1400,3 +1483,87 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter)
)
assert _get_open_connections(self.client) == 0
+
+ @pytest.mark.parametrize("failures_before_success", [0, 2, 4])
+ @mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
+ @pytest.mark.respx(base_url=base_url)
+ @pytest.mark.asyncio
+ @pytest.mark.parametrize("failure_mode", ["status", "exception"])
+ async def test_retries_taken(
+ self,
+ async_client: AsyncMaisa,
+ failures_before_success: int,
+ failure_mode: Literal["status", "exception"],
+ respx_mock: MockRouter,
+ ) -> None:
+ client = async_client.with_options(max_retries=4)
+
+ nb_retries = 0
+
+ def retry_handler(_request: httpx.Request) -> httpx.Response:
+ nonlocal nb_retries
+ if nb_retries < failures_before_success:
+ nb_retries += 1
+ if failure_mode == "exception":
+ raise RuntimeError("oops")
+ return httpx.Response(500)
+ return httpx.Response(200)
+
+ respx_mock.post("/v1/capabilities/summarize").mock(side_effect=retry_handler)
+
+ response = await client.capabilities.with_raw_response.summarize(text="Example long text...")
+
+ assert response.retries_taken == failures_before_success
+ assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
+
+ @pytest.mark.parametrize("failures_before_success", [0, 2, 4])
+ @mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
+ @pytest.mark.respx(base_url=base_url)
+ @pytest.mark.asyncio
+ async def test_omit_retry_count_header(
+ self, async_client: AsyncMaisa, failures_before_success: int, respx_mock: MockRouter
+ ) -> None:
+ client = async_client.with_options(max_retries=4)
+
+ nb_retries = 0
+
+ def retry_handler(_request: httpx.Request) -> httpx.Response:
+ nonlocal nb_retries
+ if nb_retries < failures_before_success:
+ nb_retries += 1
+ return httpx.Response(500)
+ return httpx.Response(200)
+
+ respx_mock.post("/v1/capabilities/summarize").mock(side_effect=retry_handler)
+
+ response = await client.capabilities.with_raw_response.summarize(
+ text="Example long text...", extra_headers={"x-stainless-retry-count": Omit()}
+ )
+
+ assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0
+
+ @pytest.mark.parametrize("failures_before_success", [0, 2, 4])
+ @mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
+ @pytest.mark.respx(base_url=base_url)
+ @pytest.mark.asyncio
+ async def test_overwrite_retry_count_header(
+ self, async_client: AsyncMaisa, failures_before_success: int, respx_mock: MockRouter
+ ) -> None:
+ client = async_client.with_options(max_retries=4)
+
+ nb_retries = 0
+
+ def retry_handler(_request: httpx.Request) -> httpx.Response:
+ nonlocal nb_retries
+ if nb_retries < failures_before_success:
+ nb_retries += 1
+ return httpx.Response(500)
+ return httpx.Response(200)
+
+ respx_mock.post("/v1/capabilities/summarize").mock(side_effect=retry_handler)
+
+ response = await client.capabilities.with_raw_response.summarize(
+ text="Example long text...", extra_headers={"x-stainless-retry-count": "42"}
+ )
+
+ assert response.http_request.headers.get("x-stainless-retry-count") == "42"
diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py
index a33fd8b8..3697c8c9 100644
--- a/tests/test_deepcopy.py
+++ b/tests/test_deepcopy.py
@@ -41,8 +41,7 @@ def test_nested_list() -> None:
assert_different_identities(obj1[1], obj2[1])
-class MyObject:
- ...
+class MyObject: ...
def test_ignores_other_types() -> None:
diff --git a/tests/test_models.py b/tests/test_models.py
index 9e18d54e..0dbcbc18 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -31,7 +31,7 @@ class NestedModel(BaseModel):
# mismatched types
m = NestedModel.construct(nested="hello!")
- assert m.nested == "hello!"
+ assert cast(Any, m.nested) == "hello!"
def test_optional_nested_model() -> None:
@@ -48,7 +48,7 @@ class NestedModel(BaseModel):
# mismatched types
m3 = NestedModel.construct(nested={"foo"})
assert isinstance(cast(Any, m3.nested), set)
- assert m3.nested == {"foo"}
+ assert cast(Any, m3.nested) == {"foo"}
def test_list_nested_model() -> None:
@@ -245,7 +245,7 @@ class Model(BaseModel):
assert m.foo is True
m = Model.construct(foo="CARD_HOLDER")
- assert m.foo is "CARD_HOLDER"
+ assert m.foo == "CARD_HOLDER"
m = Model.construct(foo={"bar": False})
assert isinstance(m.foo, Submodel1)
@@ -323,7 +323,7 @@ class Model(BaseModel):
assert len(m.items) == 2
assert isinstance(m.items[0], Submodel1)
assert m.items[0].level == -1
- assert m.items[1] == 156
+ assert cast(Any, m.items[1]) == 156
def test_union_of_lists() -> None:
@@ -355,7 +355,7 @@ class Model(BaseModel):
assert len(m.items) == 2
assert isinstance(m.items[0], SubModel1)
assert m.items[0].level == -1
- assert m.items[1] == 156
+ assert cast(Any, m.items[1]) == 156
def test_dict_of_union() -> None:
@@ -520,19 +520,15 @@ class Model(BaseModel):
assert m3.to_dict(exclude_none=True) == {}
assert m3.to_dict(exclude_defaults=True) == {}
- if PYDANTIC_V2:
-
- class Model2(BaseModel):
- created_at: datetime
+ class Model2(BaseModel):
+ created_at: datetime
- time_str = "2024-03-21T11:39:01.275859"
- m4 = Model2.construct(created_at=time_str)
- assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)}
- assert m4.to_dict(mode="json") == {"created_at": time_str}
- else:
- with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"):
- m.to_dict(mode="json")
+ time_str = "2024-03-21T11:39:01.275859"
+ m4 = Model2.construct(created_at=time_str)
+ assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)}
+ assert m4.to_dict(mode="json") == {"created_at": time_str}
+ if not PYDANTIC_V2:
with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"):
m.to_dict(warnings=False)
@@ -558,9 +554,6 @@ class Model(BaseModel):
assert m3.model_dump(exclude_none=True) == {}
if not PYDANTIC_V2:
- with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"):
- m.model_dump(mode="json")
-
with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"):
m.model_dump(round_trip=True)
diff --git a/tests/test_response.py b/tests/test_response.py
index eebb8027..9c56eceb 100644
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -1,5 +1,5 @@
import json
-from typing import List, cast
+from typing import Any, List, Union, cast
from typing_extensions import Annotated
import httpx
@@ -19,16 +19,13 @@
from maisa._base_client import FinalRequestOptions
-class ConcreteBaseAPIResponse(APIResponse[bytes]):
- ...
+class ConcreteBaseAPIResponse(APIResponse[bytes]): ...
-class ConcreteAPIResponse(APIResponse[List[str]]):
- ...
+class ConcreteAPIResponse(APIResponse[List[str]]): ...
-class ConcreteAsyncAPIResponse(APIResponse[httpx.Response]):
- ...
+class ConcreteAsyncAPIResponse(APIResponse[httpx.Response]): ...
def test_extract_response_type_direct_classes() -> None:
@@ -56,8 +53,7 @@ def test_extract_response_type_binary_response() -> None:
assert extract_response_type(AsyncBinaryAPIResponse) == bytes
-class PydanticModel(pydantic.BaseModel):
- ...
+class PydanticModel(pydantic.BaseModel): ...
def test_response_parse_mismatched_basemodel(client: Maisa) -> None:
@@ -192,3 +188,90 @@ async def test_async_response_parse_annotated_type(async_client: AsyncMaisa) ->
)
assert obj.foo == "hello!"
assert obj.bar == 2
+
+
+@pytest.mark.parametrize(
+ "content, expected",
+ [
+ ("false", False),
+ ("true", True),
+ ("False", False),
+ ("True", True),
+ ("TrUe", True),
+ ("FalSe", False),
+ ],
+)
+def test_response_parse_bool(client: Maisa, content: str, expected: bool) -> None:
+ response = APIResponse(
+ raw=httpx.Response(200, content=content),
+ client=client,
+ stream=False,
+ stream_cls=None,
+ cast_to=str,
+ options=FinalRequestOptions.construct(method="get", url="/foo"),
+ )
+
+ result = response.parse(to=bool)
+ assert result is expected
+
+
+@pytest.mark.parametrize(
+ "content, expected",
+ [
+ ("false", False),
+ ("true", True),
+ ("False", False),
+ ("True", True),
+ ("TrUe", True),
+ ("FalSe", False),
+ ],
+)
+async def test_async_response_parse_bool(client: AsyncMaisa, content: str, expected: bool) -> None:
+ response = AsyncAPIResponse(
+ raw=httpx.Response(200, content=content),
+ client=client,
+ stream=False,
+ stream_cls=None,
+ cast_to=str,
+ options=FinalRequestOptions.construct(method="get", url="/foo"),
+ )
+
+ result = await response.parse(to=bool)
+ assert result is expected
+
+
+class OtherModel(BaseModel):
+ a: str
+
+
+@pytest.mark.parametrize("client", [False], indirect=True) # loose validation
+def test_response_parse_expect_model_union_non_json_content(client: Maisa) -> None:
+ response = APIResponse(
+ raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}),
+ client=client,
+ stream=False,
+ stream_cls=None,
+ cast_to=str,
+ options=FinalRequestOptions.construct(method="get", url="/foo"),
+ )
+
+ obj = response.parse(to=cast(Any, Union[CustomModel, OtherModel]))
+ assert isinstance(obj, str)
+ assert obj == "foo"
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("async_client", [False], indirect=True) # loose validation
+async def test_async_response_parse_expect_model_union_non_json_content(async_client: AsyncMaisa) -> None:
+ response = AsyncAPIResponse(
+ raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}),
+ client=async_client,
+ stream=False,
+ stream_cls=None,
+ cast_to=str,
+ options=FinalRequestOptions.construct(method="get", url="/foo"),
+ )
+
+ obj = await response.parse(to=cast(Any, Union[CustomModel, OtherModel]))
+ assert isinstance(obj, str)
+ assert obj == "foo"
diff --git a/tests/test_transform.py b/tests/test_transform.py
index 19442b4f..ce1fcc70 100644
--- a/tests/test_transform.py
+++ b/tests/test_transform.py
@@ -177,17 +177,32 @@ class DateDict(TypedDict, total=False):
foo: Annotated[date, PropertyInfo(format="iso8601")]
+class DatetimeModel(BaseModel):
+ foo: datetime
+
+
+class DateModel(BaseModel):
+ foo: Optional[date]
+
+
@parametrize
@pytest.mark.asyncio
async def test_iso8601_format(use_async: bool) -> None:
dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00")
+ tz = "Z" if PYDANTIC_V2 else "+00:00"
assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap]
+ assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692" + tz} # type: ignore[comparison-overlap]
dt = dt.replace(tzinfo=None)
assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap]
+ assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap]
assert await transform({"foo": None}, DateDict, use_async) == {"foo": None} # type: ignore[comparison-overlap]
+ assert await transform(DateModel(foo=None), Any, use_async) == {"foo": None} # type: ignore
assert await transform({"foo": date.fromisoformat("2023-02-23")}, DateDict, use_async) == {"foo": "2023-02-23"} # type: ignore[comparison-overlap]
+ assert await transform(DateModel(foo=date.fromisoformat("2023-02-23")), DateDict, use_async) == {
+ "foo": "2023-02-23"
+ } # type: ignore[comparison-overlap]
@parametrize
@@ -260,20 +275,22 @@ class MyModel(BaseModel):
@parametrize
@pytest.mark.asyncio
async def test_pydantic_model_to_dictionary(use_async: bool) -> None:
- assert await transform(MyModel(foo="hi!"), Any, use_async) == {"foo": "hi!"}
- assert await transform(MyModel.construct(foo="hi!"), Any, use_async) == {"foo": "hi!"}
+ assert cast(Any, await transform(MyModel(foo="hi!"), Any, use_async)) == {"foo": "hi!"}
+ assert cast(Any, await transform(MyModel.construct(foo="hi!"), Any, use_async)) == {"foo": "hi!"}
@parametrize
@pytest.mark.asyncio
async def test_pydantic_empty_model(use_async: bool) -> None:
- assert await transform(MyModel.construct(), Any, use_async) == {}
+ assert cast(Any, await transform(MyModel.construct(), Any, use_async)) == {}
@parametrize
@pytest.mark.asyncio
async def test_pydantic_unknown_field(use_async: bool) -> None:
- assert await transform(MyModel.construct(my_untyped_field=True), Any, use_async) == {"my_untyped_field": True}
+ assert cast(Any, await transform(MyModel.construct(my_untyped_field=True), Any, use_async)) == {
+ "my_untyped_field": True
+ }
@parametrize
@@ -285,7 +302,7 @@ async def test_pydantic_mismatched_types(use_async: bool) -> None:
params = await transform(model, Any, use_async)
else:
params = await transform(model, Any, use_async)
- assert params == {"foo": True}
+ assert cast(Any, params) == {"foo": True}
@parametrize
@@ -297,7 +314,7 @@ async def test_pydantic_mismatched_object_type(use_async: bool) -> None:
params = await transform(model, Any, use_async)
else:
params = await transform(model, Any, use_async)
- assert params == {"foo": {"hello": "world"}}
+ assert cast(Any, params) == {"foo": {"hello": "world"}}
class ModelNestedObjects(BaseModel):
@@ -309,7 +326,7 @@ class ModelNestedObjects(BaseModel):
async def test_pydantic_nested_objects(use_async: bool) -> None:
model = ModelNestedObjects.construct(nested={"foo": "stainless"})
assert isinstance(model.nested, MyModel)
- assert await transform(model, Any, use_async) == {"nested": {"foo": "stainless"}}
+ assert cast(Any, await transform(model, Any, use_async)) == {"nested": {"foo": "stainless"}}
class ModelWithDefaultField(BaseModel):
@@ -325,19 +342,19 @@ async def test_pydantic_default_field(use_async: bool) -> None:
model = ModelWithDefaultField.construct()
assert model.with_none_default is None
assert model.with_str_default == "foo"
- assert await transform(model, Any, use_async) == {}
+ assert cast(Any, await transform(model, Any, use_async)) == {}
# should be included when the default value is explicitly given
model = ModelWithDefaultField.construct(with_none_default=None, with_str_default="foo")
assert model.with_none_default is None
assert model.with_str_default == "foo"
- assert await transform(model, Any, use_async) == {"with_none_default": None, "with_str_default": "foo"}
+ assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": None, "with_str_default": "foo"}
# should be included when a non-default value is explicitly given
model = ModelWithDefaultField.construct(with_none_default="bar", with_str_default="baz")
assert model.with_none_default == "bar"
assert model.with_str_default == "baz"
- assert await transform(model, Any, use_async) == {"with_none_default": "bar", "with_str_default": "baz"}
+ assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": "bar", "with_str_default": "baz"}
class TypedDictIterableUnion(TypedDict):
diff --git a/tests/test_utils/test_typing.py b/tests/test_utils/test_typing.py
index 0c5db425..f934fafc 100644
--- a/tests/test_utils/test_typing.py
+++ b/tests/test_utils/test_typing.py
@@ -9,24 +9,19 @@
_T3 = TypeVar("_T3")
-class BaseGeneric(Generic[_T]):
- ...
+class BaseGeneric(Generic[_T]): ...
-class SubclassGeneric(BaseGeneric[_T]):
- ...
+class SubclassGeneric(BaseGeneric[_T]): ...
-class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]):
- ...
+class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): ...
-class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]):
- ...
+class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): ...
-class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]):
- ...
+class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): ...
def test_extract_type_var() -> None:
diff --git a/tests/utils.py b/tests/utils.py
index 9fa6ba7a..88d3e2b5 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -8,7 +8,7 @@
from datetime import date, datetime
from typing_extensions import Literal, get_args, get_origin, assert_type
-from maisa._types import NoneType
+from maisa._types import Omit, NoneType
from maisa._utils import (
is_dict,
is_list,
@@ -139,11 +139,15 @@ def _assert_list_type(type_: type[object], value: object) -> None:
@contextlib.contextmanager
-def update_env(**new_env: str) -> Iterator[None]:
+def update_env(**new_env: str | Omit) -> Iterator[None]:
old = os.environ.copy()
try:
- os.environ.update(new_env)
+ for name, value in new_env.items():
+ if isinstance(value, Omit):
+ os.environ.pop(name, None)
+ else:
+ os.environ[name] = value
yield None
finally:
From 7c494c538e949dbccf6bcfa09a05c0119af5c4e8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 6 Nov 2024 15:52:22 +0000
Subject: [PATCH 005/200] chore: rebuild project due to codegen change (#23)
---
tests/test_client.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/test_client.py b/tests/test_client.py
index f75c5e2f..01b47a20 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -676,7 +676,7 @@ class Model(BaseModel):
[3, "", 0.5],
[2, "", 0.5 * 2.0],
[1, "", 0.5 * 4.0],
- [-1100, "", 7.8], # test large number potentially overflowing
+ [-1100, "", 8], # test large number potentially overflowing
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
@@ -1441,7 +1441,7 @@ class Model(BaseModel):
[3, "", 0.5],
[2, "", 0.5 * 2.0],
[1, "", 0.5 * 4.0],
- [-1100, "", 7.8], # test large number potentially overflowing
+ [-1100, "", 8], # test large number potentially overflowing
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
From 552ed440c47d95c939942439344e3da2907b272e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 12 Nov 2024 13:18:43 +0000
Subject: [PATCH 006/200] chore: rebuild project due to codegen change (#24)
---
src/maisa/_utils/_transform.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/maisa/_utils/_transform.py b/src/maisa/_utils/_transform.py
index d7c05345..a6b62cad 100644
--- a/src/maisa/_utils/_transform.py
+++ b/src/maisa/_utils/_transform.py
@@ -316,6 +316,11 @@ async def _async_transform_recursive(
# Iterable[T]
or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str))
):
+ # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually
+ # intended as an iterable, so we don't transform it.
+ if isinstance(data, dict):
+ return cast(object, data)
+
inner_type = extract_type_arg(stripped_type, 0)
return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data]
From 4c06923e146784772fc5198485e0b46c336beb25 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 18 Nov 2024 10:57:28 +0000
Subject: [PATCH 007/200] chore: rebuild project due to codegen change (#25)
---
tests/api_resources/test_kpu.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/api_resources/test_kpu.py b/tests/api_resources/test_kpu.py
index f916db66..c5c5115d 100644
--- a/tests/api_resources/test_kpu.py
+++ b/tests/api_resources/test_kpu.py
@@ -29,7 +29,7 @@ def test_method_run_with_all_params(self, client: Maisa) -> None:
query="query",
explain_steps=True,
retries=1,
- file=[b"raw file contents", b"raw file contents", b"raw file contents"],
+ file=[b"raw file contents"],
reasoner_model="gpt-4-turbo",
reasoner_prompt="reasoner_prompt",
)
@@ -76,7 +76,7 @@ async def test_method_run_with_all_params(self, async_client: AsyncMaisa) -> Non
query="query",
explain_steps=True,
retries=1,
- file=[b"raw file contents", b"raw file contents", b"raw file contents"],
+ file=[b"raw file contents"],
reasoner_model="gpt-4-turbo",
reasoner_prompt="reasoner_prompt",
)
From 2414a9e7d43f960247dc09534b429c7525838854 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 18 Nov 2024 12:54:20 +0000
Subject: [PATCH 008/200] chore: rebuild project due to codegen change (#26)
---
pyproject.toml | 1 +
requirements-dev.lock | 1 +
src/maisa/_utils/_sync.py | 90 +++++++++++++++++----------------------
tests/test_client.py | 38 +++++++++++++++++
4 files changed, 80 insertions(+), 50 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 1720adab..fec3a735 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -55,6 +55,7 @@ dev-dependencies = [
"dirty-equals>=0.6.0",
"importlib-metadata>=6.7.0",
"rich>=13.7.1",
+ "nest_asyncio==1.6.0"
]
[tool.rye.scripts]
diff --git a/requirements-dev.lock b/requirements-dev.lock
index aab8a26d..9bb950f3 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -51,6 +51,7 @@ mdurl==0.1.2
mypy==1.13.0
mypy-extensions==1.0.0
# via mypy
+nest-asyncio==1.6.0
nodeenv==1.8.0
# via pyright
nox==2023.4.22
diff --git a/src/maisa/_utils/_sync.py b/src/maisa/_utils/_sync.py
index d0d81033..8b3aaf2b 100644
--- a/src/maisa/_utils/_sync.py
+++ b/src/maisa/_utils/_sync.py
@@ -1,56 +1,62 @@
from __future__ import annotations
+import sys
+import asyncio
import functools
-from typing import TypeVar, Callable, Awaitable
+import contextvars
+from typing import Any, TypeVar, Callable, Awaitable
from typing_extensions import ParamSpec
-import anyio
-import anyio.to_thread
-
-from ._reflection import function_has_argument
-
T_Retval = TypeVar("T_Retval")
T_ParamSpec = ParamSpec("T_ParamSpec")
-# copied from `asyncer`, https://github.com/tiangolo/asyncer
-def asyncify(
- function: Callable[T_ParamSpec, T_Retval],
- *,
- cancellable: bool = False,
- limiter: anyio.CapacityLimiter | None = None,
-) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
+if sys.version_info >= (3, 9):
+ to_thread = asyncio.to_thread
+else:
+ # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
+ # for Python 3.8 support
+ async def to_thread(
+ func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
+ ) -> Any:
+ """Asynchronously run function *func* in a separate thread.
+
+ Any *args and **kwargs supplied for this function are directly passed
+ to *func*. Also, the current :class:`contextvars.Context` is propagated,
+ allowing context variables from the main thread to be accessed in the
+ separate thread.
+
+ Returns a coroutine that can be awaited to get the eventual result of *func*.
+ """
+ loop = asyncio.events.get_running_loop()
+ ctx = contextvars.copy_context()
+ func_call = functools.partial(ctx.run, func, *args, **kwargs)
+ return await loop.run_in_executor(None, func_call)
+
+
+# inspired by `asyncer`, https://github.com/tiangolo/asyncer
+def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
"""
Take a blocking function and create an async one that receives the same
- positional and keyword arguments, and that when called, calls the original function
- in a worker thread using `anyio.to_thread.run_sync()`. Internally,
- `asyncer.asyncify()` uses the same `anyio.to_thread.run_sync()`, but it supports
- keyword arguments additional to positional arguments and it adds better support for
- autocompletion and inline errors for the arguments of the function called and the
- return value.
-
- If the `cancellable` option is enabled and the task waiting for its completion is
- cancelled, the thread will still run its course but its return value (or any raised
- exception) will be ignored.
+ positional and keyword arguments. For python version 3.9 and above, it uses
+ asyncio.to_thread to run the function in a separate thread. For python version
+ 3.8, it uses locally defined copy of the asyncio.to_thread function which was
+ introduced in python 3.9.
- Use it like this:
+ Usage:
- ```Python
- def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str:
- # Do work
- return "Some result"
+ ```python
+ def blocking_func(arg1, arg2, kwarg1=None):
+ # blocking code
+ return result
- result = await to_thread.asyncify(do_work)("spam", "ham", kwarg1="a", kwarg2="b")
- print(result)
+ result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1)
```
## Arguments
`function`: a blocking regular callable (e.g. a function)
- `cancellable`: `True` to allow cancellation of the operation
- `limiter`: capacity limiter to use to limit the total amount of threads running
- (if omitted, the default limiter is used)
## Return
@@ -60,22 +66,6 @@ def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str:
"""
async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval:
- partial_f = functools.partial(function, *args, **kwargs)
-
- # In `v4.1.0` anyio added the `abandon_on_cancel` argument and deprecated the old
- # `cancellable` argument, so we need to use the new `abandon_on_cancel` to avoid
- # surfacing deprecation warnings.
- if function_has_argument(anyio.to_thread.run_sync, "abandon_on_cancel"):
- return await anyio.to_thread.run_sync(
- partial_f,
- abandon_on_cancel=cancellable,
- limiter=limiter,
- )
-
- return await anyio.to_thread.run_sync(
- partial_f,
- cancellable=cancellable,
- limiter=limiter,
- )
+ return await to_thread(function, *args, **kwargs)
return wrapper
diff --git a/tests/test_client.py b/tests/test_client.py
index 01b47a20..254af41d 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -4,11 +4,14 @@
import gc
import os
+import sys
import json
import asyncio
import inspect
+import subprocess
import tracemalloc
from typing import Any, Union, cast
+from textwrap import dedent
from unittest import mock
from typing_extensions import Literal
@@ -1567,3 +1570,38 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
)
assert response.http_request.headers.get("x-stainless-retry-count") == "42"
+
+ def test_get_platform(self) -> None:
+ # A previous implementation of asyncify could leave threads unterminated when
+ # used with nest_asyncio.
+ #
+ # Since nest_asyncio.apply() is global and cannot be un-applied, this
+ # test is run in a separate process to avoid affecting other tests.
+ test_code = dedent("""
+ import asyncio
+ import nest_asyncio
+ import threading
+
+ from maisa._utils import asyncify
+ from maisa._base_client import get_platform
+
+ async def test_main() -> None:
+ result = await asyncify(get_platform)()
+ print(result)
+ for thread in threading.enumerate():
+ print(thread.name)
+
+ nest_asyncio.apply()
+ asyncio.run(test_main())
+ """)
+ with subprocess.Popen(
+ [sys.executable, "-c", test_code],
+ text=True,
+ ) as process:
+ try:
+ process.wait(2)
+ if process.returncode:
+ raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code")
+ except subprocess.TimeoutExpired as e:
+ process.kill()
+ raise AssertionError("calling get_platform using asyncify resulted in a hung process") from e
From d354f4a0dea8c17c8cb9d3651f726f92f8a7b81d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 22 Nov 2024 20:31:53 +0000
Subject: [PATCH 009/200] chore(internal): fix compat model_dump method when
warnings are passed (#27)
---
src/maisa/_compat.py | 3 ++-
tests/test_models.py | 8 ++++++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/maisa/_compat.py b/src/maisa/_compat.py
index 4794129c..df173f85 100644
--- a/src/maisa/_compat.py
+++ b/src/maisa/_compat.py
@@ -145,7 +145,8 @@ def model_dump(
exclude=exclude,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
- warnings=warnings,
+ # warnings are not supported in Pydantic v1
+ warnings=warnings if PYDANTIC_V2 else True,
)
return cast(
"dict[str, Any]",
diff --git a/tests/test_models.py b/tests/test_models.py
index 0dbcbc18..c472419f 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -561,6 +561,14 @@ class Model(BaseModel):
m.model_dump(warnings=False)
+def test_compat_method_no_error_for_warnings() -> None:
+ class Model(BaseModel):
+ foo: Optional[str]
+
+ m = Model(foo="hello")
+ assert isinstance(model_dump(m, warnings=False), dict)
+
+
def test_to_json() -> None:
class Model(BaseModel):
foo: Optional[str] = Field(alias="FOO", default=None)
From ac2169019646303ec6c9eff1da3e07c2b71a6115 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 22 Nov 2024 20:33:39 +0000
Subject: [PATCH 010/200] docs: add info log level to readme (#28)
---
README.md | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index c82f836f..c416d316 100644
--- a/README.md
+++ b/README.md
@@ -172,12 +172,14 @@ Note that requests that time out are [retried twice by default](#retries).
We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module.
-You can enable logging by setting the environment variable `MAISA_LOG` to `debug`.
+You can enable logging by setting the environment variable `MAISA_LOG` to `info`.
```shell
-$ export MAISA_LOG=debug
+$ export MAISA_LOG=info
```
+Or to `debug` for more verbose logging.
+
### How to tell whether `None` means `null` or missing
In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`:
From 5c8985c2e5fa6621d4801f4c3ccf44df58af0369 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 26 Nov 2024 05:42:26 +0000
Subject: [PATCH 011/200] chore: remove now unused `cached-property` dep (#29)
---
pyproject.toml | 1 -
src/maisa/_compat.py | 5 +----
2 files changed, 1 insertion(+), 5 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index fec3a735..cc3c1bc0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,7 +14,6 @@ dependencies = [
"anyio>=3.5.0, <5",
"distro>=1.7.0, <2",
"sniffio",
- "cached-property; python_version < '3.8'",
]
requires-python = ">= 3.8"
classifiers = [
diff --git a/src/maisa/_compat.py b/src/maisa/_compat.py
index df173f85..92d9ee61 100644
--- a/src/maisa/_compat.py
+++ b/src/maisa/_compat.py
@@ -214,9 +214,6 @@ def __set_name__(self, owner: type[Any], name: str) -> None: ...
# __set__ is not defined at runtime, but @cached_property is designed to be settable
def __set__(self, instance: object, value: _T) -> None: ...
else:
- try:
- from functools import cached_property as cached_property
- except ImportError:
- from cached_property import cached_property as cached_property
+ from functools import cached_property as cached_property
typed_cached_property = cached_property
From 3bcf1c92cd0d4614f3e433a875ff5c57deef884a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 28 Nov 2024 05:40:14 +0000
Subject: [PATCH 012/200] chore(internal): exclude mypy from running on tests
(#30)
---
mypy.ini | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/mypy.ini b/mypy.ini
index 25bbcf77..080f3e6c 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -5,7 +5,10 @@ show_error_codes = True
# Exclude _files.py because mypy isn't smart enough to apply
# the correct type narrowing and as this is an internal module
# it's fine to just use Pyright.
-exclude = ^(src/maisa/_files\.py|_dev/.*\.py)$
+#
+# We also exclude our `tests` as mypy doesn't always infer
+# types correctly and Pyright will still catch any type errors.
+exclude = ^(src/maisa/_files\.py|_dev/.*\.py|tests/.*)$
strict_equality = True
implicit_reexport = True
From 3494f25077638c28d293cae09a251c6d6a75fd0e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 28 Nov 2024 18:51:15 +0000
Subject: [PATCH 013/200] fix(client): compat with new httpx 0.28.0 release
(#31)
---
src/maisa/_base_client.py | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index c5a8b7d2..5ea08e6a 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -792,6 +792,7 @@ def __init__(
custom_query: Mapping[str, object] | None = None,
_strict_response_validation: bool,
) -> None:
+ kwargs: dict[str, Any] = {}
if limits is not None:
warnings.warn(
"The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead",
@@ -804,6 +805,7 @@ def __init__(
limits = DEFAULT_CONNECTION_LIMITS
if transport is not None:
+ kwargs["transport"] = transport
warnings.warn(
"The `transport` argument is deprecated. The `http_client` argument should be passed instead",
category=DeprecationWarning,
@@ -813,6 +815,7 @@ def __init__(
raise ValueError("The `http_client` argument is mutually exclusive with `transport`")
if proxies is not None:
+ kwargs["proxies"] = proxies
warnings.warn(
"The `proxies` argument is deprecated. The `http_client` argument should be passed instead",
category=DeprecationWarning,
@@ -856,10 +859,9 @@ def __init__(
base_url=base_url,
# cast to a valid type because mypy doesn't understand our type narrowing
timeout=cast(Timeout, timeout),
- proxies=proxies,
- transport=transport,
limits=limits,
follow_redirects=True,
+ **kwargs, # type: ignore
)
def is_closed(self) -> bool:
@@ -1358,6 +1360,7 @@ def __init__(
custom_headers: Mapping[str, str] | None = None,
custom_query: Mapping[str, object] | None = None,
) -> None:
+ kwargs: dict[str, Any] = {}
if limits is not None:
warnings.warn(
"The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead",
@@ -1370,6 +1373,7 @@ def __init__(
limits = DEFAULT_CONNECTION_LIMITS
if transport is not None:
+ kwargs["transport"] = transport
warnings.warn(
"The `transport` argument is deprecated. The `http_client` argument should be passed instead",
category=DeprecationWarning,
@@ -1379,6 +1383,7 @@ def __init__(
raise ValueError("The `http_client` argument is mutually exclusive with `transport`")
if proxies is not None:
+ kwargs["proxies"] = proxies
warnings.warn(
"The `proxies` argument is deprecated. The `http_client` argument should be passed instead",
category=DeprecationWarning,
@@ -1422,10 +1427,9 @@ def __init__(
base_url=base_url,
# cast to a valid type because mypy doesn't understand our type narrowing
timeout=cast(Timeout, timeout),
- proxies=proxies,
- transport=transport,
limits=limits,
follow_redirects=True,
+ **kwargs, # type: ignore
)
def is_closed(self) -> bool:
From 0ecc7ec95c31105611aa6abd11f230654e961009 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 3 Dec 2024 05:59:04 +0000
Subject: [PATCH 014/200] chore(internal): bump pyright (#32)
---
requirements-dev.lock | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 9bb950f3..a26f1346 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -68,7 +68,7 @@ pydantic-core==2.23.4
# via pydantic
pygments==2.18.0
# via rich
-pyright==1.1.380
+pyright==1.1.389
pytest==8.3.3
# via pytest-asyncio
pytest-asyncio==0.24.0
@@ -97,6 +97,7 @@ typing-extensions==4.12.2
# via mypy
# via pydantic
# via pydantic-core
+ # via pyright
virtualenv==20.24.5
# via nox
zipp==3.17.0
From 72de3060418356f0b460f9b138a158e56eceb2a9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 4 Dec 2024 05:57:07 +0000
Subject: [PATCH 015/200] chore: make the `Omit` type public (#33)
---
src/maisa/__init__.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/maisa/__init__.py b/src/maisa/__init__.py
index dce249d9..6c1e07e4 100644
--- a/src/maisa/__init__.py
+++ b/src/maisa/__init__.py
@@ -1,7 +1,7 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from . import types
-from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes
+from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes
from ._utils import file_from_path
from ._client import Maisa, Client, Stream, Timeout, Transport, AsyncMaisa, AsyncClient, AsyncStream, RequestOptions
from ._models import BaseModel
@@ -36,6 +36,7 @@
"ProxiesTypes",
"NotGiven",
"NOT_GIVEN",
+ "Omit",
"MaisaError",
"APIError",
"APIStatusError",
From 1b764fdebbf53d067b1c208e9c9c124cab1ab76d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 10 Dec 2024 05:44:00 +0000
Subject: [PATCH 016/200] chore(internal): bump pydantic dependency (#34)
---
requirements-dev.lock | 4 ++--
requirements.lock | 4 ++--
src/maisa/_types.py | 6 ++----
3 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/requirements-dev.lock b/requirements-dev.lock
index a26f1346..b106037b 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -62,9 +62,9 @@ platformdirs==3.11.0
# via virtualenv
pluggy==1.5.0
# via pytest
-pydantic==2.9.2
+pydantic==2.10.3
# via maisa
-pydantic-core==2.23.4
+pydantic-core==2.27.1
# via pydantic
pygments==2.18.0
# via rich
diff --git a/requirements.lock b/requirements.lock
index 46d20886..6154e101 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -30,9 +30,9 @@ httpx==0.25.2
idna==3.4
# via anyio
# via httpx
-pydantic==2.9.2
+pydantic==2.10.3
# via maisa
-pydantic-core==2.23.4
+pydantic-core==2.27.1
# via pydantic
sniffio==1.3.0
# via anyio
diff --git a/src/maisa/_types.py b/src/maisa/_types.py
index a865073d..9bc2b62d 100644
--- a/src/maisa/_types.py
+++ b/src/maisa/_types.py
@@ -192,10 +192,8 @@ def get(self, __key: str) -> str | None: ...
StrBytesIntFloat = Union[str, bytes, int, float]
# Note: copied from Pydantic
-# https://github.com/pydantic/pydantic/blob/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49
-IncEx: TypeAlias = Union[
- Set[int], Set[str], Mapping[int, Union["IncEx", Literal[True]]], Mapping[str, Union["IncEx", Literal[True]]]
-]
+# https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79
+IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]]
PostParser = Callable[[Any], Any]
From 90fa18a81017f3ee361bcdb1a1f5bc1164f20052 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 10 Dec 2024 05:47:58 +0000
Subject: [PATCH 017/200] docs(readme): fix http client proxies example (#35)
---
README.md | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index c416d316..d0a64653 100644
--- a/README.md
+++ b/README.md
@@ -270,18 +270,19 @@ can also get all the extra fields on the Pydantic model as a dict with
You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including:
-- Support for proxies
-- Custom transports
+- Support for [proxies](https://www.python-httpx.org/advanced/proxies/)
+- Custom [transports](https://www.python-httpx.org/advanced/transports/)
- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality
```python
+import httpx
from maisa import Maisa, DefaultHttpxClient
client = Maisa(
# Or use the `MAISA_BASE_URL` env var
base_url="http://my.test.server.example.com:8083",
http_client=DefaultHttpxClient(
- proxies="http://my.test.proxy.example.com",
+ proxy="http://my.test.proxy.example.com",
transport=httpx.HTTPTransport(local_address="0.0.0.0"),
),
)
From a12e8fbc0c7aa7e8e40280305ef08b382e091d34 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 13 Dec 2024 05:52:46 +0000
Subject: [PATCH 018/200] chore(internal): bump pyright (#36)
---
requirements-dev.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements-dev.lock b/requirements-dev.lock
index b106037b..608452ba 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -68,7 +68,7 @@ pydantic-core==2.27.1
# via pydantic
pygments==2.18.0
# via rich
-pyright==1.1.389
+pyright==1.1.390
pytest==8.3.3
# via pytest-asyncio
pytest-asyncio==0.24.0
From c4f353f07fdb0a7f1a8b737f85973509c6aa6bb7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 13 Dec 2024 05:53:27 +0000
Subject: [PATCH 019/200] chore(internal): add support for TypeAliasType (#37)
---
pyproject.toml | 2 +-
src/maisa/_models.py | 3 +++
src/maisa/_response.py | 20 ++++++++++----------
src/maisa/_utils/__init__.py | 1 +
src/maisa/_utils/_typing.py | 31 ++++++++++++++++++++++++++++++-
tests/test_models.py | 18 +++++++++++++++++-
tests/utils.py | 4 ++++
7 files changed, 66 insertions(+), 13 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index cc3c1bc0..e0c86c18 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,7 +10,7 @@ authors = [
dependencies = [
"httpx>=0.23.0, <1",
"pydantic>=1.9.0, <3",
- "typing-extensions>=4.7, <5",
+ "typing-extensions>=4.10, <5",
"anyio>=3.5.0, <5",
"distro>=1.7.0, <2",
"sniffio",
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index 6cb469e2..7a547ce5 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -46,6 +46,7 @@
strip_not_given,
extract_type_arg,
is_annotated_type,
+ is_type_alias_type,
strip_annotated_type,
)
from ._compat import (
@@ -428,6 +429,8 @@ def construct_type(*, value: object, type_: object) -> object:
# we allow `object` as the input type because otherwise, passing things like
# `Literal['value']` will be reported as a type error by type checkers
type_ = cast("type[object]", type_)
+ if is_type_alias_type(type_):
+ type_ = type_.__value__ # type: ignore[unreachable]
# unwrap `Annotated[T, ...]` -> `T`
if is_annotated_type(type_):
diff --git a/src/maisa/_response.py b/src/maisa/_response.py
index 48f1a4ab..13e1ca35 100644
--- a/src/maisa/_response.py
+++ b/src/maisa/_response.py
@@ -25,7 +25,7 @@
import pydantic
from ._types import NoneType
-from ._utils import is_given, extract_type_arg, is_annotated_type, extract_type_var_from_base
+from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base
from ._models import BaseModel, is_basemodel
from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER
from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type
@@ -126,9 +126,15 @@ def __repr__(self) -> str:
)
def _parse(self, *, to: type[_T] | None = None) -> R | _T:
+ cast_to = to if to is not None else self._cast_to
+
+ # unwrap `TypeAlias('Name', T)` -> `T`
+ if is_type_alias_type(cast_to):
+ cast_to = cast_to.__value__ # type: ignore[unreachable]
+
# unwrap `Annotated[T, ...]` -> `T`
- if to and is_annotated_type(to):
- to = extract_type_arg(to, 0)
+ if cast_to and is_annotated_type(cast_to):
+ cast_to = extract_type_arg(cast_to, 0)
if self._is_sse_stream:
if to:
@@ -164,18 +170,12 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
return cast(
R,
stream_cls(
- cast_to=self._cast_to,
+ cast_to=cast_to,
response=self.http_response,
client=cast(Any, self._client),
),
)
- cast_to = to if to is not None else self._cast_to
-
- # unwrap `Annotated[T, ...]` -> `T`
- if is_annotated_type(cast_to):
- cast_to = extract_type_arg(cast_to, 0)
-
if cast_to is NoneType:
return cast(R, None)
diff --git a/src/maisa/_utils/__init__.py b/src/maisa/_utils/__init__.py
index a7cff3c0..d4fda26f 100644
--- a/src/maisa/_utils/__init__.py
+++ b/src/maisa/_utils/__init__.py
@@ -39,6 +39,7 @@
is_iterable_type as is_iterable_type,
is_required_type as is_required_type,
is_annotated_type as is_annotated_type,
+ is_type_alias_type as is_type_alias_type,
strip_annotated_type as strip_annotated_type,
extract_type_var_from_base as extract_type_var_from_base,
)
diff --git a/src/maisa/_utils/_typing.py b/src/maisa/_utils/_typing.py
index c036991f..278749b1 100644
--- a/src/maisa/_utils/_typing.py
+++ b/src/maisa/_utils/_typing.py
@@ -1,8 +1,17 @@
from __future__ import annotations
+import sys
+import typing
+import typing_extensions
from typing import Any, TypeVar, Iterable, cast
from collections import abc as _c_abc
-from typing_extensions import Required, Annotated, get_args, get_origin
+from typing_extensions import (
+ TypeIs,
+ Required,
+ Annotated,
+ get_args,
+ get_origin,
+)
from .._types import InheritsGeneric
from .._compat import is_union as _is_union
@@ -36,6 +45,26 @@ def is_typevar(typ: type) -> bool:
return type(typ) == TypeVar # type: ignore
+_TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,)
+if sys.version_info >= (3, 12):
+ _TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType)
+
+
+def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]:
+ """Return whether the provided argument is an instance of `TypeAliasType`.
+
+ ```python
+ type Int = int
+ is_type_alias_type(Int)
+ # > True
+ Str = TypeAliasType("Str", str)
+ is_type_alias_type(Str)
+ # > True
+ ```
+ """
+ return isinstance(tp, _TYPE_ALIAS_TYPES)
+
+
# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]]
def strip_annotated_type(typ: type) -> type:
if is_required_type(typ) or is_annotated_type(typ):
diff --git a/tests/test_models.py b/tests/test_models.py
index c472419f..27bcbc37 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,7 +1,7 @@
import json
from typing import Any, Dict, List, Union, Optional, cast
from datetime import datetime, timezone
-from typing_extensions import Literal, Annotated
+from typing_extensions import Literal, Annotated, TypeAliasType
import pytest
import pydantic
@@ -828,3 +828,19 @@ class B(BaseModel):
# if the discriminator details object stays the same between invocations then
# we hit the cache
assert UnionType.__discriminator__ is discriminator
+
+
+@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1")
+def test_type_alias_type() -> None:
+ Alias = TypeAliasType("Alias", str)
+
+ class Model(BaseModel):
+ alias: Alias
+ union: Union[int, Alias]
+
+ m = construct_type(value={"alias": "foo", "union": "bar"}, type_=Model)
+ assert isinstance(m, Model)
+ assert isinstance(m.alias, str)
+ assert m.alias == "foo"
+ assert isinstance(m.union, str)
+ assert m.union == "bar"
diff --git a/tests/utils.py b/tests/utils.py
index 88d3e2b5..4b9ed39c 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -16,6 +16,7 @@
is_union_type,
extract_type_arg,
is_annotated_type,
+ is_type_alias_type,
)
from maisa._compat import PYDANTIC_V2, field_outer_type, get_model_fields
from maisa._models import BaseModel
@@ -51,6 +52,9 @@ def assert_matches_type(
path: list[str],
allow_none: bool = False,
) -> None:
+ if is_type_alias_type(type_):
+ type_ = type_.__value__
+
# unwrap `Annotated[T, ...]` -> `T`
if is_annotated_type(type_):
type_ = extract_type_arg(type_, 0)
From 07f9a2a232504e9a4c5165e0fa8a1fc0a9cda823 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 14 Dec 2024 05:39:24 +0000
Subject: [PATCH 020/200] chore(internal): codegen related update (#38)
---
src/maisa/_client.py | 84 +++++++++++++++++++++-----------------------
1 file changed, 40 insertions(+), 44 deletions(-)
diff --git a/src/maisa/_client.py b/src/maisa/_client.py
index 5ed1eceb..de6ac769 100644
--- a/src/maisa/_client.py
+++ b/src/maisa/_client.py
@@ -8,7 +8,7 @@
import httpx
-from . import resources, _exceptions
+from . import _exceptions
from ._qs import Querystring
from ._types import (
NOT_GIVEN,
@@ -24,6 +24,7 @@
get_async_library,
)
from ._version import __version__
+from .resources import kpu
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
from ._exceptions import MaisaError, APIStatusError
from ._base_client import (
@@ -31,25 +32,18 @@
SyncAPIClient,
AsyncAPIClient,
)
+from .resources.models import models
+from .resources.capabilities import capabilities
+from .resources.file_interpreter import file_interpreter
-__all__ = [
- "Timeout",
- "Transport",
- "ProxiesTypes",
- "RequestOptions",
- "resources",
- "Maisa",
- "AsyncMaisa",
- "Client",
- "AsyncClient",
-]
+__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Maisa", "AsyncMaisa", "Client", "AsyncClient"]
class Maisa(SyncAPIClient):
- capabilities: resources.CapabilitiesResource
- models: resources.ModelsResource
- kpu: resources.KpuResource
- file_interpreter: resources.FileInterpreterResource
+ capabilities: capabilities.CapabilitiesResource
+ models: models.ModelsResource
+ kpu: kpu.KpuResource
+ file_interpreter: file_interpreter.FileInterpreterResource
with_raw_response: MaisaWithRawResponse
with_streaming_response: MaisaWithStreamedResponse
@@ -107,10 +101,10 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.capabilities = resources.CapabilitiesResource(self)
- self.models = resources.ModelsResource(self)
- self.kpu = resources.KpuResource(self)
- self.file_interpreter = resources.FileInterpreterResource(self)
+ self.capabilities = capabilities.CapabilitiesResource(self)
+ self.models = models.ModelsResource(self)
+ self.kpu = kpu.KpuResource(self)
+ self.file_interpreter = file_interpreter.FileInterpreterResource(self)
self.with_raw_response = MaisaWithRawResponse(self)
self.with_streaming_response = MaisaWithStreamedResponse(self)
@@ -220,10 +214,10 @@ def _make_status_error(
class AsyncMaisa(AsyncAPIClient):
- capabilities: resources.AsyncCapabilitiesResource
- models: resources.AsyncModelsResource
- kpu: resources.AsyncKpuResource
- file_interpreter: resources.AsyncFileInterpreterResource
+ capabilities: capabilities.AsyncCapabilitiesResource
+ models: models.AsyncModelsResource
+ kpu: kpu.AsyncKpuResource
+ file_interpreter: file_interpreter.AsyncFileInterpreterResource
with_raw_response: AsyncMaisaWithRawResponse
with_streaming_response: AsyncMaisaWithStreamedResponse
@@ -281,10 +275,10 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.capabilities = resources.AsyncCapabilitiesResource(self)
- self.models = resources.AsyncModelsResource(self)
- self.kpu = resources.AsyncKpuResource(self)
- self.file_interpreter = resources.AsyncFileInterpreterResource(self)
+ self.capabilities = capabilities.AsyncCapabilitiesResource(self)
+ self.models = models.AsyncModelsResource(self)
+ self.kpu = kpu.AsyncKpuResource(self)
+ self.file_interpreter = file_interpreter.AsyncFileInterpreterResource(self)
self.with_raw_response = AsyncMaisaWithRawResponse(self)
self.with_streaming_response = AsyncMaisaWithStreamedResponse(self)
@@ -395,34 +389,36 @@ def _make_status_error(
class MaisaWithRawResponse:
def __init__(self, client: Maisa) -> None:
- self.capabilities = resources.CapabilitiesResourceWithRawResponse(client.capabilities)
- self.models = resources.ModelsResourceWithRawResponse(client.models)
- self.kpu = resources.KpuResourceWithRawResponse(client.kpu)
- self.file_interpreter = resources.FileInterpreterResourceWithRawResponse(client.file_interpreter)
+ self.capabilities = capabilities.CapabilitiesResourceWithRawResponse(client.capabilities)
+ self.models = models.ModelsResourceWithRawResponse(client.models)
+ self.kpu = kpu.KpuResourceWithRawResponse(client.kpu)
+ self.file_interpreter = file_interpreter.FileInterpreterResourceWithRawResponse(client.file_interpreter)
class AsyncMaisaWithRawResponse:
def __init__(self, client: AsyncMaisa) -> None:
- self.capabilities = resources.AsyncCapabilitiesResourceWithRawResponse(client.capabilities)
- self.models = resources.AsyncModelsResourceWithRawResponse(client.models)
- self.kpu = resources.AsyncKpuResourceWithRawResponse(client.kpu)
- self.file_interpreter = resources.AsyncFileInterpreterResourceWithRawResponse(client.file_interpreter)
+ self.capabilities = capabilities.AsyncCapabilitiesResourceWithRawResponse(client.capabilities)
+ self.models = models.AsyncModelsResourceWithRawResponse(client.models)
+ self.kpu = kpu.AsyncKpuResourceWithRawResponse(client.kpu)
+ self.file_interpreter = file_interpreter.AsyncFileInterpreterResourceWithRawResponse(client.file_interpreter)
class MaisaWithStreamedResponse:
def __init__(self, client: Maisa) -> None:
- self.capabilities = resources.CapabilitiesResourceWithStreamingResponse(client.capabilities)
- self.models = resources.ModelsResourceWithStreamingResponse(client.models)
- self.kpu = resources.KpuResourceWithStreamingResponse(client.kpu)
- self.file_interpreter = resources.FileInterpreterResourceWithStreamingResponse(client.file_interpreter)
+ self.capabilities = capabilities.CapabilitiesResourceWithStreamingResponse(client.capabilities)
+ self.models = models.ModelsResourceWithStreamingResponse(client.models)
+ self.kpu = kpu.KpuResourceWithStreamingResponse(client.kpu)
+ self.file_interpreter = file_interpreter.FileInterpreterResourceWithStreamingResponse(client.file_interpreter)
class AsyncMaisaWithStreamedResponse:
def __init__(self, client: AsyncMaisa) -> None:
- self.capabilities = resources.AsyncCapabilitiesResourceWithStreamingResponse(client.capabilities)
- self.models = resources.AsyncModelsResourceWithStreamingResponse(client.models)
- self.kpu = resources.AsyncKpuResourceWithStreamingResponse(client.kpu)
- self.file_interpreter = resources.AsyncFileInterpreterResourceWithStreamingResponse(client.file_interpreter)
+ self.capabilities = capabilities.AsyncCapabilitiesResourceWithStreamingResponse(client.capabilities)
+ self.models = models.AsyncModelsResourceWithStreamingResponse(client.models)
+ self.kpu = kpu.AsyncKpuResourceWithStreamingResponse(client.kpu)
+ self.file_interpreter = file_interpreter.AsyncFileInterpreterResourceWithStreamingResponse(
+ client.file_interpreter
+ )
Client = Maisa
From 05260113c3701215702d9b1024ccd5c0598afcad Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 14 Dec 2024 05:40:07 +0000
Subject: [PATCH 021/200] chore(internal): codegen related update (#39)
---
src/maisa/_client.py | 84 +++++++++++++++++++++++---------------------
1 file changed, 44 insertions(+), 40 deletions(-)
diff --git a/src/maisa/_client.py b/src/maisa/_client.py
index de6ac769..5ed1eceb 100644
--- a/src/maisa/_client.py
+++ b/src/maisa/_client.py
@@ -8,7 +8,7 @@
import httpx
-from . import _exceptions
+from . import resources, _exceptions
from ._qs import Querystring
from ._types import (
NOT_GIVEN,
@@ -24,7 +24,6 @@
get_async_library,
)
from ._version import __version__
-from .resources import kpu
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
from ._exceptions import MaisaError, APIStatusError
from ._base_client import (
@@ -32,18 +31,25 @@
SyncAPIClient,
AsyncAPIClient,
)
-from .resources.models import models
-from .resources.capabilities import capabilities
-from .resources.file_interpreter import file_interpreter
-__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Maisa", "AsyncMaisa", "Client", "AsyncClient"]
+__all__ = [
+ "Timeout",
+ "Transport",
+ "ProxiesTypes",
+ "RequestOptions",
+ "resources",
+ "Maisa",
+ "AsyncMaisa",
+ "Client",
+ "AsyncClient",
+]
class Maisa(SyncAPIClient):
- capabilities: capabilities.CapabilitiesResource
- models: models.ModelsResource
- kpu: kpu.KpuResource
- file_interpreter: file_interpreter.FileInterpreterResource
+ capabilities: resources.CapabilitiesResource
+ models: resources.ModelsResource
+ kpu: resources.KpuResource
+ file_interpreter: resources.FileInterpreterResource
with_raw_response: MaisaWithRawResponse
with_streaming_response: MaisaWithStreamedResponse
@@ -101,10 +107,10 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.capabilities = capabilities.CapabilitiesResource(self)
- self.models = models.ModelsResource(self)
- self.kpu = kpu.KpuResource(self)
- self.file_interpreter = file_interpreter.FileInterpreterResource(self)
+ self.capabilities = resources.CapabilitiesResource(self)
+ self.models = resources.ModelsResource(self)
+ self.kpu = resources.KpuResource(self)
+ self.file_interpreter = resources.FileInterpreterResource(self)
self.with_raw_response = MaisaWithRawResponse(self)
self.with_streaming_response = MaisaWithStreamedResponse(self)
@@ -214,10 +220,10 @@ def _make_status_error(
class AsyncMaisa(AsyncAPIClient):
- capabilities: capabilities.AsyncCapabilitiesResource
- models: models.AsyncModelsResource
- kpu: kpu.AsyncKpuResource
- file_interpreter: file_interpreter.AsyncFileInterpreterResource
+ capabilities: resources.AsyncCapabilitiesResource
+ models: resources.AsyncModelsResource
+ kpu: resources.AsyncKpuResource
+ file_interpreter: resources.AsyncFileInterpreterResource
with_raw_response: AsyncMaisaWithRawResponse
with_streaming_response: AsyncMaisaWithStreamedResponse
@@ -275,10 +281,10 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.capabilities = capabilities.AsyncCapabilitiesResource(self)
- self.models = models.AsyncModelsResource(self)
- self.kpu = kpu.AsyncKpuResource(self)
- self.file_interpreter = file_interpreter.AsyncFileInterpreterResource(self)
+ self.capabilities = resources.AsyncCapabilitiesResource(self)
+ self.models = resources.AsyncModelsResource(self)
+ self.kpu = resources.AsyncKpuResource(self)
+ self.file_interpreter = resources.AsyncFileInterpreterResource(self)
self.with_raw_response = AsyncMaisaWithRawResponse(self)
self.with_streaming_response = AsyncMaisaWithStreamedResponse(self)
@@ -389,36 +395,34 @@ def _make_status_error(
class MaisaWithRawResponse:
def __init__(self, client: Maisa) -> None:
- self.capabilities = capabilities.CapabilitiesResourceWithRawResponse(client.capabilities)
- self.models = models.ModelsResourceWithRawResponse(client.models)
- self.kpu = kpu.KpuResourceWithRawResponse(client.kpu)
- self.file_interpreter = file_interpreter.FileInterpreterResourceWithRawResponse(client.file_interpreter)
+ self.capabilities = resources.CapabilitiesResourceWithRawResponse(client.capabilities)
+ self.models = resources.ModelsResourceWithRawResponse(client.models)
+ self.kpu = resources.KpuResourceWithRawResponse(client.kpu)
+ self.file_interpreter = resources.FileInterpreterResourceWithRawResponse(client.file_interpreter)
class AsyncMaisaWithRawResponse:
def __init__(self, client: AsyncMaisa) -> None:
- self.capabilities = capabilities.AsyncCapabilitiesResourceWithRawResponse(client.capabilities)
- self.models = models.AsyncModelsResourceWithRawResponse(client.models)
- self.kpu = kpu.AsyncKpuResourceWithRawResponse(client.kpu)
- self.file_interpreter = file_interpreter.AsyncFileInterpreterResourceWithRawResponse(client.file_interpreter)
+ self.capabilities = resources.AsyncCapabilitiesResourceWithRawResponse(client.capabilities)
+ self.models = resources.AsyncModelsResourceWithRawResponse(client.models)
+ self.kpu = resources.AsyncKpuResourceWithRawResponse(client.kpu)
+ self.file_interpreter = resources.AsyncFileInterpreterResourceWithRawResponse(client.file_interpreter)
class MaisaWithStreamedResponse:
def __init__(self, client: Maisa) -> None:
- self.capabilities = capabilities.CapabilitiesResourceWithStreamingResponse(client.capabilities)
- self.models = models.ModelsResourceWithStreamingResponse(client.models)
- self.kpu = kpu.KpuResourceWithStreamingResponse(client.kpu)
- self.file_interpreter = file_interpreter.FileInterpreterResourceWithStreamingResponse(client.file_interpreter)
+ self.capabilities = resources.CapabilitiesResourceWithStreamingResponse(client.capabilities)
+ self.models = resources.ModelsResourceWithStreamingResponse(client.models)
+ self.kpu = resources.KpuResourceWithStreamingResponse(client.kpu)
+ self.file_interpreter = resources.FileInterpreterResourceWithStreamingResponse(client.file_interpreter)
class AsyncMaisaWithStreamedResponse:
def __init__(self, client: AsyncMaisa) -> None:
- self.capabilities = capabilities.AsyncCapabilitiesResourceWithStreamingResponse(client.capabilities)
- self.models = models.AsyncModelsResourceWithStreamingResponse(client.models)
- self.kpu = kpu.AsyncKpuResourceWithStreamingResponse(client.kpu)
- self.file_interpreter = file_interpreter.AsyncFileInterpreterResourceWithStreamingResponse(
- client.file_interpreter
- )
+ self.capabilities = resources.AsyncCapabilitiesResourceWithStreamingResponse(client.capabilities)
+ self.models = resources.AsyncModelsResourceWithStreamingResponse(client.models)
+ self.kpu = resources.AsyncKpuResourceWithStreamingResponse(client.kpu)
+ self.file_interpreter = resources.AsyncFileInterpreterResourceWithStreamingResponse(client.file_interpreter)
Client = Maisa
From 601084637e5d988387413e25c0412f3b4de82c90 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 14 Dec 2024 05:54:11 +0000
Subject: [PATCH 022/200] chore(internal): codegen related update (#40)
---
src/maisa/_client.py | 84 +++++++++++++++++++++-----------------------
1 file changed, 40 insertions(+), 44 deletions(-)
diff --git a/src/maisa/_client.py b/src/maisa/_client.py
index 5ed1eceb..de6ac769 100644
--- a/src/maisa/_client.py
+++ b/src/maisa/_client.py
@@ -8,7 +8,7 @@
import httpx
-from . import resources, _exceptions
+from . import _exceptions
from ._qs import Querystring
from ._types import (
NOT_GIVEN,
@@ -24,6 +24,7 @@
get_async_library,
)
from ._version import __version__
+from .resources import kpu
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
from ._exceptions import MaisaError, APIStatusError
from ._base_client import (
@@ -31,25 +32,18 @@
SyncAPIClient,
AsyncAPIClient,
)
+from .resources.models import models
+from .resources.capabilities import capabilities
+from .resources.file_interpreter import file_interpreter
-__all__ = [
- "Timeout",
- "Transport",
- "ProxiesTypes",
- "RequestOptions",
- "resources",
- "Maisa",
- "AsyncMaisa",
- "Client",
- "AsyncClient",
-]
+__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Maisa", "AsyncMaisa", "Client", "AsyncClient"]
class Maisa(SyncAPIClient):
- capabilities: resources.CapabilitiesResource
- models: resources.ModelsResource
- kpu: resources.KpuResource
- file_interpreter: resources.FileInterpreterResource
+ capabilities: capabilities.CapabilitiesResource
+ models: models.ModelsResource
+ kpu: kpu.KpuResource
+ file_interpreter: file_interpreter.FileInterpreterResource
with_raw_response: MaisaWithRawResponse
with_streaming_response: MaisaWithStreamedResponse
@@ -107,10 +101,10 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.capabilities = resources.CapabilitiesResource(self)
- self.models = resources.ModelsResource(self)
- self.kpu = resources.KpuResource(self)
- self.file_interpreter = resources.FileInterpreterResource(self)
+ self.capabilities = capabilities.CapabilitiesResource(self)
+ self.models = models.ModelsResource(self)
+ self.kpu = kpu.KpuResource(self)
+ self.file_interpreter = file_interpreter.FileInterpreterResource(self)
self.with_raw_response = MaisaWithRawResponse(self)
self.with_streaming_response = MaisaWithStreamedResponse(self)
@@ -220,10 +214,10 @@ def _make_status_error(
class AsyncMaisa(AsyncAPIClient):
- capabilities: resources.AsyncCapabilitiesResource
- models: resources.AsyncModelsResource
- kpu: resources.AsyncKpuResource
- file_interpreter: resources.AsyncFileInterpreterResource
+ capabilities: capabilities.AsyncCapabilitiesResource
+ models: models.AsyncModelsResource
+ kpu: kpu.AsyncKpuResource
+ file_interpreter: file_interpreter.AsyncFileInterpreterResource
with_raw_response: AsyncMaisaWithRawResponse
with_streaming_response: AsyncMaisaWithStreamedResponse
@@ -281,10 +275,10 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.capabilities = resources.AsyncCapabilitiesResource(self)
- self.models = resources.AsyncModelsResource(self)
- self.kpu = resources.AsyncKpuResource(self)
- self.file_interpreter = resources.AsyncFileInterpreterResource(self)
+ self.capabilities = capabilities.AsyncCapabilitiesResource(self)
+ self.models = models.AsyncModelsResource(self)
+ self.kpu = kpu.AsyncKpuResource(self)
+ self.file_interpreter = file_interpreter.AsyncFileInterpreterResource(self)
self.with_raw_response = AsyncMaisaWithRawResponse(self)
self.with_streaming_response = AsyncMaisaWithStreamedResponse(self)
@@ -395,34 +389,36 @@ def _make_status_error(
class MaisaWithRawResponse:
def __init__(self, client: Maisa) -> None:
- self.capabilities = resources.CapabilitiesResourceWithRawResponse(client.capabilities)
- self.models = resources.ModelsResourceWithRawResponse(client.models)
- self.kpu = resources.KpuResourceWithRawResponse(client.kpu)
- self.file_interpreter = resources.FileInterpreterResourceWithRawResponse(client.file_interpreter)
+ self.capabilities = capabilities.CapabilitiesResourceWithRawResponse(client.capabilities)
+ self.models = models.ModelsResourceWithRawResponse(client.models)
+ self.kpu = kpu.KpuResourceWithRawResponse(client.kpu)
+ self.file_interpreter = file_interpreter.FileInterpreterResourceWithRawResponse(client.file_interpreter)
class AsyncMaisaWithRawResponse:
def __init__(self, client: AsyncMaisa) -> None:
- self.capabilities = resources.AsyncCapabilitiesResourceWithRawResponse(client.capabilities)
- self.models = resources.AsyncModelsResourceWithRawResponse(client.models)
- self.kpu = resources.AsyncKpuResourceWithRawResponse(client.kpu)
- self.file_interpreter = resources.AsyncFileInterpreterResourceWithRawResponse(client.file_interpreter)
+ self.capabilities = capabilities.AsyncCapabilitiesResourceWithRawResponse(client.capabilities)
+ self.models = models.AsyncModelsResourceWithRawResponse(client.models)
+ self.kpu = kpu.AsyncKpuResourceWithRawResponse(client.kpu)
+ self.file_interpreter = file_interpreter.AsyncFileInterpreterResourceWithRawResponse(client.file_interpreter)
class MaisaWithStreamedResponse:
def __init__(self, client: Maisa) -> None:
- self.capabilities = resources.CapabilitiesResourceWithStreamingResponse(client.capabilities)
- self.models = resources.ModelsResourceWithStreamingResponse(client.models)
- self.kpu = resources.KpuResourceWithStreamingResponse(client.kpu)
- self.file_interpreter = resources.FileInterpreterResourceWithStreamingResponse(client.file_interpreter)
+ self.capabilities = capabilities.CapabilitiesResourceWithStreamingResponse(client.capabilities)
+ self.models = models.ModelsResourceWithStreamingResponse(client.models)
+ self.kpu = kpu.KpuResourceWithStreamingResponse(client.kpu)
+ self.file_interpreter = file_interpreter.FileInterpreterResourceWithStreamingResponse(client.file_interpreter)
class AsyncMaisaWithStreamedResponse:
def __init__(self, client: AsyncMaisa) -> None:
- self.capabilities = resources.AsyncCapabilitiesResourceWithStreamingResponse(client.capabilities)
- self.models = resources.AsyncModelsResourceWithStreamingResponse(client.models)
- self.kpu = resources.AsyncKpuResourceWithStreamingResponse(client.kpu)
- self.file_interpreter = resources.AsyncFileInterpreterResourceWithStreamingResponse(client.file_interpreter)
+ self.capabilities = capabilities.AsyncCapabilitiesResourceWithStreamingResponse(client.capabilities)
+ self.models = models.AsyncModelsResourceWithStreamingResponse(client.models)
+ self.kpu = kpu.AsyncKpuResourceWithStreamingResponse(client.kpu)
+ self.file_interpreter = file_interpreter.AsyncFileInterpreterResourceWithStreamingResponse(
+ client.file_interpreter
+ )
Client = Maisa
From 017e4c3a0a59b4e50beef1b6486642f9e9ccd7cb Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 14 Dec 2024 05:55:15 +0000
Subject: [PATCH 023/200] chore(internal): codegen related update (#41)
---
src/maisa/_client.py | 84 +++++++++++++++++++++++---------------------
1 file changed, 44 insertions(+), 40 deletions(-)
diff --git a/src/maisa/_client.py b/src/maisa/_client.py
index de6ac769..5ed1eceb 100644
--- a/src/maisa/_client.py
+++ b/src/maisa/_client.py
@@ -8,7 +8,7 @@
import httpx
-from . import _exceptions
+from . import resources, _exceptions
from ._qs import Querystring
from ._types import (
NOT_GIVEN,
@@ -24,7 +24,6 @@
get_async_library,
)
from ._version import __version__
-from .resources import kpu
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
from ._exceptions import MaisaError, APIStatusError
from ._base_client import (
@@ -32,18 +31,25 @@
SyncAPIClient,
AsyncAPIClient,
)
-from .resources.models import models
-from .resources.capabilities import capabilities
-from .resources.file_interpreter import file_interpreter
-__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Maisa", "AsyncMaisa", "Client", "AsyncClient"]
+__all__ = [
+ "Timeout",
+ "Transport",
+ "ProxiesTypes",
+ "RequestOptions",
+ "resources",
+ "Maisa",
+ "AsyncMaisa",
+ "Client",
+ "AsyncClient",
+]
class Maisa(SyncAPIClient):
- capabilities: capabilities.CapabilitiesResource
- models: models.ModelsResource
- kpu: kpu.KpuResource
- file_interpreter: file_interpreter.FileInterpreterResource
+ capabilities: resources.CapabilitiesResource
+ models: resources.ModelsResource
+ kpu: resources.KpuResource
+ file_interpreter: resources.FileInterpreterResource
with_raw_response: MaisaWithRawResponse
with_streaming_response: MaisaWithStreamedResponse
@@ -101,10 +107,10 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.capabilities = capabilities.CapabilitiesResource(self)
- self.models = models.ModelsResource(self)
- self.kpu = kpu.KpuResource(self)
- self.file_interpreter = file_interpreter.FileInterpreterResource(self)
+ self.capabilities = resources.CapabilitiesResource(self)
+ self.models = resources.ModelsResource(self)
+ self.kpu = resources.KpuResource(self)
+ self.file_interpreter = resources.FileInterpreterResource(self)
self.with_raw_response = MaisaWithRawResponse(self)
self.with_streaming_response = MaisaWithStreamedResponse(self)
@@ -214,10 +220,10 @@ def _make_status_error(
class AsyncMaisa(AsyncAPIClient):
- capabilities: capabilities.AsyncCapabilitiesResource
- models: models.AsyncModelsResource
- kpu: kpu.AsyncKpuResource
- file_interpreter: file_interpreter.AsyncFileInterpreterResource
+ capabilities: resources.AsyncCapabilitiesResource
+ models: resources.AsyncModelsResource
+ kpu: resources.AsyncKpuResource
+ file_interpreter: resources.AsyncFileInterpreterResource
with_raw_response: AsyncMaisaWithRawResponse
with_streaming_response: AsyncMaisaWithStreamedResponse
@@ -275,10 +281,10 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.capabilities = capabilities.AsyncCapabilitiesResource(self)
- self.models = models.AsyncModelsResource(self)
- self.kpu = kpu.AsyncKpuResource(self)
- self.file_interpreter = file_interpreter.AsyncFileInterpreterResource(self)
+ self.capabilities = resources.AsyncCapabilitiesResource(self)
+ self.models = resources.AsyncModelsResource(self)
+ self.kpu = resources.AsyncKpuResource(self)
+ self.file_interpreter = resources.AsyncFileInterpreterResource(self)
self.with_raw_response = AsyncMaisaWithRawResponse(self)
self.with_streaming_response = AsyncMaisaWithStreamedResponse(self)
@@ -389,36 +395,34 @@ def _make_status_error(
class MaisaWithRawResponse:
def __init__(self, client: Maisa) -> None:
- self.capabilities = capabilities.CapabilitiesResourceWithRawResponse(client.capabilities)
- self.models = models.ModelsResourceWithRawResponse(client.models)
- self.kpu = kpu.KpuResourceWithRawResponse(client.kpu)
- self.file_interpreter = file_interpreter.FileInterpreterResourceWithRawResponse(client.file_interpreter)
+ self.capabilities = resources.CapabilitiesResourceWithRawResponse(client.capabilities)
+ self.models = resources.ModelsResourceWithRawResponse(client.models)
+ self.kpu = resources.KpuResourceWithRawResponse(client.kpu)
+ self.file_interpreter = resources.FileInterpreterResourceWithRawResponse(client.file_interpreter)
class AsyncMaisaWithRawResponse:
def __init__(self, client: AsyncMaisa) -> None:
- self.capabilities = capabilities.AsyncCapabilitiesResourceWithRawResponse(client.capabilities)
- self.models = models.AsyncModelsResourceWithRawResponse(client.models)
- self.kpu = kpu.AsyncKpuResourceWithRawResponse(client.kpu)
- self.file_interpreter = file_interpreter.AsyncFileInterpreterResourceWithRawResponse(client.file_interpreter)
+ self.capabilities = resources.AsyncCapabilitiesResourceWithRawResponse(client.capabilities)
+ self.models = resources.AsyncModelsResourceWithRawResponse(client.models)
+ self.kpu = resources.AsyncKpuResourceWithRawResponse(client.kpu)
+ self.file_interpreter = resources.AsyncFileInterpreterResourceWithRawResponse(client.file_interpreter)
class MaisaWithStreamedResponse:
def __init__(self, client: Maisa) -> None:
- self.capabilities = capabilities.CapabilitiesResourceWithStreamingResponse(client.capabilities)
- self.models = models.ModelsResourceWithStreamingResponse(client.models)
- self.kpu = kpu.KpuResourceWithStreamingResponse(client.kpu)
- self.file_interpreter = file_interpreter.FileInterpreterResourceWithStreamingResponse(client.file_interpreter)
+ self.capabilities = resources.CapabilitiesResourceWithStreamingResponse(client.capabilities)
+ self.models = resources.ModelsResourceWithStreamingResponse(client.models)
+ self.kpu = resources.KpuResourceWithStreamingResponse(client.kpu)
+ self.file_interpreter = resources.FileInterpreterResourceWithStreamingResponse(client.file_interpreter)
class AsyncMaisaWithStreamedResponse:
def __init__(self, client: AsyncMaisa) -> None:
- self.capabilities = capabilities.AsyncCapabilitiesResourceWithStreamingResponse(client.capabilities)
- self.models = models.AsyncModelsResourceWithStreamingResponse(client.models)
- self.kpu = kpu.AsyncKpuResourceWithStreamingResponse(client.kpu)
- self.file_interpreter = file_interpreter.AsyncFileInterpreterResourceWithStreamingResponse(
- client.file_interpreter
- )
+ self.capabilities = resources.AsyncCapabilitiesResourceWithStreamingResponse(client.capabilities)
+ self.models = resources.AsyncModelsResourceWithStreamingResponse(client.models)
+ self.kpu = resources.AsyncKpuResourceWithStreamingResponse(client.kpu)
+ self.file_interpreter = resources.AsyncFileInterpreterResourceWithStreamingResponse(client.file_interpreter)
Client = Maisa
From 738fb58f48f2a7cdbdc1c5a04b8ac2e50eb4c3ab Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 17 Dec 2024 05:41:19 +0000
Subject: [PATCH 024/200] chore(internal): codegen related update (#42)
---
src/maisa/_client.py | 84 +++++++++++++++++++++-----------------------
1 file changed, 40 insertions(+), 44 deletions(-)
diff --git a/src/maisa/_client.py b/src/maisa/_client.py
index 5ed1eceb..de6ac769 100644
--- a/src/maisa/_client.py
+++ b/src/maisa/_client.py
@@ -8,7 +8,7 @@
import httpx
-from . import resources, _exceptions
+from . import _exceptions
from ._qs import Querystring
from ._types import (
NOT_GIVEN,
@@ -24,6 +24,7 @@
get_async_library,
)
from ._version import __version__
+from .resources import kpu
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
from ._exceptions import MaisaError, APIStatusError
from ._base_client import (
@@ -31,25 +32,18 @@
SyncAPIClient,
AsyncAPIClient,
)
+from .resources.models import models
+from .resources.capabilities import capabilities
+from .resources.file_interpreter import file_interpreter
-__all__ = [
- "Timeout",
- "Transport",
- "ProxiesTypes",
- "RequestOptions",
- "resources",
- "Maisa",
- "AsyncMaisa",
- "Client",
- "AsyncClient",
-]
+__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Maisa", "AsyncMaisa", "Client", "AsyncClient"]
class Maisa(SyncAPIClient):
- capabilities: resources.CapabilitiesResource
- models: resources.ModelsResource
- kpu: resources.KpuResource
- file_interpreter: resources.FileInterpreterResource
+ capabilities: capabilities.CapabilitiesResource
+ models: models.ModelsResource
+ kpu: kpu.KpuResource
+ file_interpreter: file_interpreter.FileInterpreterResource
with_raw_response: MaisaWithRawResponse
with_streaming_response: MaisaWithStreamedResponse
@@ -107,10 +101,10 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.capabilities = resources.CapabilitiesResource(self)
- self.models = resources.ModelsResource(self)
- self.kpu = resources.KpuResource(self)
- self.file_interpreter = resources.FileInterpreterResource(self)
+ self.capabilities = capabilities.CapabilitiesResource(self)
+ self.models = models.ModelsResource(self)
+ self.kpu = kpu.KpuResource(self)
+ self.file_interpreter = file_interpreter.FileInterpreterResource(self)
self.with_raw_response = MaisaWithRawResponse(self)
self.with_streaming_response = MaisaWithStreamedResponse(self)
@@ -220,10 +214,10 @@ def _make_status_error(
class AsyncMaisa(AsyncAPIClient):
- capabilities: resources.AsyncCapabilitiesResource
- models: resources.AsyncModelsResource
- kpu: resources.AsyncKpuResource
- file_interpreter: resources.AsyncFileInterpreterResource
+ capabilities: capabilities.AsyncCapabilitiesResource
+ models: models.AsyncModelsResource
+ kpu: kpu.AsyncKpuResource
+ file_interpreter: file_interpreter.AsyncFileInterpreterResource
with_raw_response: AsyncMaisaWithRawResponse
with_streaming_response: AsyncMaisaWithStreamedResponse
@@ -281,10 +275,10 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.capabilities = resources.AsyncCapabilitiesResource(self)
- self.models = resources.AsyncModelsResource(self)
- self.kpu = resources.AsyncKpuResource(self)
- self.file_interpreter = resources.AsyncFileInterpreterResource(self)
+ self.capabilities = capabilities.AsyncCapabilitiesResource(self)
+ self.models = models.AsyncModelsResource(self)
+ self.kpu = kpu.AsyncKpuResource(self)
+ self.file_interpreter = file_interpreter.AsyncFileInterpreterResource(self)
self.with_raw_response = AsyncMaisaWithRawResponse(self)
self.with_streaming_response = AsyncMaisaWithStreamedResponse(self)
@@ -395,34 +389,36 @@ def _make_status_error(
class MaisaWithRawResponse:
def __init__(self, client: Maisa) -> None:
- self.capabilities = resources.CapabilitiesResourceWithRawResponse(client.capabilities)
- self.models = resources.ModelsResourceWithRawResponse(client.models)
- self.kpu = resources.KpuResourceWithRawResponse(client.kpu)
- self.file_interpreter = resources.FileInterpreterResourceWithRawResponse(client.file_interpreter)
+ self.capabilities = capabilities.CapabilitiesResourceWithRawResponse(client.capabilities)
+ self.models = models.ModelsResourceWithRawResponse(client.models)
+ self.kpu = kpu.KpuResourceWithRawResponse(client.kpu)
+ self.file_interpreter = file_interpreter.FileInterpreterResourceWithRawResponse(client.file_interpreter)
class AsyncMaisaWithRawResponse:
def __init__(self, client: AsyncMaisa) -> None:
- self.capabilities = resources.AsyncCapabilitiesResourceWithRawResponse(client.capabilities)
- self.models = resources.AsyncModelsResourceWithRawResponse(client.models)
- self.kpu = resources.AsyncKpuResourceWithRawResponse(client.kpu)
- self.file_interpreter = resources.AsyncFileInterpreterResourceWithRawResponse(client.file_interpreter)
+ self.capabilities = capabilities.AsyncCapabilitiesResourceWithRawResponse(client.capabilities)
+ self.models = models.AsyncModelsResourceWithRawResponse(client.models)
+ self.kpu = kpu.AsyncKpuResourceWithRawResponse(client.kpu)
+ self.file_interpreter = file_interpreter.AsyncFileInterpreterResourceWithRawResponse(client.file_interpreter)
class MaisaWithStreamedResponse:
def __init__(self, client: Maisa) -> None:
- self.capabilities = resources.CapabilitiesResourceWithStreamingResponse(client.capabilities)
- self.models = resources.ModelsResourceWithStreamingResponse(client.models)
- self.kpu = resources.KpuResourceWithStreamingResponse(client.kpu)
- self.file_interpreter = resources.FileInterpreterResourceWithStreamingResponse(client.file_interpreter)
+ self.capabilities = capabilities.CapabilitiesResourceWithStreamingResponse(client.capabilities)
+ self.models = models.ModelsResourceWithStreamingResponse(client.models)
+ self.kpu = kpu.KpuResourceWithStreamingResponse(client.kpu)
+ self.file_interpreter = file_interpreter.FileInterpreterResourceWithStreamingResponse(client.file_interpreter)
class AsyncMaisaWithStreamedResponse:
def __init__(self, client: AsyncMaisa) -> None:
- self.capabilities = resources.AsyncCapabilitiesResourceWithStreamingResponse(client.capabilities)
- self.models = resources.AsyncModelsResourceWithStreamingResponse(client.models)
- self.kpu = resources.AsyncKpuResourceWithStreamingResponse(client.kpu)
- self.file_interpreter = resources.AsyncFileInterpreterResourceWithStreamingResponse(client.file_interpreter)
+ self.capabilities = capabilities.AsyncCapabilitiesResourceWithStreamingResponse(client.capabilities)
+ self.models = models.AsyncModelsResourceWithStreamingResponse(client.models)
+ self.kpu = kpu.AsyncKpuResourceWithStreamingResponse(client.kpu)
+ self.file_interpreter = file_interpreter.AsyncFileInterpreterResourceWithStreamingResponse(
+ client.file_interpreter
+ )
Client = Maisa
From 03b1d41f8bf0eb3b7e4374ed66655dc393530303 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 17 Dec 2024 05:44:24 +0000
Subject: [PATCH 025/200] docs(readme): example snippet for client context
manager (#43)
---
README.md | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/README.md b/README.md
index d0a64653..070b2f70 100644
--- a/README.md
+++ b/README.md
@@ -298,6 +298,16 @@ client.with_options(http_client=DefaultHttpxClient(...))
By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting.
+```py
+from maisa import Maisa
+
+with Maisa() as client:
+ # make requests here
+ ...
+
+# HTTP client is now closed
+```
+
## Versioning
This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
From 01228e6aade70fd952ee192abdb2f90a4381fc92 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 18 Dec 2024 05:41:54 +0000
Subject: [PATCH 026/200] chore(internal): fix some typos (#44)
---
tests/test_client.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/tests/test_client.py b/tests/test_client.py
index 254af41d..e205d276 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -338,11 +338,11 @@ def test_default_query_option(self) -> None:
FinalRequestOptions(
method="get",
url="/foo",
- params={"foo": "baz", "query_param": "overriden"},
+ params={"foo": "baz", "query_param": "overridden"},
)
)
url = httpx.URL(request.url)
- assert dict(url.params) == {"foo": "baz", "query_param": "overriden"}
+ assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
def test_request_extra_json(self) -> None:
request = self.client._build_request(
@@ -1092,11 +1092,11 @@ def test_default_query_option(self) -> None:
FinalRequestOptions(
method="get",
url="/foo",
- params={"foo": "baz", "query_param": "overriden"},
+ params={"foo": "baz", "query_param": "overridden"},
)
)
url = httpx.URL(request.url)
- assert dict(url.params) == {"foo": "baz", "query_param": "overriden"}
+ assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
def test_request_extra_json(self) -> None:
request = self.client._build_request(
From a28dbb952c8668b1fbd895979705646789522712 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 2 Jan 2025 05:33:37 +0000
Subject: [PATCH 027/200] chore(internal): codegen related update (#45)
---
LICENSE | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/LICENSE b/LICENSE
index 34d78468..cab49bf2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2024 Maisa
+ Copyright 2025 Maisa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
From 0e94ac22ca8e33653888f357526433a23e82dad0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Jan 2025 05:38:33 +0000
Subject: [PATCH 028/200] chore: add missing isclass check (#46)
---
src/maisa/_models.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index 7a547ce5..d56ea1d9 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -488,7 +488,11 @@ def construct_type(*, value: object, type_: object) -> object:
_, items_type = get_args(type_) # Dict[_, items_type]
return {key: construct_type(value=item, type_=items_type) for key, item in value.items()}
- if not is_literal_type(type_) and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)):
+ if (
+ not is_literal_type(type_)
+ and inspect.isclass(origin)
+ and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel))
+ ):
if is_list(value):
return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value]
From fb2bc9496c5b7c66454d1b2e77c8bf8665a28ab5 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 8 Jan 2025 05:33:04 +0000
Subject: [PATCH 029/200] chore(internal): bump httpx dependency (#47)
---
pyproject.toml | 2 +-
requirements-dev.lock | 5 ++---
requirements.lock | 3 +--
3 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index e0c86c18..50973633 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,7 +54,7 @@ dev-dependencies = [
"dirty-equals>=0.6.0",
"importlib-metadata>=6.7.0",
"rich>=13.7.1",
- "nest_asyncio==1.6.0"
+ "nest_asyncio==1.6.0",
]
[tool.rye.scripts]
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 608452ba..abb8a8ef 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -35,7 +35,7 @@ h11==0.14.0
# via httpcore
httpcore==1.0.2
# via httpx
-httpx==0.25.2
+httpx==0.28.1
# via maisa
# via respx
idna==3.4
@@ -76,7 +76,7 @@ python-dateutil==2.8.2
# via time-machine
pytz==2023.3.post1
# via dirty-equals
-respx==0.20.2
+respx==0.22.0
rich==13.7.1
ruff==0.6.9
setuptools==68.2.2
@@ -85,7 +85,6 @@ six==1.16.0
# via python-dateutil
sniffio==1.3.0
# via anyio
- # via httpx
# via maisa
time-machine==2.9.0
tomli==2.0.2
diff --git a/requirements.lock b/requirements.lock
index 6154e101..53f72df6 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -25,7 +25,7 @@ h11==0.14.0
# via httpcore
httpcore==1.0.2
# via httpx
-httpx==0.25.2
+httpx==0.28.1
# via maisa
idna==3.4
# via anyio
@@ -36,7 +36,6 @@ pydantic-core==2.27.1
# via pydantic
sniffio==1.3.0
# via anyio
- # via httpx
# via maisa
typing-extensions==4.12.2
# via anyio
From 2a8b3eae602602a012db86751b7569129fb1a02c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 8 Jan 2025 05:36:55 +0000
Subject: [PATCH 030/200] fix(client): only call .close() when needed (#48)
---
src/maisa/_base_client.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index 5ea08e6a..bc87921f 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -767,6 +767,9 @@ def __init__(self, **kwargs: Any) -> None:
class SyncHttpxClientWrapper(DefaultHttpxClient):
def __del__(self) -> None:
+ if self.is_closed:
+ return
+
try:
self.close()
except Exception:
@@ -1334,6 +1337,9 @@ def __init__(self, **kwargs: Any) -> None:
class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient):
def __del__(self) -> None:
+ if self.is_closed:
+ return
+
try:
# TODO(someday): support non asyncio runtimes here
asyncio.get_running_loop().create_task(self.aclose())
From baf1fbc795d885fd3871cf3a65da1950b53e7391 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 9 Jan 2025 05:44:08 +0000
Subject: [PATCH 031/200] docs: fix typos (#49)
---
README.md | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 070b2f70..9408cea2 100644
--- a/README.md
+++ b/README.md
@@ -101,7 +101,7 @@ except maisa.APIStatusError as e:
print(e.response)
```
-Error codes are as followed:
+Error codes are as follows:
| Status Code | Error Type |
| ----------- | -------------------------- |
@@ -240,8 +240,7 @@ If you need to access undocumented endpoints, params, or response properties, th
#### Undocumented endpoints
To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other
-http verbs. Options on the client will be respected (such as retries) will be respected when making this
-request.
+http verbs. Options on the client will be respected (such as retries) when making this request.
```py
import httpx
From 0f877e7b5d82c07647afaddb9f236181e286ad07 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 9 Jan 2025 05:45:18 +0000
Subject: [PATCH 032/200] chore(internal): codegen related update (#50)
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 9408cea2..d7524692 100644
--- a/README.md
+++ b/README.md
@@ -312,7 +312,7 @@ with Maisa() as client:
This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
1. Changes that only affect static types, without breaking runtime behavior.
-2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals)_.
+2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_
3. Changes that we do not expect to impact the vast majority of users in practice.
We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
From 8bab70c60cd7bf9e441173a0273ce17e0f41a302 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 10 Jan 2025 05:38:56 +0000
Subject: [PATCH 033/200] fix: correctly handle deserialising `cls` fields
(#51)
---
src/maisa/_models.py | 8 ++++----
tests/test_models.py | 10 ++++++++++
2 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index d56ea1d9..9a918aab 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -179,14 +179,14 @@ def __str__(self) -> str:
@classmethod
@override
def construct( # pyright: ignore[reportIncompatibleMethodOverride]
- cls: Type[ModelT],
+ __cls: Type[ModelT],
_fields_set: set[str] | None = None,
**values: object,
) -> ModelT:
- m = cls.__new__(cls)
+ m = __cls.__new__(__cls)
fields_values: dict[str, object] = {}
- config = get_model_config(cls)
+ config = get_model_config(__cls)
populate_by_name = (
config.allow_population_by_field_name
if isinstance(config, _ConfigProtocol)
@@ -196,7 +196,7 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride]
if _fields_set is None:
_fields_set = set()
- model_fields = get_model_fields(cls)
+ model_fields = get_model_fields(__cls)
for name, field in model_fields.items():
key = field.alias
if key is None or (key not in values and populate_by_name):
diff --git a/tests/test_models.py b/tests/test_models.py
index 27bcbc37..73c41a9d 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -844,3 +844,13 @@ class Model(BaseModel):
assert m.alias == "foo"
assert isinstance(m.union, str)
assert m.union == "bar"
+
+
+@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1")
+def test_field_named_cls() -> None:
+ class Model(BaseModel):
+ cls: str
+
+ m = construct_type(value={"cls": "foo"}, type_=Model)
+ assert isinstance(m, Model)
+ assert isinstance(m.cls, str)
From e097f1c81b5504db6d7761a845542a5dd300221f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 17 Jan 2025 06:11:04 +0000
Subject: [PATCH 034/200] chore(internal): codegen related update (#52)
---
mypy.ini | 2 +-
requirements-dev.lock | 4 ++--
src/maisa/_response.py | 8 +++++++-
3 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/mypy.ini b/mypy.ini
index 080f3e6c..602e0dc5 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -41,7 +41,7 @@ cache_fine_grained = True
# ```
# Changing this codegen to make mypy happy would increase complexity
# and would not be worth it.
-disable_error_code = func-returns-value
+disable_error_code = func-returns-value,overload-cannot-match
# https://github.com/python/mypy/issues/12162
[mypy.overrides]
diff --git a/requirements-dev.lock b/requirements-dev.lock
index abb8a8ef..c2af68ce 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -48,7 +48,7 @@ markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
-mypy==1.13.0
+mypy==1.14.1
mypy-extensions==1.0.0
# via mypy
nest-asyncio==1.6.0
@@ -68,7 +68,7 @@ pydantic-core==2.27.1
# via pydantic
pygments==2.18.0
# via rich
-pyright==1.1.390
+pyright==1.1.392.post0
pytest==8.3.3
# via pytest-asyncio
pytest-asyncio==0.24.0
diff --git a/src/maisa/_response.py b/src/maisa/_response.py
index 13e1ca35..7d48a8df 100644
--- a/src/maisa/_response.py
+++ b/src/maisa/_response.py
@@ -210,7 +210,13 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`")
return cast(R, response)
- if inspect.isclass(origin) and not issubclass(origin, BaseModel) and issubclass(origin, pydantic.BaseModel):
+ if (
+ inspect.isclass(
+ origin # pyright: ignore[reportUnknownArgumentType]
+ )
+ and not issubclass(origin, BaseModel)
+ and issubclass(origin, pydantic.BaseModel)
+ ):
raise TypeError("Pydantic models must subclass our base model type, e.g. `from maisa import BaseModel`")
if (
From ff88f4fb37f793982750daffd89742f06edd9d0d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 21 Jan 2025 05:39:11 +0000
Subject: [PATCH 035/200] docs(raw responses): fix duplicate `the` (#53)
---
src/maisa/resources/capabilities/capabilities.py | 4 ++--
src/maisa/resources/capabilities/media.py | 4 ++--
src/maisa/resources/file_interpreter/file_interpreter.py | 4 ++--
src/maisa/resources/file_interpreter/from_audio.py | 4 ++--
src/maisa/resources/file_interpreter/from_docx.py | 4 ++--
src/maisa/resources/file_interpreter/from_html.py | 4 ++--
src/maisa/resources/file_interpreter/from_image.py | 4 ++--
src/maisa/resources/file_interpreter/from_pdf.py | 4 ++--
src/maisa/resources/kpu.py | 4 ++--
src/maisa/resources/models/embeddings.py | 4 ++--
src/maisa/resources/models/models.py | 4 ++--
11 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/src/maisa/resources/capabilities/capabilities.py b/src/maisa/resources/capabilities/capabilities.py
index 7c760b4e..a154ede2 100644
--- a/src/maisa/resources/capabilities/capabilities.py
+++ b/src/maisa/resources/capabilities/capabilities.py
@@ -45,7 +45,7 @@ def media(self) -> MediaResource:
@cached_property
def with_raw_response(self) -> CapabilitiesResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
@@ -234,7 +234,7 @@ def media(self) -> AsyncMediaResource:
@cached_property
def with_raw_response(self) -> AsyncCapabilitiesResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
diff --git a/src/maisa/resources/capabilities/media.py b/src/maisa/resources/capabilities/media.py
index f9287395..d7ad849d 100644
--- a/src/maisa/resources/capabilities/media.py
+++ b/src/maisa/resources/capabilities/media.py
@@ -35,7 +35,7 @@ class MediaResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> MediaResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
@@ -323,7 +323,7 @@ class AsyncMediaResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncMediaResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
diff --git a/src/maisa/resources/file_interpreter/file_interpreter.py b/src/maisa/resources/file_interpreter/file_interpreter.py
index 6519a992..f7bb78fe 100644
--- a/src/maisa/resources/file_interpreter/file_interpreter.py
+++ b/src/maisa/resources/file_interpreter/file_interpreter.py
@@ -72,7 +72,7 @@ def from_audio(self) -> FromAudioResource:
@cached_property
def with_raw_response(self) -> FileInterpreterResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
@@ -113,7 +113,7 @@ def from_audio(self) -> AsyncFromAudioResource:
@cached_property
def with_raw_response(self) -> AsyncFileInterpreterResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
diff --git a/src/maisa/resources/file_interpreter/from_audio.py b/src/maisa/resources/file_interpreter/from_audio.py
index 73c52414..e0b676c1 100644
--- a/src/maisa/resources/file_interpreter/from_audio.py
+++ b/src/maisa/resources/file_interpreter/from_audio.py
@@ -31,7 +31,7 @@ class FromAudioResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> FromAudioResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
@@ -91,7 +91,7 @@ class AsyncFromAudioResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncFromAudioResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
diff --git a/src/maisa/resources/file_interpreter/from_docx.py b/src/maisa/resources/file_interpreter/from_docx.py
index ca0b6ee0..2675b3be 100644
--- a/src/maisa/resources/file_interpreter/from_docx.py
+++ b/src/maisa/resources/file_interpreter/from_docx.py
@@ -31,7 +31,7 @@ class FromDocxResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> FromDocxResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
@@ -91,7 +91,7 @@ class AsyncFromDocxResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncFromDocxResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
diff --git a/src/maisa/resources/file_interpreter/from_html.py b/src/maisa/resources/file_interpreter/from_html.py
index ba998348..fbc9c0f1 100644
--- a/src/maisa/resources/file_interpreter/from_html.py
+++ b/src/maisa/resources/file_interpreter/from_html.py
@@ -31,7 +31,7 @@ class FromHTMLResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> FromHTMLResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
@@ -91,7 +91,7 @@ class AsyncFromHTMLResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncFromHTMLResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
diff --git a/src/maisa/resources/file_interpreter/from_image.py b/src/maisa/resources/file_interpreter/from_image.py
index bc967a10..0278ae9f 100644
--- a/src/maisa/resources/file_interpreter/from_image.py
+++ b/src/maisa/resources/file_interpreter/from_image.py
@@ -31,7 +31,7 @@ class FromImageResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> FromImageResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
@@ -91,7 +91,7 @@ class AsyncFromImageResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncFromImageResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
diff --git a/src/maisa/resources/file_interpreter/from_pdf.py b/src/maisa/resources/file_interpreter/from_pdf.py
index d8f2a691..f9e20b86 100644
--- a/src/maisa/resources/file_interpreter/from_pdf.py
+++ b/src/maisa/resources/file_interpreter/from_pdf.py
@@ -31,7 +31,7 @@ class FromPdfResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> FromPdfResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
@@ -96,7 +96,7 @@ class AsyncFromPdfResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncFromPdfResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
diff --git a/src/maisa/resources/kpu.py b/src/maisa/resources/kpu.py
index c6b22de2..3bfdb1c2 100644
--- a/src/maisa/resources/kpu.py
+++ b/src/maisa/resources/kpu.py
@@ -32,7 +32,7 @@ class KpuResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> KpuResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
@@ -139,7 +139,7 @@ class AsyncKpuResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncKpuResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
diff --git a/src/maisa/resources/models/embeddings.py b/src/maisa/resources/models/embeddings.py
index 15f0aa17..4e9f3c12 100644
--- a/src/maisa/resources/models/embeddings.py
+++ b/src/maisa/resources/models/embeddings.py
@@ -30,7 +30,7 @@ class EmbeddingsResource(SyncAPIResource):
@cached_property
def with_raw_response(self) -> EmbeddingsResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
@@ -85,7 +85,7 @@ class AsyncEmbeddingsResource(AsyncAPIResource):
@cached_property
def with_raw_response(self) -> AsyncEmbeddingsResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
diff --git a/src/maisa/resources/models/models.py b/src/maisa/resources/models/models.py
index 04e974a9..5c1d2d1c 100644
--- a/src/maisa/resources/models/models.py
+++ b/src/maisa/resources/models/models.py
@@ -24,7 +24,7 @@ def embeddings(self) -> EmbeddingsResource:
@cached_property
def with_raw_response(self) -> ModelsResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
@@ -49,7 +49,7 @@ def embeddings(self) -> AsyncEmbeddingsResource:
@cached_property
def with_raw_response(self) -> AsyncModelsResourceWithRawResponse:
"""
- This property can be used as a prefix for any HTTP method call to return the
+ This property can be used as a prefix for any HTTP method call to return
the raw response object instead of the parsed content.
For more information, see https://www.github.com/maisaai/python-sdk#accessing-raw-response-data-eg-headers
From 0f53e2eaf845b8631222747adbed6aef26e71aef Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 21 Jan 2025 05:39:36 +0000
Subject: [PATCH 036/200] fix(tests): make test_get_platform less flaky (#54)
---
tests/test_client.py | 25 ++++++++++++++++++-------
1 file changed, 18 insertions(+), 7 deletions(-)
diff --git a/tests/test_client.py b/tests/test_client.py
index e205d276..5c75121a 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -6,6 +6,7 @@
import os
import sys
import json
+import time
import asyncio
import inspect
import subprocess
@@ -1598,10 +1599,20 @@ async def test_main() -> None:
[sys.executable, "-c", test_code],
text=True,
) as process:
- try:
- process.wait(2)
- if process.returncode:
- raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code")
- except subprocess.TimeoutExpired as e:
- process.kill()
- raise AssertionError("calling get_platform using asyncify resulted in a hung process") from e
+ timeout = 10 # seconds
+
+ start_time = time.monotonic()
+ while True:
+ return_code = process.poll()
+ if return_code is not None:
+ if return_code != 0:
+ raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code")
+
+ # success
+ break
+
+ if time.monotonic() - start_time > timeout:
+ process.kill()
+ raise AssertionError("calling get_platform using asyncify resulted in a hung process")
+
+ time.sleep(0.1)
From bc8b43ef36216fbfac1526075402c1cd20d4af57 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 21 Jan 2025 05:40:08 +0000
Subject: [PATCH 037/200] chore(internal): avoid pytest-asyncio deprecation
warning (#55)
---
pyproject.toml | 1 +
1 file changed, 1 insertion(+)
diff --git a/pyproject.toml b/pyproject.toml
index 50973633..56a71904 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -129,6 +129,7 @@ testpaths = ["tests"]
addopts = "--tb=short"
xfail_strict = true
asyncio_mode = "auto"
+asyncio_default_fixture_loop_scope = "session"
filterwarnings = [
"error"
]
From 0614b4a6d98d5f36c2abf99692cc249eec7b1462 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 22 Jan 2025 05:35:48 +0000
Subject: [PATCH 038/200] chore(internal): minor style changes (#56)
---
src/maisa/_response.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/maisa/_response.py b/src/maisa/_response.py
index 7d48a8df..84d4c98d 100644
--- a/src/maisa/_response.py
+++ b/src/maisa/_response.py
@@ -136,6 +136,8 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
if cast_to and is_annotated_type(cast_to):
cast_to = extract_type_arg(cast_to, 0)
+ origin = get_origin(cast_to) or cast_to
+
if self._is_sse_stream:
if to:
if not is_stream_class_type(to):
@@ -195,8 +197,6 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
if cast_to == bool:
return cast(R, response.text.lower() == "true")
- origin = get_origin(cast_to) or cast_to
-
if origin == APIResponse:
raise RuntimeError("Unexpected state - cast_to is `APIResponse`")
From 218176bbb0aad125049959bf249298d9275276ef Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 24 Jan 2025 04:34:38 +0000
Subject: [PATCH 039/200] chore(internal): minor formatting changes (#57)
---
.github/workflows/ci.yml | 3 +--
scripts/bootstrap | 2 +-
scripts/lint | 1 -
3 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 40293964..c8a8a4f7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,7 +12,6 @@ jobs:
lint:
name: lint
runs-on: ubuntu-latest
-
steps:
- uses: actions/checkout@v4
@@ -30,6 +29,7 @@ jobs:
- name: Run lints
run: ./scripts/lint
+
test:
name: test
runs-on: ubuntu-latest
@@ -50,4 +50,3 @@ jobs:
- name: Run tests
run: ./scripts/test
-
diff --git a/scripts/bootstrap b/scripts/bootstrap
index 8c5c60eb..e84fe62c 100755
--- a/scripts/bootstrap
+++ b/scripts/bootstrap
@@ -4,7 +4,7 @@ set -e
cd "$(dirname "$0")/.."
-if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then
+if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then
brew bundle check >/dev/null 2>&1 || {
echo "==> Installing Homebrew dependencies…"
brew bundle
diff --git a/scripts/lint b/scripts/lint
index b9127eda..76a131b0 100755
--- a/scripts/lint
+++ b/scripts/lint
@@ -9,4 +9,3 @@ rye run lint
echo "==> Making sure it imports"
rye run python -c 'import maisa'
-
From 9b8fbb799000c30e15cdb7777e71aecf1584826c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 4 Feb 2025 03:18:45 +0000
Subject: [PATCH 040/200] chore(internal): change default timeout to an int
(#58)
---
src/maisa/_constants.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/maisa/_constants.py b/src/maisa/_constants.py
index a2ac3b6f..6ddf2c71 100644
--- a/src/maisa/_constants.py
+++ b/src/maisa/_constants.py
@@ -6,7 +6,7 @@
OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to"
# default timeout is 1 minute
-DEFAULT_TIMEOUT = httpx.Timeout(timeout=60.0, connect=5.0)
+DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0)
DEFAULT_MAX_RETRIES = 2
DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20)
From 7882f2c5a49d968bafb60729ac31c831e2b8161f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 4 Feb 2025 03:20:26 +0000
Subject: [PATCH 041/200] chore(internal): bummp ruff dependency (#59)
---
pyproject.toml | 2 +-
requirements-dev.lock | 2 +-
scripts/utils/ruffen-docs.py | 4 ++--
src/maisa/_models.py | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 56a71904..b0f84b0a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -177,7 +177,7 @@ select = [
"T201",
"T203",
# misuse of typing.TYPE_CHECKING
- "TCH004",
+ "TC004",
# import rules
"TID251",
]
diff --git a/requirements-dev.lock b/requirements-dev.lock
index c2af68ce..ed583720 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -78,7 +78,7 @@ pytz==2023.3.post1
# via dirty-equals
respx==0.22.0
rich==13.7.1
-ruff==0.6.9
+ruff==0.9.4
setuptools==68.2.2
# via nodeenv
six==1.16.0
diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py
index 37b3d94f..0cf2bd2f 100644
--- a/scripts/utils/ruffen-docs.py
+++ b/scripts/utils/ruffen-docs.py
@@ -47,7 +47,7 @@ def _md_match(match: Match[str]) -> str:
with _collect_error(match):
code = format_code_block(code)
code = textwrap.indent(code, match["indent"])
- return f'{match["before"]}{code}{match["after"]}'
+ return f"{match['before']}{code}{match['after']}"
def _pycon_match(match: Match[str]) -> str:
code = ""
@@ -97,7 +97,7 @@ def finish_fragment() -> None:
def _md_pycon_match(match: Match[str]) -> str:
code = _pycon_match(match)
code = textwrap.indent(code, match["indent"])
- return f'{match["before"]}{code}{match["after"]}'
+ return f"{match['before']}{code}{match['after']}"
src = MD_RE.sub(_md_match, src)
src = MD_PYCON_RE.sub(_md_pycon_match, src)
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index 9a918aab..12c34b7d 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -172,7 +172,7 @@ def to_json(
@override
def __str__(self) -> str:
# mypy complains about an invalid self arg
- return f'{self.__repr_name__()}({self.__repr_str__(", ")})' # type: ignore[misc]
+ return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc]
# Override the 'construct' method in a way that supports recursive parsing without validation.
# Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836.
From 69e514f736ef246a99c7b842521cf59a751a7814 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 6 Feb 2025 03:24:11 +0000
Subject: [PATCH 042/200] feat(client): send `X-Stainless-Read-Timeout` header
(#60)
---
src/maisa/_base_client.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index bc87921f..78779db0 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -418,10 +418,17 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0
if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers:
headers[idempotency_header] = options.idempotency_key or self._idempotency_key()
- # Don't set the retry count header if it was already set or removed by the caller. We check
+ # Don't set these headers if they were already set or removed by the caller. We check
# `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case.
- if "x-stainless-retry-count" not in (header.lower() for header in custom_headers):
+ lower_custom_headers = [header.lower() for header in custom_headers]
+ if "x-stainless-retry-count" not in lower_custom_headers:
headers["x-stainless-retry-count"] = str(retries_taken)
+ if "x-stainless-read-timeout" not in lower_custom_headers:
+ timeout = self.timeout if isinstance(options.timeout, NotGiven) else options.timeout
+ if isinstance(timeout, Timeout):
+ timeout = timeout.read
+ if timeout is not None:
+ headers["x-stainless-read-timeout"] = str(timeout)
return headers
From d69c8cecc1eb95971ad11814b5c39506b5a35ae3 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 7 Feb 2025 03:22:25 +0000
Subject: [PATCH 043/200] chore(internal): fix type traversing dictionary
params (#61)
---
src/maisa/_utils/_transform.py | 12 +++++++++++-
tests/test_transform.py | 11 ++++++++++-
2 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/src/maisa/_utils/_transform.py b/src/maisa/_utils/_transform.py
index a6b62cad..18afd9d8 100644
--- a/src/maisa/_utils/_transform.py
+++ b/src/maisa/_utils/_transform.py
@@ -25,7 +25,7 @@
is_annotated_type,
strip_annotated_type,
)
-from .._compat import model_dump, is_typeddict
+from .._compat import get_origin, model_dump, is_typeddict
_T = TypeVar("_T")
@@ -164,9 +164,14 @@ def _transform_recursive(
inner_type = annotation
stripped_type = strip_annotated_type(inner_type)
+ origin = get_origin(stripped_type) or stripped_type
if is_typeddict(stripped_type) and is_mapping(data):
return _transform_typeddict(data, stripped_type)
+ if origin == dict and is_mapping(data):
+ items_type = get_args(stripped_type)[1]
+ return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()}
+
if (
# List[T]
(is_list_type(stripped_type) and is_list(data))
@@ -307,9 +312,14 @@ async def _async_transform_recursive(
inner_type = annotation
stripped_type = strip_annotated_type(inner_type)
+ origin = get_origin(stripped_type) or stripped_type
if is_typeddict(stripped_type) and is_mapping(data):
return await _async_transform_typeddict(data, stripped_type)
+ if origin == dict and is_mapping(data):
+ items_type = get_args(stripped_type)[1]
+ return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()}
+
if (
# List[T]
(is_list_type(stripped_type) and is_list(data))
diff --git a/tests/test_transform.py b/tests/test_transform.py
index ce1fcc70..73315099 100644
--- a/tests/test_transform.py
+++ b/tests/test_transform.py
@@ -2,7 +2,7 @@
import io
import pathlib
-from typing import Any, List, Union, TypeVar, Iterable, Optional, cast
+from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast
from datetime import date, datetime
from typing_extensions import Required, Annotated, TypedDict
@@ -388,6 +388,15 @@ def my_iter() -> Iterable[Baz8]:
}
+@parametrize
+@pytest.mark.asyncio
+async def test_dictionary_items(use_async: bool) -> None:
+ class DictItems(TypedDict):
+ foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")]
+
+ assert await transform({"foo": {"foo_baz": "bar"}}, Dict[str, DictItems], use_async) == {"foo": {"fooBaz": "bar"}}
+
+
class TypedDictIterableUnionStr(TypedDict):
foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")]
From 3cde3a52a5cc06e686bf44bebce5642d5ebbd6f0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 7 Feb 2025 03:25:31 +0000
Subject: [PATCH 044/200] chore(internal): minor type handling changes (#62)
---
src/maisa/_models.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index 12c34b7d..c4401ff8 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -426,10 +426,16 @@ def construct_type(*, value: object, type_: object) -> object:
If the given value does not match the expected type then it is returned as-is.
"""
+
+ # store a reference to the original type we were given before we extract any inner
+ # types so that we can properly resolve forward references in `TypeAliasType` annotations
+ original_type = None
+
# we allow `object` as the input type because otherwise, passing things like
# `Literal['value']` will be reported as a type error by type checkers
type_ = cast("type[object]", type_)
if is_type_alias_type(type_):
+ original_type = type_ # type: ignore[unreachable]
type_ = type_.__value__ # type: ignore[unreachable]
# unwrap `Annotated[T, ...]` -> `T`
@@ -446,7 +452,7 @@ def construct_type(*, value: object, type_: object) -> object:
if is_union(origin):
try:
- return validate_type(type_=cast("type[object]", type_), value=value)
+ return validate_type(type_=cast("type[object]", original_type or type_), value=value)
except Exception:
pass
From 4596d45e982f7d3fc3de025a1ba5574c590d875a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 13 Feb 2025 04:15:28 +0000
Subject: [PATCH 045/200] chore(internal): update client tests (#63)
---
tests/test_client.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/tests/test_client.py b/tests/test_client.py
index 5c75121a..d8045ff6 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -23,10 +23,12 @@
from maisa import Maisa, AsyncMaisa, APIResponseValidationError
from maisa._types import Omit
+from maisa._utils import maybe_transform
from maisa._models import BaseModel, FinalRequestOptions
from maisa._constants import RAW_RESPONSE_HEADER
from maisa._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError
from maisa._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options
+from maisa.types.capability_summarize_params import CapabilitySummarizeParams
from .utils import update_env
@@ -700,7 +702,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No
with pytest.raises(APITimeoutError):
self.client.post(
"/v1/capabilities/summarize",
- body=cast(object, dict(text="Example long text...")),
+ body=cast(object, maybe_transform(dict(text="Example long text..."), CapabilitySummarizeParams)),
cast_to=httpx.Response,
options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
)
@@ -715,7 +717,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non
with pytest.raises(APIStatusError):
self.client.post(
"/v1/capabilities/summarize",
- body=cast(object, dict(text="Example long text...")),
+ body=cast(object, maybe_transform(dict(text="Example long text..."), CapabilitySummarizeParams)),
cast_to=httpx.Response,
options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
)
@@ -1466,7 +1468,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter)
with pytest.raises(APITimeoutError):
await self.client.post(
"/v1/capabilities/summarize",
- body=cast(object, dict(text="Example long text...")),
+ body=cast(object, maybe_transform(dict(text="Example long text..."), CapabilitySummarizeParams)),
cast_to=httpx.Response,
options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
)
@@ -1481,7 +1483,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter)
with pytest.raises(APIStatusError):
await self.client.post(
"/v1/capabilities/summarize",
- body=cast(object, dict(text="Example long text...")),
+ body=cast(object, maybe_transform(dict(text="Example long text..."), CapabilitySummarizeParams)),
cast_to=httpx.Response,
options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
)
From f7ad8737d8a8996062f6dc62e9ad8ecf34456dd5 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 14 Feb 2025 03:55:21 +0000
Subject: [PATCH 046/200] fix: asyncify on non-asyncio runtimes (#64)
---
src/maisa/_utils/_sync.py | 19 +++++++++++++++++--
1 file changed, 17 insertions(+), 2 deletions(-)
diff --git a/src/maisa/_utils/_sync.py b/src/maisa/_utils/_sync.py
index 8b3aaf2b..ad7ec71b 100644
--- a/src/maisa/_utils/_sync.py
+++ b/src/maisa/_utils/_sync.py
@@ -7,16 +7,20 @@
from typing import Any, TypeVar, Callable, Awaitable
from typing_extensions import ParamSpec
+import anyio
+import sniffio
+import anyio.to_thread
+
T_Retval = TypeVar("T_Retval")
T_ParamSpec = ParamSpec("T_ParamSpec")
if sys.version_info >= (3, 9):
- to_thread = asyncio.to_thread
+ _asyncio_to_thread = asyncio.to_thread
else:
# backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
# for Python 3.8 support
- async def to_thread(
+ async def _asyncio_to_thread(
func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
) -> Any:
"""Asynchronously run function *func* in a separate thread.
@@ -34,6 +38,17 @@ async def to_thread(
return await loop.run_in_executor(None, func_call)
+async def to_thread(
+ func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
+) -> T_Retval:
+ if sniffio.current_async_library() == "asyncio":
+ return await _asyncio_to_thread(func, *args, **kwargs)
+
+ return await anyio.to_thread.run_sync(
+ functools.partial(func, *args, **kwargs),
+ )
+
+
# inspired by `asyncer`, https://github.com/tiangolo/asyncer
def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
"""
From 851aa1d4e01286a35eb3b64e6d299dda49ccad28 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 19 Feb 2025 04:16:47 +0000
Subject: [PATCH 047/200] chore(internal): codegen related update (#65)
---
README.md | 18 ++++++++++++++++++
src/maisa/_files.py | 2 +-
2 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index d7524692..9e30c8f9 100644
--- a/README.md
+++ b/README.md
@@ -71,6 +71,24 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ
Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`.
+## File uploads
+
+Request parameters that correspond to file uploads can be passed as `bytes`, a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`.
+
+```python
+from pathlib import Path
+from maisa import Maisa
+
+client = Maisa()
+
+client.capabilities.media.compare(
+ file1=Path("/path/to/file"),
+ file2=b"raw file contents",
+)
+```
+
+The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically.
+
## Handling errors
When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `maisa.APIConnectionError` is raised.
diff --git a/src/maisa/_files.py b/src/maisa/_files.py
index 715cc207..4b635213 100644
--- a/src/maisa/_files.py
+++ b/src/maisa/_files.py
@@ -34,7 +34,7 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None:
if not is_file_content(obj):
prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`"
raise RuntimeError(
- f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead."
+ f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/maisaai/python-sdk/tree/main#file-uploads"
) from None
From c53de39f59d976f8c2e3540c6254a0ec19de2e05 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 21 Feb 2025 05:46:48 +0000
Subject: [PATCH 048/200] feat(client): allow passing `NotGiven` for body (#66)
fix(client): mark some request bodies as optional
---
src/maisa/_base_client.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index 78779db0..82a5ab17 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -518,7 +518,7 @@ def _build_request(
# so that passing a `TypedDict` doesn't cause an error.
# https://github.com/microsoft/pyright/issues/3526#event-6715453066
params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None,
- json=json_data,
+ json=json_data if is_given(json_data) else None,
files=files,
**kwargs,
)
From 992579197032920caa208eb6930507235d2b08a7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 22 Feb 2025 04:28:13 +0000
Subject: [PATCH 049/200] chore(internal): fix devcontainers setup (#67)
---
.devcontainer/Dockerfile | 2 +-
.devcontainer/devcontainer.json | 3 +++
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index ac9a2e75..55d20255 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -6,4 +6,4 @@ USER vscode
RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.35.0" RYE_INSTALL_OPTION="--yes" bash
ENV PATH=/home/vscode/.rye/shims:$PATH
-RUN echo "[[ -d .venv ]] && source .venv/bin/activate" >> /home/vscode/.bashrc
+RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index bbeb30b1..c17fdc16 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -24,6 +24,9 @@
}
}
}
+ },
+ "features": {
+ "ghcr.io/devcontainers/features/node:1": {}
}
// Features to add to the dev container. More info: https://containers.dev/features.
From 7af75eb3b3ca2b06521473713f0fb3d1a05a6517 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 26 Feb 2025 03:44:46 +0000
Subject: [PATCH 050/200] chore(internal): properly set __pydantic_private__
(#68)
---
src/maisa/_base_client.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index 82a5ab17..d7e55b19 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -63,7 +63,7 @@
ModelBuilderProtocol,
)
from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
-from ._compat import model_copy, model_dump
+from ._compat import PYDANTIC_V2, model_copy, model_dump
from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type
from ._response import (
APIResponse,
@@ -207,6 +207,9 @@ def _set_private_attributes(
model: Type[_T],
options: FinalRequestOptions,
) -> None:
+ if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None:
+ self.__pydantic_private__ = {}
+
self._model = model
self._client = client
self._options = options
@@ -292,6 +295,9 @@ def _set_private_attributes(
client: AsyncAPIClient,
options: FinalRequestOptions,
) -> None:
+ if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None:
+ self.__pydantic_private__ = {}
+
self._model = model
self._client = client
self._options = options
From dac10cc19c2b7a0d12e2261e55019cb2eafef2f3 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 28 Feb 2025 03:10:35 +0000
Subject: [PATCH 051/200] docs: update URLs from stainlessapi.com to
stainless.com (#69)
More details at https://www.stainless.com/changelog/stainless-com
---
README.md | 2 +-
SECURITY.md | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 9e30c8f9..cda57e46 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ The Maisa Python library provides convenient access to the Maisa REST API from a
application. The library includes type definitions for all request params and response fields,
and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).
-It is generated with [Stainless](https://www.stainlessapi.com/).
+It is generated with [Stainless](https://www.stainless.com/).
## Documentation
diff --git a/SECURITY.md b/SECURITY.md
index 459e7314..e69f1e19 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,9 +2,9 @@
## Reporting Security Issues
-This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
+This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
-To report a security issue, please contact the Stainless team at security@stainlessapi.com.
+To report a security issue, please contact the Stainless team at security@stainless.com.
## Responsible Disclosure
From 7044c85821ce9bb9837e6d7069af88249c4a983a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 28 Feb 2025 03:11:16 +0000
Subject: [PATCH 052/200] chore(docs): update client docstring (#70)
---
src/maisa/_client.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/maisa/_client.py b/src/maisa/_client.py
index de6ac769..f72cca60 100644
--- a/src/maisa/_client.py
+++ b/src/maisa/_client.py
@@ -247,7 +247,7 @@ def __init__(
# part of our public interface in the future.
_strict_response_validation: bool = False,
) -> None:
- """Construct a new async Maisa client instance.
+ """Construct a new async AsyncMaisa client instance.
This automatically infers the `api_key` argument from the `MAISA_API_KEY` environment variable if it is not provided.
"""
From 855d58284c791307976763f104eb8ecf8bb94fb3 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 4 Mar 2025 04:34:15 +0000
Subject: [PATCH 053/200] chore(internal): remove unused http client options
forwarding (#71)
---
src/maisa/_base_client.py | 97 +--------------------------------------
1 file changed, 1 insertion(+), 96 deletions(-)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index d7e55b19..cfbb4c56 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -9,7 +9,6 @@
import inspect
import logging
import platform
-import warnings
import email.utils
from types import TracebackType
from random import random
@@ -36,7 +35,7 @@
import httpx
import distro
import pydantic
-from httpx import URL, Limits
+from httpx import URL
from pydantic import PrivateAttr
from . import _exceptions
@@ -51,13 +50,10 @@
Timeout,
NotGiven,
ResponseT,
- Transport,
AnyMapping,
PostParser,
- ProxiesTypes,
RequestFiles,
HttpxSendArgs,
- AsyncTransport,
RequestOptions,
HttpxRequestFiles,
ModelBuilderProtocol,
@@ -337,9 +333,6 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
_base_url: URL
max_retries: int
timeout: Union[float, Timeout, None]
- _limits: httpx.Limits
- _proxies: ProxiesTypes | None
- _transport: Transport | AsyncTransport | None
_strict_response_validation: bool
_idempotency_header: str | None
_default_stream_cls: type[_DefaultStreamT] | None = None
@@ -352,9 +345,6 @@ def __init__(
_strict_response_validation: bool,
max_retries: int = DEFAULT_MAX_RETRIES,
timeout: float | Timeout | None = DEFAULT_TIMEOUT,
- limits: httpx.Limits,
- transport: Transport | AsyncTransport | None,
- proxies: ProxiesTypes | None,
custom_headers: Mapping[str, str] | None = None,
custom_query: Mapping[str, object] | None = None,
) -> None:
@@ -362,9 +352,6 @@ def __init__(
self._base_url = self._enforce_trailing_slash(URL(base_url))
self.max_retries = max_retries
self.timeout = timeout
- self._limits = limits
- self._proxies = proxies
- self._transport = transport
self._custom_headers = custom_headers or {}
self._custom_query = custom_query or {}
self._strict_response_validation = _strict_response_validation
@@ -800,46 +787,11 @@ def __init__(
base_url: str | URL,
max_retries: int = DEFAULT_MAX_RETRIES,
timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
- transport: Transport | None = None,
- proxies: ProxiesTypes | None = None,
- limits: Limits | None = None,
http_client: httpx.Client | None = None,
custom_headers: Mapping[str, str] | None = None,
custom_query: Mapping[str, object] | None = None,
_strict_response_validation: bool,
) -> None:
- kwargs: dict[str, Any] = {}
- if limits is not None:
- warnings.warn(
- "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead",
- category=DeprecationWarning,
- stacklevel=3,
- )
- if http_client is not None:
- raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`")
- else:
- limits = DEFAULT_CONNECTION_LIMITS
-
- if transport is not None:
- kwargs["transport"] = transport
- warnings.warn(
- "The `transport` argument is deprecated. The `http_client` argument should be passed instead",
- category=DeprecationWarning,
- stacklevel=3,
- )
- if http_client is not None:
- raise ValueError("The `http_client` argument is mutually exclusive with `transport`")
-
- if proxies is not None:
- kwargs["proxies"] = proxies
- warnings.warn(
- "The `proxies` argument is deprecated. The `http_client` argument should be passed instead",
- category=DeprecationWarning,
- stacklevel=3,
- )
- if http_client is not None:
- raise ValueError("The `http_client` argument is mutually exclusive with `proxies`")
-
if not is_given(timeout):
# if the user passed in a custom http client with a non-default
# timeout set then we use that timeout.
@@ -860,12 +812,9 @@ def __init__(
super().__init__(
version=version,
- limits=limits,
# cast to a valid type because mypy doesn't understand our type narrowing
timeout=cast(Timeout, timeout),
- proxies=proxies,
base_url=base_url,
- transport=transport,
max_retries=max_retries,
custom_query=custom_query,
custom_headers=custom_headers,
@@ -875,9 +824,6 @@ def __init__(
base_url=base_url,
# cast to a valid type because mypy doesn't understand our type narrowing
timeout=cast(Timeout, timeout),
- limits=limits,
- follow_redirects=True,
- **kwargs, # type: ignore
)
def is_closed(self) -> bool:
@@ -1372,45 +1318,10 @@ def __init__(
_strict_response_validation: bool,
max_retries: int = DEFAULT_MAX_RETRIES,
timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
- transport: AsyncTransport | None = None,
- proxies: ProxiesTypes | None = None,
- limits: Limits | None = None,
http_client: httpx.AsyncClient | None = None,
custom_headers: Mapping[str, str] | None = None,
custom_query: Mapping[str, object] | None = None,
) -> None:
- kwargs: dict[str, Any] = {}
- if limits is not None:
- warnings.warn(
- "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead",
- category=DeprecationWarning,
- stacklevel=3,
- )
- if http_client is not None:
- raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`")
- else:
- limits = DEFAULT_CONNECTION_LIMITS
-
- if transport is not None:
- kwargs["transport"] = transport
- warnings.warn(
- "The `transport` argument is deprecated. The `http_client` argument should be passed instead",
- category=DeprecationWarning,
- stacklevel=3,
- )
- if http_client is not None:
- raise ValueError("The `http_client` argument is mutually exclusive with `transport`")
-
- if proxies is not None:
- kwargs["proxies"] = proxies
- warnings.warn(
- "The `proxies` argument is deprecated. The `http_client` argument should be passed instead",
- category=DeprecationWarning,
- stacklevel=3,
- )
- if http_client is not None:
- raise ValueError("The `http_client` argument is mutually exclusive with `proxies`")
-
if not is_given(timeout):
# if the user passed in a custom http client with a non-default
# timeout set then we use that timeout.
@@ -1432,11 +1343,8 @@ def __init__(
super().__init__(
version=version,
base_url=base_url,
- limits=limits,
# cast to a valid type because mypy doesn't understand our type narrowing
timeout=cast(Timeout, timeout),
- proxies=proxies,
- transport=transport,
max_retries=max_retries,
custom_query=custom_query,
custom_headers=custom_headers,
@@ -1446,9 +1354,6 @@ def __init__(
base_url=base_url,
# cast to a valid type because mypy doesn't understand our type narrowing
timeout=cast(Timeout, timeout),
- limits=limits,
- follow_redirects=True,
- **kwargs, # type: ignore
)
def is_closed(self) -> bool:
From 8db3e915eb799879ed3ae35e19b0043628fe91a7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 11 Mar 2025 07:29:12 +0000
Subject: [PATCH 054/200] test: add DEFER_PYDANTIC_BUILD=false flag to tests
(#72)
---
scripts/test | 2 ++
1 file changed, 2 insertions(+)
diff --git a/scripts/test b/scripts/test
index 4fa5698b..2b878456 100755
--- a/scripts/test
+++ b/scripts/test
@@ -52,6 +52,8 @@ else
echo
fi
+export DEFER_PYDANTIC_BUILD=false
+
echo "==> Running tests"
rye run pytest "$@"
From 9e957376224f86d8a863fe83077513faa4d94239 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 14 Mar 2025 05:18:59 +0000
Subject: [PATCH 055/200] chore(internal): remove extra empty newlines (#73)
---
pyproject.toml | 2 --
1 file changed, 2 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index b0f84b0a..c9da5702 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -38,7 +38,6 @@ Homepage = "https://github.com/maisaai/python-sdk"
Repository = "https://github.com/maisaai/python-sdk"
-
[tool.rye]
managed = true
# version pins are in requirements-dev.lock
@@ -152,7 +151,6 @@ reportImplicitOverride = true
reportImportCycles = false
reportPrivateUsage = false
-
[tool.ruff]
line-length = 120
output-format = "grouped"
From 7f7fc88a76874624cfc14408fb3080b0b2798ba8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 15 Mar 2025 04:21:42 +0000
Subject: [PATCH 056/200] chore(internal): codegen related update (#74)
---
requirements-dev.lock | 1 +
requirements.lock | 1 +
2 files changed, 2 insertions(+)
diff --git a/requirements-dev.lock b/requirements-dev.lock
index ed583720..ae54eecb 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -7,6 +7,7 @@
# all-features: true
# with-sources: false
# generate-hashes: false
+# universal: false
-e file:.
annotated-types==0.6.0
diff --git a/requirements.lock b/requirements.lock
index 53f72df6..2d3958f0 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -7,6 +7,7 @@
# all-features: true
# with-sources: false
# generate-hashes: false
+# universal: false
-e file:.
annotated-types==0.6.0
From 0d2de6828ac638e20428cd71a03dba1aff27167a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 15 Mar 2025 04:29:41 +0000
Subject: [PATCH 057/200] chore(internal): bump rye to 0.44.0 (#75)
---
.devcontainer/Dockerfile | 2 +-
.github/workflows/ci.yml | 4 ++--
.github/workflows/publish-pypi.yml | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 55d20255..ff261bad 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -3,7 +3,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
USER vscode
-RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.35.0" RYE_INSTALL_OPTION="--yes" bash
+RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash
ENV PATH=/home/vscode/.rye/shims:$PATH
RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c8a8a4f7..3b286e5a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,7 +21,7 @@ jobs:
curl -sSf https://rye.astral.sh/get | bash
echo "$HOME/.rye/shims" >> $GITHUB_PATH
env:
- RYE_VERSION: '0.35.0'
+ RYE_VERSION: '0.44.0'
RYE_INSTALL_OPTION: '--yes'
- name: Install dependencies
@@ -42,7 +42,7 @@ jobs:
curl -sSf https://rye.astral.sh/get | bash
echo "$HOME/.rye/shims" >> $GITHUB_PATH
env:
- RYE_VERSION: '0.35.0'
+ RYE_VERSION: '0.44.0'
RYE_INSTALL_OPTION: '--yes'
- name: Bootstrap
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index a251ab22..a79667d3 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -21,7 +21,7 @@ jobs:
curl -sSf https://rye.astral.sh/get | bash
echo "$HOME/.rye/shims" >> $GITHUB_PATH
env:
- RYE_VERSION: '0.35.0'
+ RYE_VERSION: '0.44.0'
RYE_INSTALL_OPTION: '--yes'
- name: Publish to PyPI
From 4c5855527fa35f1de85c19f3eb44080c96671fe8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 15 Mar 2025 04:36:03 +0000
Subject: [PATCH 058/200] fix(types): handle more discriminated union shapes
(#76)
---
src/maisa/_models.py | 7 +++++--
tests/test_models.py | 32 ++++++++++++++++++++++++++++++++
2 files changed, 37 insertions(+), 2 deletions(-)
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index c4401ff8..b51a1bf5 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -65,7 +65,7 @@
from ._constants import RAW_RESPONSE_HEADER
if TYPE_CHECKING:
- from pydantic_core.core_schema import ModelField, LiteralSchema, ModelFieldsSchema
+ from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema
__all__ = ["BaseModel", "GenericModel"]
@@ -646,15 +646,18 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None:
schema = model.__pydantic_core_schema__
+ if schema["type"] == "definitions":
+ schema = schema["schema"]
+
if schema["type"] != "model":
return None
+ schema = cast("ModelSchema", schema)
fields_schema = schema["schema"]
if fields_schema["type"] != "model-fields":
return None
fields_schema = cast("ModelFieldsSchema", fields_schema)
-
field = fields_schema["fields"].get(field_name)
if not field:
return None
diff --git a/tests/test_models.py b/tests/test_models.py
index 73c41a9d..dd187b5b 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -854,3 +854,35 @@ class Model(BaseModel):
m = construct_type(value={"cls": "foo"}, type_=Model)
assert isinstance(m, Model)
assert isinstance(m.cls, str)
+
+
+def test_discriminated_union_case() -> None:
+ class A(BaseModel):
+ type: Literal["a"]
+
+ data: bool
+
+ class B(BaseModel):
+ type: Literal["b"]
+
+ data: List[Union[A, object]]
+
+ class ModelA(BaseModel):
+ type: Literal["modelA"]
+
+ data: int
+
+ class ModelB(BaseModel):
+ type: Literal["modelB"]
+
+ required: str
+
+ data: Union[A, B]
+
+ # when constructing ModelA | ModelB, value data doesn't match ModelB exactly - missing `required`
+ m = construct_type(
+ value={"type": "modelB", "data": {"type": "a", "data": True}},
+ type_=cast(Any, Annotated[Union[ModelA, ModelB], PropertyInfo(discriminator="type")]),
+ )
+
+ assert isinstance(m, ModelB)
From 5eb597ec123f7ca80ccefbefaeda0a9397297c9e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 17 Mar 2025 16:23:23 +0000
Subject: [PATCH 059/200] fix(ci): ensure pip is always available (#77)
---
bin/publish-pypi | 1 +
1 file changed, 1 insertion(+)
diff --git a/bin/publish-pypi b/bin/publish-pypi
index 05bfccbb..ebebf916 100644
--- a/bin/publish-pypi
+++ b/bin/publish-pypi
@@ -5,5 +5,6 @@ mkdir -p dist
rye build --clean
# Patching importlib-metadata version until upstream library version is updated
# https://github.com/pypa/twine/issues/977#issuecomment-2189800841
+"$HOME/.rye/self/bin/python3" -m ensurepip
"$HOME/.rye/self/bin/python3" -m pip install 'importlib-metadata==7.2.1'
rye publish --yes --token=$PYPI_TOKEN
From b142c8802df54c2601023ad97841be29c9605ae4 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 17 Mar 2025 16:30:14 +0000
Subject: [PATCH 060/200] fix(ci): remove publishing patch (#78)
---
bin/publish-pypi | 4 ----
pyproject.toml | 2 +-
2 files changed, 1 insertion(+), 5 deletions(-)
diff --git a/bin/publish-pypi b/bin/publish-pypi
index ebebf916..826054e9 100644
--- a/bin/publish-pypi
+++ b/bin/publish-pypi
@@ -3,8 +3,4 @@
set -eux
mkdir -p dist
rye build --clean
-# Patching importlib-metadata version until upstream library version is updated
-# https://github.com/pypa/twine/issues/977#issuecomment-2189800841
-"$HOME/.rye/self/bin/python3" -m ensurepip
-"$HOME/.rye/self/bin/python3" -m pip install 'importlib-metadata==7.2.1'
rye publish --yes --token=$PYPI_TOKEN
diff --git a/pyproject.toml b/pyproject.toml
index c9da5702..6a90e9cb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -86,7 +86,7 @@ typecheck = { chain = [
"typecheck:mypy" = "mypy ."
[build-system]
-requires = ["hatchling", "hatch-fancy-pypi-readme"]
+requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"]
build-backend = "hatchling.build"
[tool.hatch.build]
From 2b40b832f20097203b6fcb3cbb78a73ed0f34640 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 27 Mar 2025 03:40:21 +0000
Subject: [PATCH 061/200] chore: fix typos (#79)
---
src/maisa/_models.py | 2 +-
src/maisa/_utils/_transform.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index b51a1bf5..34935716 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -681,7 +681,7 @@ def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None:
setattr(typ, "__pydantic_config__", config) # noqa: B010
-# our use of subclasssing here causes weirdness for type checkers,
+# our use of subclassing here causes weirdness for type checkers,
# so we just pretend that we don't subclass
if TYPE_CHECKING:
GenericModel = BaseModel
diff --git a/src/maisa/_utils/_transform.py b/src/maisa/_utils/_transform.py
index 18afd9d8..7ac2e17f 100644
--- a/src/maisa/_utils/_transform.py
+++ b/src/maisa/_utils/_transform.py
@@ -126,7 +126,7 @@ def _get_annotated_type(type_: type) -> type | None:
def _maybe_transform_key(key: str, type_: type) -> str:
"""Transform the given `data` based on the annotations provided in `type_`.
- Note: this function only looks at `Annotated` types that contain `PropertInfo` metadata.
+ Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata.
"""
annotated_type = _get_annotated_type(type_)
if annotated_type is None:
From 8821387a9b51abb18f874fcf8f6b714ee1b6a57e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 27 Mar 2025 03:41:48 +0000
Subject: [PATCH 062/200] codegen metadata
---
.stats.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.stats.yml b/.stats.yml
index 4076c464..16464750 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,2 +1,4 @@
configured_endpoints: 13
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/maisa%2FMaisa-c82e0a80b379d33d11af783865ec15844bcd750043bf0f1543ebdf72bda79f3a.yml
+openapi_spec_hash: 67e2baafe7455c2a734ac0b0ea699719
+config_hash: d6a8d922bf8b98716e3f55b9c829cd45
From e336af250e5b570100da9573ee341bb81859c850 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 4 Apr 2025 09:33:28 +0000
Subject: [PATCH 063/200] chore(internal): remove trailing character (#80)
---
tests/test_client.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_client.py b/tests/test_client.py
index d8045ff6..a60c9024 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -1586,7 +1586,7 @@ def test_get_platform(self) -> None:
import threading
from maisa._utils import asyncify
- from maisa._base_client import get_platform
+ from maisa._base_client import get_platform
async def test_main() -> None:
result = await asyncify(get_platform)()
From 725bac24208c0107455b11db84a860039905ae3e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 9 Apr 2025 02:34:32 +0000
Subject: [PATCH 064/200] chore(internal): slight transform perf improvement
(#81)
---
src/maisa/_utils/_transform.py | 22 ++++++++++++++++++++++
tests/test_transform.py | 12 ++++++++++++
2 files changed, 34 insertions(+)
diff --git a/src/maisa/_utils/_transform.py b/src/maisa/_utils/_transform.py
index 7ac2e17f..3ec62081 100644
--- a/src/maisa/_utils/_transform.py
+++ b/src/maisa/_utils/_transform.py
@@ -142,6 +142,10 @@ def _maybe_transform_key(key: str, type_: type) -> str:
return key
+def _no_transform_needed(annotation: type) -> bool:
+ return annotation == float or annotation == int
+
+
def _transform_recursive(
data: object,
*,
@@ -184,6 +188,15 @@ def _transform_recursive(
return cast(object, data)
inner_type = extract_type_arg(stripped_type, 0)
+ if _no_transform_needed(inner_type):
+ # for some types there is no need to transform anything, so we can get a small
+ # perf boost from skipping that work.
+ #
+ # but we still need to convert to a list to ensure the data is json-serializable
+ if is_list(data):
+ return data
+ return list(data)
+
return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data]
if is_union_type(stripped_type):
@@ -332,6 +345,15 @@ async def _async_transform_recursive(
return cast(object, data)
inner_type = extract_type_arg(stripped_type, 0)
+ if _no_transform_needed(inner_type):
+ # for some types there is no need to transform anything, so we can get a small
+ # perf boost from skipping that work.
+ #
+ # but we still need to convert to a list to ensure the data is json-serializable
+ if is_list(data):
+ return data
+ return list(data)
+
return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data]
if is_union_type(stripped_type):
diff --git a/tests/test_transform.py b/tests/test_transform.py
index 73315099..e451516a 100644
--- a/tests/test_transform.py
+++ b/tests/test_transform.py
@@ -432,3 +432,15 @@ async def test_base64_file_input(use_async: bool) -> None:
assert await transform({"foo": io.BytesIO(b"Hello, world!")}, TypedDictBase64Input, use_async) == {
"foo": "SGVsbG8sIHdvcmxkIQ=="
} # type: ignore[comparison-overlap]
+
+
+@parametrize
+@pytest.mark.asyncio
+async def test_transform_skipping(use_async: bool) -> None:
+ # lists of ints are left as-is
+ data = [1, 2, 3]
+ assert await transform(data, List[int], use_async) is data
+
+ # iterables of ints are converted to a list
+ data = iter([1, 2, 3])
+ assert await transform(data, Iterable[int], use_async) == [1, 2, 3]
From 992264cfce392fa1504cc65871652aa0452fec82 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 9 Apr 2025 02:35:05 +0000
Subject: [PATCH 065/200] chore(tests): improve enum examples (#82)
---
tests/api_resources/test_capabilities.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/api_resources/test_capabilities.py b/tests/api_resources/test_capabilities.py
index 724f1f6d..913e52c8 100644
--- a/tests/api_resources/test_capabilities.py
+++ b/tests/api_resources/test_capabilities.py
@@ -161,7 +161,7 @@ def test_method_summarize_with_all_params(self, client: Maisa) -> None:
text="Example long text...",
format="paragraph",
lang="en",
- length="short",
+ length="medium",
summary_hint="Example summary of the text...",
)
assert_matches_type(TextSummary, capability, path=["response"])
@@ -338,7 +338,7 @@ async def test_method_summarize_with_all_params(self, async_client: AsyncMaisa)
text="Example long text...",
format="paragraph",
lang="en",
- length="short",
+ length="medium",
summary_hint="Example summary of the text...",
)
assert_matches_type(TextSummary, capability, path=["response"])
From b01b22eac5d5ea0177624858b062b9f30f75c3ed Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 9 Apr 2025 02:39:19 +0000
Subject: [PATCH 066/200] chore: slight wording improvement in README (#83)
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index cda57e46..5b158e25 100644
--- a/README.md
+++ b/README.md
@@ -73,7 +73,7 @@ Typed requests and responses provide autocomplete and documentation within your
## File uploads
-Request parameters that correspond to file uploads can be passed as `bytes`, a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`.
+Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`.
```python
from pathlib import Path
From db760ce185cc1f105e7ef2667ca67fa188748ac1 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 10 Apr 2025 02:43:33 +0000
Subject: [PATCH 067/200] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 16464750..817e9225 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 13
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/maisa%2FMaisa-c82e0a80b379d33d11af783865ec15844bcd750043bf0f1543ebdf72bda79f3a.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/maisa%2Fmaisa-c82e0a80b379d33d11af783865ec15844bcd750043bf0f1543ebdf72bda79f3a.yml
openapi_spec_hash: 67e2baafe7455c2a734ac0b0ea699719
config_hash: d6a8d922bf8b98716e3f55b9c829cd45
From b849844d128bb9cc3f04d16f126c075018013b92 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 10 Apr 2025 02:48:16 +0000
Subject: [PATCH 068/200] chore(internal): expand CI branch coverage
---
.github/workflows/ci.yml | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3b286e5a..53a3a09c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,18 +1,18 @@
name: CI
on:
push:
- branches:
- - main
- pull_request:
- branches:
- - main
- - next
+ branches-ignore:
+ - 'generated'
+ - 'codegen/**'
+ - 'integrated/**'
+ - 'preview-head/**'
+ - 'preview-base/**'
+ - 'preview/**'
jobs:
lint:
name: lint
runs-on: ubuntu-latest
-
steps:
- uses: actions/checkout@v4
@@ -33,7 +33,6 @@ jobs:
test:
name: test
runs-on: ubuntu-latest
-
steps:
- uses: actions/checkout@v4
From 971a4ef91cb47972bbc2fbcbea369c84bb4890d8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 10 Apr 2025 02:52:28 +0000
Subject: [PATCH 069/200] chore(internal): reduce CI branch coverage
---
.github/workflows/ci.yml | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 53a3a09c..81f6dc20 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,13 +1,12 @@
name: CI
on:
push:
- branches-ignore:
- - 'generated'
- - 'codegen/**'
- - 'integrated/**'
- - 'preview-head/**'
- - 'preview-base/**'
- - 'preview/**'
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+ - next
jobs:
lint:
From 96b5e8a122394ac86460aec62d0319bb81b8b0a5 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 12 Apr 2025 02:45:30 +0000
Subject: [PATCH 070/200] fix(perf): skip traversing types for NotGiven values
---
src/maisa/_utils/_transform.py | 11 +++++++++++
tests/test_transform.py | 9 ++++++++-
2 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/src/maisa/_utils/_transform.py b/src/maisa/_utils/_transform.py
index 3ec62081..3b2b8e00 100644
--- a/src/maisa/_utils/_transform.py
+++ b/src/maisa/_utils/_transform.py
@@ -12,6 +12,7 @@
from ._utils import (
is_list,
+ is_given,
is_mapping,
is_iterable,
)
@@ -258,6 +259,11 @@ def _transform_typeddict(
result: dict[str, object] = {}
annotations = get_type_hints(expected_type, include_extras=True)
for key, value in data.items():
+ if not is_given(value):
+ # we don't need to include `NotGiven` values here as they'll
+ # be stripped out before the request is sent anyway
+ continue
+
type_ = annotations.get(key)
if type_ is None:
# we do not have a type annotation for this field, leave it as is
@@ -415,6 +421,11 @@ async def _async_transform_typeddict(
result: dict[str, object] = {}
annotations = get_type_hints(expected_type, include_extras=True)
for key, value in data.items():
+ if not is_given(value):
+ # we don't need to include `NotGiven` values here as they'll
+ # be stripped out before the request is sent anyway
+ continue
+
type_ = annotations.get(key)
if type_ is None:
# we do not have a type annotation for this field, leave it as is
diff --git a/tests/test_transform.py b/tests/test_transform.py
index e451516a..12d050be 100644
--- a/tests/test_transform.py
+++ b/tests/test_transform.py
@@ -8,7 +8,7 @@
import pytest
-from maisa._types import Base64FileInput
+from maisa._types import NOT_GIVEN, Base64FileInput
from maisa._utils import (
PropertyInfo,
transform as _transform,
@@ -444,3 +444,10 @@ async def test_transform_skipping(use_async: bool) -> None:
# iterables of ints are converted to a list
data = iter([1, 2, 3])
assert await transform(data, Iterable[int], use_async) == [1, 2, 3]
+
+
+@parametrize
+@pytest.mark.asyncio
+async def test_strips_notgiven(use_async: bool) -> None:
+ assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"}
+ assert await transform({"foo_bar": NOT_GIVEN}, Foo1, use_async) == {}
From c0a0d6e0b5a5a3327f2e50c5e5d87f1db62abd97 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 12 Apr 2025 02:46:26 +0000
Subject: [PATCH 071/200] fix(perf): optimize some hot paths
---
src/maisa/_utils/_transform.py | 14 +++++++++++++-
src/maisa/_utils/_typing.py | 2 ++
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/src/maisa/_utils/_transform.py b/src/maisa/_utils/_transform.py
index 3b2b8e00..b0cc20a7 100644
--- a/src/maisa/_utils/_transform.py
+++ b/src/maisa/_utils/_transform.py
@@ -5,7 +5,7 @@
import pathlib
from typing import Any, Mapping, TypeVar, cast
from datetime import date, datetime
-from typing_extensions import Literal, get_args, override, get_type_hints
+from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints
import anyio
import pydantic
@@ -13,6 +13,7 @@
from ._utils import (
is_list,
is_given,
+ lru_cache,
is_mapping,
is_iterable,
)
@@ -109,6 +110,7 @@ class Params(TypedDict, total=False):
return cast(_T, transformed)
+@lru_cache(maxsize=8096)
def _get_annotated_type(type_: type) -> type | None:
"""If the given type is an `Annotated` type then it is returned, if not `None` is returned.
@@ -433,3 +435,13 @@ async def _async_transform_typeddict(
else:
result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_)
return result
+
+
+@lru_cache(maxsize=8096)
+def get_type_hints(
+ obj: Any,
+ globalns: dict[str, Any] | None = None,
+ localns: Mapping[str, Any] | None = None,
+ include_extras: bool = False,
+) -> dict[str, Any]:
+ return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras)
diff --git a/src/maisa/_utils/_typing.py b/src/maisa/_utils/_typing.py
index 278749b1..1958820f 100644
--- a/src/maisa/_utils/_typing.py
+++ b/src/maisa/_utils/_typing.py
@@ -13,6 +13,7 @@
get_origin,
)
+from ._utils import lru_cache
from .._types import InheritsGeneric
from .._compat import is_union as _is_union
@@ -66,6 +67,7 @@ def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]:
# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]]
+@lru_cache(maxsize=8096)
def strip_annotated_type(typ: type) -> type:
if is_required_type(typ) or is_annotated_type(typ):
return strip_annotated_type(cast(type, get_args(typ)[0]))
From 86bac8f7145e4097a4ca5295db678d37dc99dddc Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 15 Apr 2025 03:04:02 +0000
Subject: [PATCH 072/200] chore(internal): update pyright settings
---
pyproject.toml | 1 +
1 file changed, 1 insertion(+)
diff --git a/pyproject.toml b/pyproject.toml
index 6a90e9cb..9e62772d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -147,6 +147,7 @@ exclude = [
]
reportImplicitOverride = true
+reportOverlappingOverload = false
reportImportCycles = false
reportPrivateUsage = false
From 46c60128342a86388db165058fd8801da92e8d33 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 15 Apr 2025 03:05:29 +0000
Subject: [PATCH 073/200] chore(client): minor internal fixes
---
src/maisa/_base_client.py | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index cfbb4c56..628db1a8 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -409,7 +409,8 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0
idempotency_header = self._idempotency_header
if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers:
- headers[idempotency_header] = options.idempotency_key or self._idempotency_key()
+ options.idempotency_key = options.idempotency_key or self._idempotency_key()
+ headers[idempotency_header] = options.idempotency_key
# Don't set these headers if they were already set or removed by the caller. We check
# `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case.
@@ -943,6 +944,10 @@ def _request(
request = self._build_request(options, retries_taken=retries_taken)
self._prepare_request(request)
+ if options.idempotency_key:
+ # ensure the idempotency key is reused between requests
+ input_options.idempotency_key = options.idempotency_key
+
kwargs: HttpxSendArgs = {}
if self.custom_auth is not None:
kwargs["auth"] = self.custom_auth
@@ -1475,6 +1480,10 @@ async def _request(
request = self._build_request(options, retries_taken=retries_taken)
await self._prepare_request(request)
+ if options.idempotency_key:
+ # ensure the idempotency key is reused between requests
+ input_options.idempotency_key = options.idempotency_key
+
kwargs: HttpxSendArgs = {}
if self.custom_auth is not None:
kwargs["auth"] = self.custom_auth
From 14e4c2012cfca8f2b159917d3e63a8c79b850341 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 17 Apr 2025 02:50:48 +0000
Subject: [PATCH 074/200] chore(internal): bump pyright version
---
pyproject.toml | 2 +-
requirements-dev.lock | 2 +-
src/maisa/_base_client.py | 6 +++++-
src/maisa/_models.py | 1 -
src/maisa/_utils/_typing.py | 2 +-
tests/conftest.py | 2 +-
tests/test_models.py | 2 +-
7 files changed, 10 insertions(+), 7 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 9e62772d..ac252c5f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,7 +42,7 @@ Repository = "https://github.com/maisaai/python-sdk"
managed = true
# version pins are in requirements-dev.lock
dev-dependencies = [
- "pyright>=1.1.359",
+ "pyright==1.1.399",
"mypy",
"respx",
"pytest",
diff --git a/requirements-dev.lock b/requirements-dev.lock
index ae54eecb..336690be 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -69,7 +69,7 @@ pydantic-core==2.27.1
# via pydantic
pygments==2.18.0
# via rich
-pyright==1.1.392.post0
+pyright==1.1.399
pytest==8.3.3
# via pytest-asyncio
pytest-asyncio==0.24.0
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index 628db1a8..75bff23a 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -98,7 +98,11 @@
_AsyncStreamT = TypeVar("_AsyncStreamT", bound=AsyncStream[Any])
if TYPE_CHECKING:
- from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT
+ from httpx._config import (
+ DEFAULT_TIMEOUT_CONFIG, # pyright: ignore[reportPrivateImportUsage]
+ )
+
+ HTTPX_DEFAULT_TIMEOUT = DEFAULT_TIMEOUT_CONFIG
else:
try:
from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index 34935716..58b9263e 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -19,7 +19,6 @@
)
import pydantic
-import pydantic.generics
from pydantic.fields import FieldInfo
from ._types import (
diff --git a/src/maisa/_utils/_typing.py b/src/maisa/_utils/_typing.py
index 1958820f..1bac9542 100644
--- a/src/maisa/_utils/_typing.py
+++ b/src/maisa/_utils/_typing.py
@@ -110,7 +110,7 @@ class MyResponse(Foo[_T]):
```
"""
cls = cast(object, get_origin(typ) or typ)
- if cls in generic_bases:
+ if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains]
# we're given the class directly
return extract_type_arg(typ, index)
diff --git a/tests/conftest.py b/tests/conftest.py
index e4a04e40..1d3331e5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,7 +10,7 @@
from maisa import Maisa, AsyncMaisa
if TYPE_CHECKING:
- from _pytest.fixtures import FixtureRequest
+ from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage]
pytest.register_assert_rewrite("tests.utils")
diff --git a/tests/test_models.py b/tests/test_models.py
index dd187b5b..32baf8f5 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -832,7 +832,7 @@ class B(BaseModel):
@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1")
def test_type_alias_type() -> None:
- Alias = TypeAliasType("Alias", str)
+ Alias = TypeAliasType("Alias", str) # pyright: ignore
class Model(BaseModel):
alias: Alias
From e30b668cf0787e16dd26a0a77e13e04cc0a09200 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 17 Apr 2025 02:51:28 +0000
Subject: [PATCH 075/200] chore(internal): base client updates
---
src/maisa/_base_client.py | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index 75bff23a..e5c3d487 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -119,6 +119,7 @@ class PageInfo:
url: URL | NotGiven
params: Query | NotGiven
+ json: Body | NotGiven
@overload
def __init__(
@@ -134,19 +135,30 @@ def __init__(
params: Query,
) -> None: ...
+ @overload
+ def __init__(
+ self,
+ *,
+ json: Body,
+ ) -> None: ...
+
def __init__(
self,
*,
url: URL | NotGiven = NOT_GIVEN,
+ json: Body | NotGiven = NOT_GIVEN,
params: Query | NotGiven = NOT_GIVEN,
) -> None:
self.url = url
+ self.json = json
self.params = params
@override
def __repr__(self) -> str:
if self.url:
return f"{self.__class__.__name__}(url={self.url})"
+ if self.json:
+ return f"{self.__class__.__name__}(json={self.json})"
return f"{self.__class__.__name__}(params={self.params})"
@@ -195,6 +207,19 @@ def _info_to_options(self, info: PageInfo) -> FinalRequestOptions:
options.url = str(url)
return options
+ if not isinstance(info.json, NotGiven):
+ if not is_mapping(info.json):
+ raise TypeError("Pagination is only supported with mappings")
+
+ if not options.json_data:
+ options.json_data = {**info.json}
+ else:
+ if not is_mapping(options.json_data):
+ raise TypeError("Pagination is only supported with mappings")
+
+ options.json_data = {**options.json_data, **info.json}
+ return options
+
raise ValueError("Unexpected PageInfo state")
From 2625b528dd6c331336ff2edb6e37a2eb55c16d34 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 19 Apr 2025 02:58:22 +0000
Subject: [PATCH 076/200] chore(internal): update models test
---
tests/test_models.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tests/test_models.py b/tests/test_models.py
index 32baf8f5..d52ab260 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -492,12 +492,15 @@ class Model(BaseModel):
resource_id: Optional[str] = None
m = Model.construct()
+ assert m.resource_id is None
assert "resource_id" not in m.model_fields_set
m = Model.construct(resource_id=None)
+ assert m.resource_id is None
assert "resource_id" in m.model_fields_set
m = Model.construct(resource_id="foo")
+ assert m.resource_id == "foo"
assert "resource_id" in m.model_fields_set
From c64f88bcb87e36d04d3965dd3a1cd9394782b0ae Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 23 Apr 2025 03:37:24 +0000
Subject: [PATCH 077/200] chore(ci): add timeout thresholds for CI jobs
---
.github/workflows/ci.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 81f6dc20..04b083ca 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,6 +10,7 @@ on:
jobs:
lint:
+ timeout-minutes: 10
name: lint
runs-on: ubuntu-latest
steps:
@@ -30,6 +31,7 @@ jobs:
run: ./scripts/lint
test:
+ timeout-minutes: 10
name: test
runs-on: ubuntu-latest
steps:
From 69505f7ff92e385feba9854eae4e597ac748c7c8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 23 Apr 2025 03:37:59 +0000
Subject: [PATCH 078/200] chore(internal): import reformatting
---
src/maisa/_client.py | 5 +----
src/maisa/resources/capabilities/capabilities.py | 5 +----
src/maisa/resources/capabilities/media.py | 7 +------
src/maisa/resources/file_interpreter/from_audio.py | 7 +------
src/maisa/resources/file_interpreter/from_docx.py | 7 +------
src/maisa/resources/file_interpreter/from_html.py | 7 +------
src/maisa/resources/file_interpreter/from_image.py | 7 +------
src/maisa/resources/file_interpreter/from_pdf.py | 7 +------
src/maisa/resources/kpu.py | 7 +------
src/maisa/resources/models/embeddings.py | 5 +----
10 files changed, 10 insertions(+), 54 deletions(-)
diff --git a/src/maisa/_client.py b/src/maisa/_client.py
index f72cca60..7b112683 100644
--- a/src/maisa/_client.py
+++ b/src/maisa/_client.py
@@ -19,10 +19,7 @@
ProxiesTypes,
RequestOptions,
)
-from ._utils import (
- is_given,
- get_async_library,
-)
+from ._utils import is_given, get_async_library
from ._version import __version__
from .resources import kpu
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
diff --git a/src/maisa/resources/capabilities/capabilities.py b/src/maisa/resources/capabilities/capabilities.py
index a154ede2..8412385e 100644
--- a/src/maisa/resources/capabilities/capabilities.py
+++ b/src/maisa/resources/capabilities/capabilities.py
@@ -17,10 +17,7 @@
)
from ...types import capability_compare_params, capability_extract_params, capability_summarize_params
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from ..._utils import (
- maybe_transform,
- async_maybe_transform,
-)
+from ..._utils import maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
diff --git a/src/maisa/resources/capabilities/media.py b/src/maisa/resources/capabilities/media.py
index d7ad849d..2f3a399d 100644
--- a/src/maisa/resources/capabilities/media.py
+++ b/src/maisa/resources/capabilities/media.py
@@ -8,12 +8,7 @@
import httpx
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
-from ..._utils import (
- extract_files,
- maybe_transform,
- deepcopy_minimal,
- async_maybe_transform,
-)
+from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
diff --git a/src/maisa/resources/file_interpreter/from_audio.py b/src/maisa/resources/file_interpreter/from_audio.py
index e0b676c1..a543de36 100644
--- a/src/maisa/resources/file_interpreter/from_audio.py
+++ b/src/maisa/resources/file_interpreter/from_audio.py
@@ -7,12 +7,7 @@
import httpx
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
-from ..._utils import (
- extract_files,
- maybe_transform,
- deepcopy_minimal,
- async_maybe_transform,
-)
+from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
diff --git a/src/maisa/resources/file_interpreter/from_docx.py b/src/maisa/resources/file_interpreter/from_docx.py
index 2675b3be..fbff89b8 100644
--- a/src/maisa/resources/file_interpreter/from_docx.py
+++ b/src/maisa/resources/file_interpreter/from_docx.py
@@ -7,12 +7,7 @@
import httpx
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
-from ..._utils import (
- extract_files,
- maybe_transform,
- deepcopy_minimal,
- async_maybe_transform,
-)
+from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
diff --git a/src/maisa/resources/file_interpreter/from_html.py b/src/maisa/resources/file_interpreter/from_html.py
index fbc9c0f1..ca2cbe08 100644
--- a/src/maisa/resources/file_interpreter/from_html.py
+++ b/src/maisa/resources/file_interpreter/from_html.py
@@ -7,12 +7,7 @@
import httpx
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
-from ..._utils import (
- extract_files,
- maybe_transform,
- deepcopy_minimal,
- async_maybe_transform,
-)
+from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
diff --git a/src/maisa/resources/file_interpreter/from_image.py b/src/maisa/resources/file_interpreter/from_image.py
index 0278ae9f..2d35cc0b 100644
--- a/src/maisa/resources/file_interpreter/from_image.py
+++ b/src/maisa/resources/file_interpreter/from_image.py
@@ -7,12 +7,7 @@
import httpx
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
-from ..._utils import (
- extract_files,
- maybe_transform,
- deepcopy_minimal,
- async_maybe_transform,
-)
+from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
diff --git a/src/maisa/resources/file_interpreter/from_pdf.py b/src/maisa/resources/file_interpreter/from_pdf.py
index f9e20b86..3286c935 100644
--- a/src/maisa/resources/file_interpreter/from_pdf.py
+++ b/src/maisa/resources/file_interpreter/from_pdf.py
@@ -7,12 +7,7 @@
import httpx
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
-from ..._utils import (
- extract_files,
- maybe_transform,
- deepcopy_minimal,
- async_maybe_transform,
-)
+from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
diff --git a/src/maisa/resources/kpu.py b/src/maisa/resources/kpu.py
index 3bfdb1c2..006e4d11 100644
--- a/src/maisa/resources/kpu.py
+++ b/src/maisa/resources/kpu.py
@@ -9,12 +9,7 @@
from ..types import kpu_run_params
from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
-from .._utils import (
- extract_files,
- maybe_transform,
- deepcopy_minimal,
- async_maybe_transform,
-)
+from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import (
diff --git a/src/maisa/resources/models/embeddings.py b/src/maisa/resources/models/embeddings.py
index 4e9f3c12..974cfa29 100644
--- a/src/maisa/resources/models/embeddings.py
+++ b/src/maisa/resources/models/embeddings.py
@@ -7,10 +7,7 @@
import httpx
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
-from ..._utils import (
- maybe_transform,
- async_maybe_transform,
-)
+from ..._utils import maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
From dcc896c62ce95f2a76b4f153c2c86045f31d1192 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 23 Apr 2025 03:39:27 +0000
Subject: [PATCH 079/200] chore(internal): fix list file params
---
src/maisa/_utils/_utils.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/src/maisa/_utils/_utils.py b/src/maisa/_utils/_utils.py
index e5811bba..ea3cf3f2 100644
--- a/src/maisa/_utils/_utils.py
+++ b/src/maisa/_utils/_utils.py
@@ -72,8 +72,16 @@ def _extract_items(
from .._files import assert_is_file_content
# We have exhausted the path, return the entry we found.
- assert_is_file_content(obj, key=flattened_key)
assert flattened_key is not None
+
+ if is_list(obj):
+ files: list[tuple[str, FileTypes]] = []
+ for entry in obj:
+ assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "")
+ files.append((flattened_key + "[]", cast(FileTypes, entry)))
+ return files
+
+ assert_is_file_content(obj, key=flattened_key)
return [(flattened_key, cast(FileTypes, obj))]
index += 1
From d8eb684bc1f626f87bc0228ef09af0d4c71c2769 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 23 Apr 2025 03:40:02 +0000
Subject: [PATCH 080/200] chore(internal): refactor retries to not use
recursion
---
src/maisa/_base_client.py | 414 ++++++++++++++++----------------------
1 file changed, 175 insertions(+), 239 deletions(-)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index e5c3d487..f79d9b3d 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -437,8 +437,7 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0
headers = httpx.Headers(headers_dict)
idempotency_header = self._idempotency_header
- if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers:
- options.idempotency_key = options.idempotency_key or self._idempotency_key()
+ if idempotency_header and options.idempotency_key and idempotency_header not in headers:
headers[idempotency_header] = options.idempotency_key
# Don't set these headers if they were already set or removed by the caller. We check
@@ -903,7 +902,6 @@ def request(
self,
cast_to: Type[ResponseT],
options: FinalRequestOptions,
- remaining_retries: Optional[int] = None,
*,
stream: Literal[True],
stream_cls: Type[_StreamT],
@@ -914,7 +912,6 @@ def request(
self,
cast_to: Type[ResponseT],
options: FinalRequestOptions,
- remaining_retries: Optional[int] = None,
*,
stream: Literal[False] = False,
) -> ResponseT: ...
@@ -924,7 +921,6 @@ def request(
self,
cast_to: Type[ResponseT],
options: FinalRequestOptions,
- remaining_retries: Optional[int] = None,
*,
stream: bool = False,
stream_cls: Type[_StreamT] | None = None,
@@ -934,125 +930,109 @@ def request(
self,
cast_to: Type[ResponseT],
options: FinalRequestOptions,
- remaining_retries: Optional[int] = None,
*,
stream: bool = False,
stream_cls: type[_StreamT] | None = None,
) -> ResponseT | _StreamT:
- if remaining_retries is not None:
- retries_taken = options.get_max_retries(self.max_retries) - remaining_retries
- else:
- retries_taken = 0
-
- return self._request(
- cast_to=cast_to,
- options=options,
- stream=stream,
- stream_cls=stream_cls,
- retries_taken=retries_taken,
- )
+ cast_to = self._maybe_override_cast_to(cast_to, options)
- def _request(
- self,
- *,
- cast_to: Type[ResponseT],
- options: FinalRequestOptions,
- retries_taken: int,
- stream: bool,
- stream_cls: type[_StreamT] | None,
- ) -> ResponseT | _StreamT:
# create a copy of the options we were given so that if the
# options are mutated later & we then retry, the retries are
# given the original options
input_options = model_copy(options)
-
- cast_to = self._maybe_override_cast_to(cast_to, options)
- options = self._prepare_options(options)
-
- remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
- request = self._build_request(options, retries_taken=retries_taken)
- self._prepare_request(request)
-
- if options.idempotency_key:
+ if input_options.idempotency_key is None and input_options.method.lower() != "get":
# ensure the idempotency key is reused between requests
- input_options.idempotency_key = options.idempotency_key
+ input_options.idempotency_key = self._idempotency_key()
- kwargs: HttpxSendArgs = {}
- if self.custom_auth is not None:
- kwargs["auth"] = self.custom_auth
+ response: httpx.Response | None = None
+ max_retries = input_options.get_max_retries(self.max_retries)
- log.debug("Sending HTTP Request: %s %s", request.method, request.url)
+ retries_taken = 0
+ for retries_taken in range(max_retries + 1):
+ options = model_copy(input_options)
+ options = self._prepare_options(options)
- try:
- response = self._client.send(
- request,
- stream=stream or self._should_stream_response_body(request=request),
- **kwargs,
- )
- except httpx.TimeoutException as err:
- log.debug("Encountered httpx.TimeoutException", exc_info=True)
+ remaining_retries = max_retries - retries_taken
+ request = self._build_request(options, retries_taken=retries_taken)
+ self._prepare_request(request)
- if remaining_retries > 0:
- return self._retry_request(
- input_options,
- cast_to,
- retries_taken=retries_taken,
- stream=stream,
- stream_cls=stream_cls,
- response_headers=None,
- )
+ kwargs: HttpxSendArgs = {}
+ if self.custom_auth is not None:
+ kwargs["auth"] = self.custom_auth
- log.debug("Raising timeout error")
- raise APITimeoutError(request=request) from err
- except Exception as err:
- log.debug("Encountered Exception", exc_info=True)
+ log.debug("Sending HTTP Request: %s %s", request.method, request.url)
- if remaining_retries > 0:
- return self._retry_request(
- input_options,
- cast_to,
- retries_taken=retries_taken,
- stream=stream,
- stream_cls=stream_cls,
- response_headers=None,
+ response = None
+ try:
+ response = self._client.send(
+ request,
+ stream=stream or self._should_stream_response_body(request=request),
+ **kwargs,
)
+ except httpx.TimeoutException as err:
+ log.debug("Encountered httpx.TimeoutException", exc_info=True)
+
+ if remaining_retries > 0:
+ self._sleep_for_retry(
+ retries_taken=retries_taken,
+ max_retries=max_retries,
+ options=input_options,
+ response=None,
+ )
+ continue
+
+ log.debug("Raising timeout error")
+ raise APITimeoutError(request=request) from err
+ except Exception as err:
+ log.debug("Encountered Exception", exc_info=True)
+
+ if remaining_retries > 0:
+ self._sleep_for_retry(
+ retries_taken=retries_taken,
+ max_retries=max_retries,
+ options=input_options,
+ response=None,
+ )
+ continue
+
+ log.debug("Raising connection error")
+ raise APIConnectionError(request=request) from err
+
+ log.debug(
+ 'HTTP Response: %s %s "%i %s" %s',
+ request.method,
+ request.url,
+ response.status_code,
+ response.reason_phrase,
+ response.headers,
+ )
- log.debug("Raising connection error")
- raise APIConnectionError(request=request) from err
-
- log.debug(
- 'HTTP Response: %s %s "%i %s" %s',
- request.method,
- request.url,
- response.status_code,
- response.reason_phrase,
- response.headers,
- )
+ try:
+ response.raise_for_status()
+ except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
+ log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
+
+ if remaining_retries > 0 and self._should_retry(err.response):
+ err.response.close()
+ self._sleep_for_retry(
+ retries_taken=retries_taken,
+ max_retries=max_retries,
+ options=input_options,
+ response=response,
+ )
+ continue
- try:
- response.raise_for_status()
- except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
- log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
-
- if remaining_retries > 0 and self._should_retry(err.response):
- err.response.close()
- return self._retry_request(
- input_options,
- cast_to,
- retries_taken=retries_taken,
- response_headers=err.response.headers,
- stream=stream,
- stream_cls=stream_cls,
- )
+ # If the response is streamed then we need to explicitly read the response
+ # to completion before attempting to access the response text.
+ if not err.response.is_closed:
+ err.response.read()
- # If the response is streamed then we need to explicitly read the response
- # to completion before attempting to access the response text.
- if not err.response.is_closed:
- err.response.read()
+ log.debug("Re-raising status error")
+ raise self._make_status_error_from_response(err.response) from None
- log.debug("Re-raising status error")
- raise self._make_status_error_from_response(err.response) from None
+ break
+ assert response is not None, "could not resolve response (should never happen)"
return self._process_response(
cast_to=cast_to,
options=options,
@@ -1062,37 +1042,20 @@ def _request(
retries_taken=retries_taken,
)
- def _retry_request(
- self,
- options: FinalRequestOptions,
- cast_to: Type[ResponseT],
- *,
- retries_taken: int,
- response_headers: httpx.Headers | None,
- stream: bool,
- stream_cls: type[_StreamT] | None,
- ) -> ResponseT | _StreamT:
- remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
+ def _sleep_for_retry(
+ self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None
+ ) -> None:
+ remaining_retries = max_retries - retries_taken
if remaining_retries == 1:
log.debug("1 retry left")
else:
log.debug("%i retries left", remaining_retries)
- timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers)
+ timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None)
log.info("Retrying request to %s in %f seconds", options.url, timeout)
- # In a synchronous context we are blocking the entire thread. Up to the library user to run the client in a
- # different thread if necessary.
time.sleep(timeout)
- return self._request(
- options=options,
- cast_to=cast_to,
- retries_taken=retries_taken + 1,
- stream=stream,
- stream_cls=stream_cls,
- )
-
def _process_response(
self,
*,
@@ -1436,7 +1399,6 @@ async def request(
options: FinalRequestOptions,
*,
stream: Literal[False] = False,
- remaining_retries: Optional[int] = None,
) -> ResponseT: ...
@overload
@@ -1447,7 +1409,6 @@ async def request(
*,
stream: Literal[True],
stream_cls: type[_AsyncStreamT],
- remaining_retries: Optional[int] = None,
) -> _AsyncStreamT: ...
@overload
@@ -1458,7 +1419,6 @@ async def request(
*,
stream: bool,
stream_cls: type[_AsyncStreamT] | None = None,
- remaining_retries: Optional[int] = None,
) -> ResponseT | _AsyncStreamT: ...
async def request(
@@ -1468,120 +1428,111 @@ async def request(
*,
stream: bool = False,
stream_cls: type[_AsyncStreamT] | None = None,
- remaining_retries: Optional[int] = None,
- ) -> ResponseT | _AsyncStreamT:
- if remaining_retries is not None:
- retries_taken = options.get_max_retries(self.max_retries) - remaining_retries
- else:
- retries_taken = 0
-
- return await self._request(
- cast_to=cast_to,
- options=options,
- stream=stream,
- stream_cls=stream_cls,
- retries_taken=retries_taken,
- )
-
- async def _request(
- self,
- cast_to: Type[ResponseT],
- options: FinalRequestOptions,
- *,
- stream: bool,
- stream_cls: type[_AsyncStreamT] | None,
- retries_taken: int,
) -> ResponseT | _AsyncStreamT:
if self._platform is None:
# `get_platform` can make blocking IO calls so we
# execute it earlier while we are in an async context
self._platform = await asyncify(get_platform)()
+ cast_to = self._maybe_override_cast_to(cast_to, options)
+
# create a copy of the options we were given so that if the
# options are mutated later & we then retry, the retries are
# given the original options
input_options = model_copy(options)
-
- cast_to = self._maybe_override_cast_to(cast_to, options)
- options = await self._prepare_options(options)
-
- remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
- request = self._build_request(options, retries_taken=retries_taken)
- await self._prepare_request(request)
-
- if options.idempotency_key:
+ if input_options.idempotency_key is None and input_options.method.lower() != "get":
# ensure the idempotency key is reused between requests
- input_options.idempotency_key = options.idempotency_key
+ input_options.idempotency_key = self._idempotency_key()
- kwargs: HttpxSendArgs = {}
- if self.custom_auth is not None:
- kwargs["auth"] = self.custom_auth
+ response: httpx.Response | None = None
+ max_retries = input_options.get_max_retries(self.max_retries)
- try:
- response = await self._client.send(
- request,
- stream=stream or self._should_stream_response_body(request=request),
- **kwargs,
- )
- except httpx.TimeoutException as err:
- log.debug("Encountered httpx.TimeoutException", exc_info=True)
+ retries_taken = 0
+ for retries_taken in range(max_retries + 1):
+ options = model_copy(input_options)
+ options = await self._prepare_options(options)
- if remaining_retries > 0:
- return await self._retry_request(
- input_options,
- cast_to,
- retries_taken=retries_taken,
- stream=stream,
- stream_cls=stream_cls,
- response_headers=None,
- )
+ remaining_retries = max_retries - retries_taken
+ request = self._build_request(options, retries_taken=retries_taken)
+ await self._prepare_request(request)
- log.debug("Raising timeout error")
- raise APITimeoutError(request=request) from err
- except Exception as err:
- log.debug("Encountered Exception", exc_info=True)
+ kwargs: HttpxSendArgs = {}
+ if self.custom_auth is not None:
+ kwargs["auth"] = self.custom_auth
- if remaining_retries > 0:
- return await self._retry_request(
- input_options,
- cast_to,
- retries_taken=retries_taken,
- stream=stream,
- stream_cls=stream_cls,
- response_headers=None,
- )
+ log.debug("Sending HTTP Request: %s %s", request.method, request.url)
- log.debug("Raising connection error")
- raise APIConnectionError(request=request) from err
+ response = None
+ try:
+ response = await self._client.send(
+ request,
+ stream=stream or self._should_stream_response_body(request=request),
+ **kwargs,
+ )
+ except httpx.TimeoutException as err:
+ log.debug("Encountered httpx.TimeoutException", exc_info=True)
+
+ if remaining_retries > 0:
+ await self._sleep_for_retry(
+ retries_taken=retries_taken,
+ max_retries=max_retries,
+ options=input_options,
+ response=None,
+ )
+ continue
+
+ log.debug("Raising timeout error")
+ raise APITimeoutError(request=request) from err
+ except Exception as err:
+ log.debug("Encountered Exception", exc_info=True)
+
+ if remaining_retries > 0:
+ await self._sleep_for_retry(
+ retries_taken=retries_taken,
+ max_retries=max_retries,
+ options=input_options,
+ response=None,
+ )
+ continue
+
+ log.debug("Raising connection error")
+ raise APIConnectionError(request=request) from err
+
+ log.debug(
+ 'HTTP Response: %s %s "%i %s" %s',
+ request.method,
+ request.url,
+ response.status_code,
+ response.reason_phrase,
+ response.headers,
+ )
- log.debug(
- 'HTTP Request: %s %s "%i %s"', request.method, request.url, response.status_code, response.reason_phrase
- )
+ try:
+ response.raise_for_status()
+ except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
+ log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
+
+ if remaining_retries > 0 and self._should_retry(err.response):
+ await err.response.aclose()
+ await self._sleep_for_retry(
+ retries_taken=retries_taken,
+ max_retries=max_retries,
+ options=input_options,
+ response=response,
+ )
+ continue
- try:
- response.raise_for_status()
- except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
- log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
-
- if remaining_retries > 0 and self._should_retry(err.response):
- await err.response.aclose()
- return await self._retry_request(
- input_options,
- cast_to,
- retries_taken=retries_taken,
- response_headers=err.response.headers,
- stream=stream,
- stream_cls=stream_cls,
- )
+ # If the response is streamed then we need to explicitly read the response
+ # to completion before attempting to access the response text.
+ if not err.response.is_closed:
+ await err.response.aread()
- # If the response is streamed then we need to explicitly read the response
- # to completion before attempting to access the response text.
- if not err.response.is_closed:
- await err.response.aread()
+ log.debug("Re-raising status error")
+ raise self._make_status_error_from_response(err.response) from None
- log.debug("Re-raising status error")
- raise self._make_status_error_from_response(err.response) from None
+ break
+ assert response is not None, "could not resolve response (should never happen)"
return await self._process_response(
cast_to=cast_to,
options=options,
@@ -1591,35 +1542,20 @@ async def _request(
retries_taken=retries_taken,
)
- async def _retry_request(
- self,
- options: FinalRequestOptions,
- cast_to: Type[ResponseT],
- *,
- retries_taken: int,
- response_headers: httpx.Headers | None,
- stream: bool,
- stream_cls: type[_AsyncStreamT] | None,
- ) -> ResponseT | _AsyncStreamT:
- remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
+ async def _sleep_for_retry(
+ self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None
+ ) -> None:
+ remaining_retries = max_retries - retries_taken
if remaining_retries == 1:
log.debug("1 retry left")
else:
log.debug("%i retries left", remaining_retries)
- timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers)
+ timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None)
log.info("Retrying request to %s in %f seconds", options.url, timeout)
await anyio.sleep(timeout)
- return await self._request(
- options=options,
- cast_to=cast_to,
- retries_taken=retries_taken + 1,
- stream=stream,
- stream_cls=stream_cls,
- )
-
async def _process_response(
self,
*,
From dcf4095858cbd3c8794eb38ba5092e4040e7efe3 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 23 Apr 2025 03:40:34 +0000
Subject: [PATCH 081/200] fix(pydantic v1): more robust ModelField.annotation
check
---
src/maisa/_models.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index 58b9263e..798956f1 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -626,8 +626,8 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
# Note: if one variant defines an alias then they all should
discriminator_alias = field_info.alias
- if field_info.annotation and is_literal_type(field_info.annotation):
- for entry in get_args(field_info.annotation):
+ if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation):
+ for entry in get_args(annotation):
if isinstance(entry, str):
mapping[entry] = variant
From 8933135761ff9085dcc53ba504c0c7469328769f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 24 Apr 2025 02:37:35 +0000
Subject: [PATCH 082/200] chore(internal): minor formatting changes
---
src/maisa/types/shared/text_comparator.py | 1 -
src/maisa/types/shared/text_extractor.py | 1 -
src/maisa/types/shared/text_summary.py | 1 -
3 files changed, 3 deletions(-)
diff --git a/src/maisa/types/shared/text_comparator.py b/src/maisa/types/shared/text_comparator.py
index 646f30ad..1c7408f7 100644
--- a/src/maisa/types/shared/text_comparator.py
+++ b/src/maisa/types/shared/text_comparator.py
@@ -1,6 +1,5 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
from ..._models import BaseModel
__all__ = ["TextComparator"]
diff --git a/src/maisa/types/shared/text_extractor.py b/src/maisa/types/shared/text_extractor.py
index d58d0905..0b243200 100644
--- a/src/maisa/types/shared/text_extractor.py
+++ b/src/maisa/types/shared/text_extractor.py
@@ -1,6 +1,5 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
from ..._models import BaseModel
__all__ = ["TextExtractor"]
diff --git a/src/maisa/types/shared/text_summary.py b/src/maisa/types/shared/text_summary.py
index 86d1c11a..53aed0de 100644
--- a/src/maisa/types/shared/text_summary.py
+++ b/src/maisa/types/shared/text_summary.py
@@ -1,6 +1,5 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
from ..._models import BaseModel
__all__ = ["TextSummary"]
From 2bdbbe484071b54ac7ce2bf15404aa1e33c4e766 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 24 Apr 2025 02:38:11 +0000
Subject: [PATCH 083/200] chore(internal): codegen related update
---
.github/workflows/ci.yml | 16 ++++++++--------
.github/workflows/publish-pypi.yml | 2 +-
.github/workflows/release-doctor.yml | 2 +-
3 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 04b083ca..33820422 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,18 +1,18 @@
name: CI
on:
push:
- branches:
- - main
- pull_request:
- branches:
- - main
- - next
+ branches-ignore:
+ - 'generated'
+ - 'codegen/**'
+ - 'integrated/**'
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
jobs:
lint:
timeout-minutes: 10
name: lint
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -33,7 +33,7 @@ jobs:
test:
timeout-minutes: 10
name: test
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index a79667d3..93a94d03 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -11,7 +11,7 @@ on:
jobs:
publish:
name: publish
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index e2332a84..732ad304 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -8,7 +8,7 @@ on:
jobs:
release_doctor:
name: release doctor
- runs-on: ubuntu-latest
+ runs-on: depot-ubuntu-24.04
if: github.repository == 'maisaai/python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
From a34edb6a27cc6c6365749cc45ebb3a7ae13d480e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 24 Apr 2025 02:38:44 +0000
Subject: [PATCH 084/200] chore(ci): only use depot for staging repos
---
.github/workflows/ci.yml | 4 ++--
.github/workflows/publish-pypi.yml | 2 +-
.github/workflows/release-doctor.yml | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 33820422..2c74c56a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,7 +12,7 @@ jobs:
lint:
timeout-minutes: 10
name: lint
- runs-on: depot-ubuntu-24.04
+ runs-on: ${{ github.repository == 'stainless-sdks/maisa-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v4
@@ -33,7 +33,7 @@ jobs:
test:
timeout-minutes: 10
name: test
- runs-on: depot-ubuntu-24.04
+ runs-on: ${{ github.repository == 'stainless-sdks/maisa-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index 93a94d03..a79667d3 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -11,7 +11,7 @@ on:
jobs:
publish:
name: publish
- runs-on: depot-ubuntu-24.04
+ runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index 732ad304..e2332a84 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -8,7 +8,7 @@ on:
jobs:
release_doctor:
name: release doctor
- runs-on: depot-ubuntu-24.04
+ runs-on: ubuntu-latest
if: github.repository == 'maisaai/python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
From 484ef558aedf0bfe5b0a34e302b22254f0170963 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 24 Apr 2025 02:40:36 +0000
Subject: [PATCH 085/200] chore: broadly detect json family of content-type
headers
---
src/maisa/_response.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/maisa/_response.py b/src/maisa/_response.py
index 84d4c98d..89735fac 100644
--- a/src/maisa/_response.py
+++ b/src/maisa/_response.py
@@ -233,7 +233,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
# split is required to handle cases where additional information is included
# in the response, e.g. application/json; charset=utf-8
content_type, *_ = response.headers.get("content-type", "*").split(";")
- if content_type != "application/json":
+ if not content_type.endswith("json"):
if is_basemodel(cast_to):
try:
data = response.json()
From a7b6a0505996024a903de3d310e16233ba99ddff Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 9 May 2025 04:12:09 +0000
Subject: [PATCH 086/200] chore(internal): avoid errors for isinstance checks
on proxies
---
src/maisa/_utils/_proxy.py | 5 ++++-
tests/test_utils/test_proxy.py | 11 +++++++++++
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/src/maisa/_utils/_proxy.py b/src/maisa/_utils/_proxy.py
index ffd883e9..0f239a33 100644
--- a/src/maisa/_utils/_proxy.py
+++ b/src/maisa/_utils/_proxy.py
@@ -46,7 +46,10 @@ def __dir__(self) -> Iterable[str]:
@property # type: ignore
@override
def __class__(self) -> type: # pyright: ignore
- proxied = self.__get_proxied__()
+ try:
+ proxied = self.__get_proxied__()
+ except Exception:
+ return type(self)
if issubclass(type(proxied), LazyProxy):
return type(proxied)
return proxied.__class__
diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py
index 1cbfeb0f..a223b183 100644
--- a/tests/test_utils/test_proxy.py
+++ b/tests/test_utils/test_proxy.py
@@ -21,3 +21,14 @@ def test_recursive_proxy() -> None:
assert dir(proxy) == []
assert type(proxy).__name__ == "RecursiveLazyProxy"
assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy"
+
+
+def test_isinstance_does_not_error() -> None:
+ class AlwaysErrorProxy(LazyProxy[Any]):
+ @override
+ def __load__(self) -> Any:
+ raise RuntimeError("Mocking missing dependency")
+
+ proxy = AlwaysErrorProxy()
+ assert not isinstance(proxy, dict)
+ assert isinstance(proxy, LazyProxy)
From 9443d25d49504fcdc76e6c7fe435339113500146 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 10 May 2025 03:34:22 +0000
Subject: [PATCH 087/200] fix(package): support direct resource imports
---
src/maisa/__init__.py | 5 +++++
src/maisa/_utils/_resources_proxy.py | 24 ++++++++++++++++++++++++
2 files changed, 29 insertions(+)
create mode 100644 src/maisa/_utils/_resources_proxy.py
diff --git a/src/maisa/__init__.py b/src/maisa/__init__.py
index 6c1e07e4..430b9c4a 100644
--- a/src/maisa/__init__.py
+++ b/src/maisa/__init__.py
@@ -1,5 +1,7 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+import typing as _t
+
from . import types
from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes
from ._utils import file_from_path
@@ -68,6 +70,9 @@
"DefaultAsyncHttpxClient",
]
+if not _t.TYPE_CHECKING:
+ from ._utils._resources_proxy import resources as resources
+
_setup_logging()
# Update the __module__ attribute for exported symbols so that
diff --git a/src/maisa/_utils/_resources_proxy.py b/src/maisa/_utils/_resources_proxy.py
new file mode 100644
index 00000000..13c1a7fb
--- /dev/null
+++ b/src/maisa/_utils/_resources_proxy.py
@@ -0,0 +1,24 @@
+from __future__ import annotations
+
+from typing import Any
+from typing_extensions import override
+
+from ._proxy import LazyProxy
+
+
+class ResourcesProxy(LazyProxy[Any]):
+ """A proxy for the `maisa.resources` module.
+
+ This is used so that we can lazily import `maisa.resources` only when
+ needed *and* so that users can just import `maisa` and reference `maisa.resources`
+ """
+
+ @override
+ def __load__(self) -> Any:
+ import importlib
+
+ mod = importlib.import_module("maisa.resources")
+ return mod
+
+
+resources = ResourcesProxy().__as_proxied__()
From 92b578ebad4bd611eb0b7e28890cdb2f40f3622f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 15 May 2025 04:48:46 +0000
Subject: [PATCH 088/200] chore(ci): upload sdks to package manager
---
.github/workflows/ci.yml | 24 ++++++++++++++++++++++++
scripts/utils/upload-artifact.sh | 25 +++++++++++++++++++++++++
2 files changed, 49 insertions(+)
create mode 100755 scripts/utils/upload-artifact.sh
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2c74c56a..0ec43d31 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,6 +30,30 @@ jobs:
- name: Run lints
run: ./scripts/lint
+ upload:
+ if: github.repository == 'stainless-sdks/maisa-python'
+ timeout-minutes: 10
+ name: upload
+ permissions:
+ contents: read
+ id-token: write
+ runs-on: depot-ubuntu-24.04
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Get GitHub OIDC Token
+ id: github-oidc
+ uses: actions/github-script@v6
+ with:
+ script: core.setOutput('github_token', await core.getIDToken());
+
+ - name: Upload tarball
+ env:
+ URL: https://pkg.stainless.com/s
+ AUTH: ${{ steps.github-oidc.outputs.github_token }}
+ SHA: ${{ github.sha }}
+ run: ./scripts/utils/upload-artifact.sh
+
test:
timeout-minutes: 10
name: test
diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh
new file mode 100755
index 00000000..42aec717
--- /dev/null
+++ b/scripts/utils/upload-artifact.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+set -exuo pipefail
+
+RESPONSE=$(curl -X POST "$URL" \
+ -H "Authorization: Bearer $AUTH" \
+ -H "Content-Type: application/json")
+
+SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url')
+
+if [[ "$SIGNED_URL" == "null" ]]; then
+ echo -e "\033[31mFailed to get signed URL.\033[0m"
+ exit 1
+fi
+
+UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \
+ -H "Content-Type: application/gzip" \
+ --data-binary @- "$SIGNED_URL" 2>&1)
+
+if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then
+ echo -e "\033[32mUploaded build to Stainless storage.\033[0m"
+ echo -e "\033[32mInstallation: npm install 'https://pkg.stainless.com/s/maisa-python/$SHA'\033[0m"
+else
+ echo -e "\033[31mFailed to upload artifact.\033[0m"
+ exit 1
+fi
From 3ec60bfd974811e6ed892566497405a2388a6ede Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 16 May 2025 03:43:49 +0000
Subject: [PATCH 089/200] chore(ci): fix installation instructions
---
scripts/utils/upload-artifact.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh
index 42aec717..855cd87d 100755
--- a/scripts/utils/upload-artifact.sh
+++ b/scripts/utils/upload-artifact.sh
@@ -18,7 +18,7 @@ UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \
if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then
echo -e "\033[32mUploaded build to Stainless storage.\033[0m"
- echo -e "\033[32mInstallation: npm install 'https://pkg.stainless.com/s/maisa-python/$SHA'\033[0m"
+ echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/maisa-python/$SHA'\033[0m"
else
echo -e "\033[31mFailed to upload artifact.\033[0m"
exit 1
From 07cb56f3ec286adc1744b5080056c1156a8f850d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 17 May 2025 02:56:22 +0000
Subject: [PATCH 090/200] chore(internal): codegen related update
---
scripts/utils/upload-artifact.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh
index 855cd87d..9af38144 100755
--- a/scripts/utils/upload-artifact.sh
+++ b/scripts/utils/upload-artifact.sh
@@ -18,7 +18,7 @@ UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \
if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then
echo -e "\033[32mUploaded build to Stainless storage.\033[0m"
- echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/maisa-python/$SHA'\033[0m"
+ echo -e "\033[32mInstallation: pip install --pre 'https://pkg.stainless.com/s/maisa-python/$SHA'\033[0m"
else
echo -e "\033[31mFailed to upload artifact.\033[0m"
exit 1
From 365638e4c43e29b6d8384a782eb2f54abeff608e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 22 May 2025 02:36:18 +0000
Subject: [PATCH 091/200] chore(docs): grammar improvements
---
SECURITY.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/SECURITY.md b/SECURITY.md
index e69f1e19..f7e82534 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -16,11 +16,11 @@ before making any information public.
## Reporting Non-SDK Related Security Issues
If you encounter security issues that are not directly related to SDKs but pertain to the services
-or products provided by Maisa please follow the respective company's security reporting guidelines.
+or products provided by Maisa, please follow the respective company's security reporting guidelines.
### Maisa Terms and Policies
-Please contact support@maisa.ai for any questions or concerns regarding security of our services.
+Please contact support@maisa.ai for any questions or concerns regarding the security of our services.
---
From b86bfb75296288127820f5f46c98a9525ba0183c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 28 May 2025 02:26:34 +0000
Subject: [PATCH 092/200] chore(internal): codegen related update
---
README.md | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 5b158e25..48bf1aa2 100644
--- a/README.md
+++ b/README.md
@@ -24,9 +24,12 @@ pip install --pre maisa
The full API of this library can be found in [api.md](api.md).
```python
+import os
from maisa import Maisa
-client = Maisa()
+client = Maisa(
+ api_key=os.environ.get("MAISA_API_KEY"), # This is the default and can be omitted
+)
text_summary = client.capabilities.summarize(
text="Example long text...",
@@ -44,10 +47,13 @@ so that your API Key is not stored in source control.
Simply import `AsyncMaisa` instead of `Maisa` and use `await` with each API call:
```python
+import os
import asyncio
from maisa import AsyncMaisa
-client = AsyncMaisa()
+client = AsyncMaisa(
+ api_key=os.environ.get("MAISA_API_KEY"), # This is the default and can be omitted
+)
async def main() -> None:
From 28735f60ec7129a1026a1b361a66fb034ccf6584 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 28 May 2025 03:33:20 +0000
Subject: [PATCH 093/200] fix(docs/api): remove references to nonexistent types
---
api.md | 48 ++++++------------------------------------------
1 file changed, 6 insertions(+), 42 deletions(-)
diff --git a/api.md b/api.md
index 163cb96c..2ffe8eb2 100644
--- a/api.md
+++ b/api.md
@@ -36,74 +36,38 @@ Methods:
# Kpu
-Types:
-
-```python
-from maisa.types import KpuRunResponse
-```
-
Methods:
-- client.kpu.run(\*\*params) -> object
+- client.kpu.run(\*\*params) -> object
# FileInterpreter
## FromPdf
-Types:
-
-```python
-from maisa.types.file_interpreter import FromPdfCreateResponse
-```
-
Methods:
-- client.file_interpreter.from_pdf.create(\*\*params) -> object
+- client.file_interpreter.from_pdf.create(\*\*params) -> object
## FromDocx
-Types:
-
-```python
-from maisa.types.file_interpreter import FromDocxCreateResponse
-```
-
Methods:
-- client.file_interpreter.from_docx.create(\*\*params) -> object
+- client.file_interpreter.from_docx.create(\*\*params) -> object
## FromHTML
-Types:
-
-```python
-from maisa.types.file_interpreter import FromHTMLCreateResponse
-```
-
Methods:
-- client.file_interpreter.from_html.create(\*\*params) -> object
+- client.file_interpreter.from_html.create(\*\*params) -> object
## FromImage
-Types:
-
-```python
-from maisa.types.file_interpreter import FromImageCreateResponse
-```
-
Methods:
-- client.file_interpreter.from_image.create(\*\*params) -> object
+- client.file_interpreter.from_image.create(\*\*params) -> object
## FromAudio
-Types:
-
-```python
-from maisa.types.file_interpreter import FromAudioCreateResponse
-```
-
Methods:
-- client.file_interpreter.from_audio.create(\*\*params) -> object
+- client.file_interpreter.from_audio.create(\*\*params) -> object
From df13a62c2c7ed732f3c4fed57ace19569caac052 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 3 Jun 2025 02:29:34 +0000
Subject: [PATCH 094/200] chore(docs): remove reference to rye shell
---
CONTRIBUTING.md | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a2ddcfb0..a1e868dc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -17,8 +17,7 @@ $ rye sync --all-features
You can then run scripts using `rye run python script.py` or by activating the virtual environment:
```sh
-$ rye shell
-# or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work
+# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work
$ source .venv/bin/activate
# now you can omit the `rye run` prefix
From cbd27b4498b8cefde68fd1a1dfd1078ed7d29767 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 3 Jun 2025 03:45:05 +0000
Subject: [PATCH 095/200] feat(client): add follow_redirects request option
---
src/maisa/_base_client.py | 6 +++++
src/maisa/_models.py | 2 ++
src/maisa/_types.py | 2 ++
tests/test_client.py | 54 +++++++++++++++++++++++++++++++++++++++
4 files changed, 64 insertions(+)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index f79d9b3d..21edc17e 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -960,6 +960,9 @@ def request(
if self.custom_auth is not None:
kwargs["auth"] = self.custom_auth
+ if options.follow_redirects is not None:
+ kwargs["follow_redirects"] = options.follow_redirects
+
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
response = None
@@ -1460,6 +1463,9 @@ async def request(
if self.custom_auth is not None:
kwargs["auth"] = self.custom_auth
+ if options.follow_redirects is not None:
+ kwargs["follow_redirects"] = options.follow_redirects
+
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
response = None
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index 798956f1..4f214980 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -737,6 +737,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
idempotency_key: str
json_data: Body
extra_json: AnyMapping
+ follow_redirects: bool
@final
@@ -750,6 +751,7 @@ class FinalRequestOptions(pydantic.BaseModel):
files: Union[HttpxRequestFiles, None] = None
idempotency_key: Union[str, None] = None
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
+ follow_redirects: Union[bool, None] = None
# It should be noted that we cannot use `json` here as that would override
# a BaseModel method in an incompatible fashion.
diff --git a/src/maisa/_types.py b/src/maisa/_types.py
index 9bc2b62d..eb1f7534 100644
--- a/src/maisa/_types.py
+++ b/src/maisa/_types.py
@@ -100,6 +100,7 @@ class RequestOptions(TypedDict, total=False):
params: Query
extra_json: AnyMapping
idempotency_key: str
+ follow_redirects: bool
# Sentinel class used until PEP 0661 is accepted
@@ -215,3 +216,4 @@ class _GenericAlias(Protocol):
class HttpxSendArgs(TypedDict, total=False):
auth: httpx.Auth
+ follow_redirects: bool
diff --git a/tests/test_client.py b/tests/test_client.py
index a60c9024..c51563fd 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -803,6 +803,33 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
assert response.http_request.headers.get("x-stainless-retry-count") == "42"
+ @pytest.mark.respx(base_url=base_url)
+ def test_follow_redirects(self, respx_mock: MockRouter) -> None:
+ # Test that the default follow_redirects=True allows following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+ respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
+
+ response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
+ assert response.status_code == 200
+ assert response.json() == {"status": "ok"}
+
+ @pytest.mark.respx(base_url=base_url)
+ def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
+ # Test that follow_redirects=False prevents following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+
+ with pytest.raises(APIStatusError) as exc_info:
+ self.client.post(
+ "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
+ )
+
+ assert exc_info.value.response.status_code == 302
+ assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
+
class TestAsyncMaisa:
client = AsyncMaisa(base_url=base_url, api_key=api_key, _strict_response_validation=True)
@@ -1618,3 +1645,30 @@ async def test_main() -> None:
raise AssertionError("calling get_platform using asyncify resulted in a hung process")
time.sleep(0.1)
+
+ @pytest.mark.respx(base_url=base_url)
+ async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
+ # Test that the default follow_redirects=True allows following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+ respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
+
+ response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
+ assert response.status_code == 200
+ assert response.json() == {"status": "ok"}
+
+ @pytest.mark.respx(base_url=base_url)
+ async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
+ # Test that follow_redirects=False prevents following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+
+ with pytest.raises(APIStatusError) as exc_info:
+ await self.client.post(
+ "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
+ )
+
+ assert exc_info.value.response.status_code == 302
+ assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
From 672fffa3fe4800289419bf14d4532b3a4cdf07fc Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 13 Jun 2025 02:15:36 +0000
Subject: [PATCH 096/200] chore(tests): run tests in parallel
---
pyproject.toml | 3 ++-
requirements-dev.lock | 4 ++++
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index ac252c5f..e3db0410 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,6 +54,7 @@ dev-dependencies = [
"importlib-metadata>=6.7.0",
"rich>=13.7.1",
"nest_asyncio==1.6.0",
+ "pytest-xdist>=3.6.1",
]
[tool.rye.scripts]
@@ -125,7 +126,7 @@ replacement = '[\1](https://github.com/maisaai/python-sdk/tree/main/\g<2>)'
[tool.pytest.ini_options]
testpaths = ["tests"]
-addopts = "--tb=short"
+addopts = "--tb=short -n auto"
xfail_strict = true
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 336690be..ad16a7f8 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -30,6 +30,8 @@ distro==1.8.0
exceptiongroup==1.2.2
# via anyio
# via pytest
+execnet==2.1.1
+ # via pytest-xdist
filelock==3.12.4
# via virtualenv
h11==0.14.0
@@ -72,7 +74,9 @@ pygments==2.18.0
pyright==1.1.399
pytest==8.3.3
# via pytest-asyncio
+ # via pytest-xdist
pytest-asyncio==0.24.0
+pytest-xdist==3.7.0
python-dateutil==2.8.2
# via time-machine
pytz==2023.3.post1
From 0762f8da514337c02d925be41cfbcd799fc3439d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 13 Jun 2025 02:40:10 +0000
Subject: [PATCH 097/200] fix(client): correctly parse binary response | stream
---
src/maisa/_base_client.py | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index 21edc17e..9bc17acb 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -1071,7 +1071,14 @@ def _process_response(
) -> ResponseT:
origin = get_origin(cast_to) or cast_to
- if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse):
+ if (
+ inspect.isclass(origin)
+ and issubclass(origin, BaseAPIResponse)
+ # we only want to actually return the custom BaseAPIResponse class if we're
+ # returning the raw response, or if we're not streaming SSE, as if we're streaming
+ # SSE then `cast_to` doesn't actively reflect the type we need to parse into
+ and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER)))
+ ):
if not issubclass(origin, APIResponse):
raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}")
@@ -1574,7 +1581,14 @@ async def _process_response(
) -> ResponseT:
origin = get_origin(cast_to) or cast_to
- if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse):
+ if (
+ inspect.isclass(origin)
+ and issubclass(origin, BaseAPIResponse)
+ # we only want to actually return the custom BaseAPIResponse class if we're
+ # returning the raw response, or if we're not streaming SSE, as if we're streaming
+ # SSE then `cast_to` doesn't actively reflect the type we need to parse into
+ and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER)))
+ ):
if not issubclass(origin, AsyncAPIResponse):
raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}")
From 911a987972c2a0d25113272994702f0c8eb07abb Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 17 Jun 2025 02:46:53 +0000
Subject: [PATCH 098/200] chore(tests): add tests for httpx client
instantiation & proxies
---
tests/test_client.py | 53 +++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 52 insertions(+), 1 deletion(-)
diff --git a/tests/test_client.py b/tests/test_client.py
index c51563fd..bab6b172 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -27,7 +27,14 @@
from maisa._models import BaseModel, FinalRequestOptions
from maisa._constants import RAW_RESPONSE_HEADER
from maisa._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError
-from maisa._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options
+from maisa._base_client import (
+ DEFAULT_TIMEOUT,
+ HTTPX_DEFAULT_TIMEOUT,
+ BaseClient,
+ DefaultHttpxClient,
+ DefaultAsyncHttpxClient,
+ make_request_options,
+)
from maisa.types.capability_summarize_params import CapabilitySummarizeParams
from .utils import update_env
@@ -803,6 +810,28 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
assert response.http_request.headers.get("x-stainless-retry-count") == "42"
+ def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ # Test that the proxy environment variables are set correctly
+ monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
+
+ client = DefaultHttpxClient()
+
+ mounts = tuple(client._mounts.items())
+ assert len(mounts) == 1
+ assert mounts[0][0].pattern == "https://"
+
+ @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning")
+ def test_default_client_creation(self) -> None:
+ # Ensure that the client can be initialized without any exceptions
+ DefaultHttpxClient(
+ verify=True,
+ cert=None,
+ trust_env=True,
+ http1=True,
+ http2=False,
+ limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
+ )
+
@pytest.mark.respx(base_url=base_url)
def test_follow_redirects(self, respx_mock: MockRouter) -> None:
# Test that the default follow_redirects=True allows following redirects
@@ -1646,6 +1675,28 @@ async def test_main() -> None:
time.sleep(0.1)
+ async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ # Test that the proxy environment variables are set correctly
+ monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
+
+ client = DefaultAsyncHttpxClient()
+
+ mounts = tuple(client._mounts.items())
+ assert len(mounts) == 1
+ assert mounts[0][0].pattern == "https://"
+
+ @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning")
+ async def test_default_client_creation(self) -> None:
+ # Ensure that the client can be initialized without any exceptions
+ DefaultAsyncHttpxClient(
+ verify=True,
+ cert=None,
+ trust_env=True,
+ http1=True,
+ http2=False,
+ limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
+ )
+
@pytest.mark.respx(base_url=base_url)
async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
# Test that the default follow_redirects=True allows following redirects
From 0b96f69329ed5c975ef8013acac2e699949f2c2c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 17 Jun 2025 04:16:20 +0000
Subject: [PATCH 099/200] chore(internal): update conftest.py
---
tests/conftest.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tests/conftest.py b/tests/conftest.py
index 1d3331e5..1789d2ef 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,3 +1,5 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
from __future__ import annotations
import os
From 4d4582354137ab157b489745da734da45250aa09 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 17 Jun 2025 06:47:01 +0000
Subject: [PATCH 100/200] chore(ci): enable for pull requests
---
.github/workflows/ci.yml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0ec43d31..1643341c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,6 +7,10 @@ on:
- 'integrated/**'
- 'stl-preview-head/**'
- 'stl-preview-base/**'
+ pull_request:
+ branches-ignore:
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
jobs:
lint:
From c47640be2fdb0f0d1c6b33eaf1eb11a771f892b7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 18 Jun 2025 02:19:12 +0000
Subject: [PATCH 101/200] chore(readme): update badges
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 48bf1aa2..a8360311 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Maisa Python API library
-[](https://pypi.org/project/maisa/)
+[![PyPI version]()](https://pypi.org/project/maisa/)
The Maisa Python library provides convenient access to the Maisa REST API from any Python 3.8+
application. The library includes type definitions for all request params and response fields,
From 1edfc1391bb51fad2f3f0468eae25b24e7124549 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 18 Jun 2025 05:55:11 +0000
Subject: [PATCH 102/200] fix(tests): fix: tests which call HTTP endpoints
directly with the example parameters
---
tests/test_client.py | 41 ++++++++---------------------------------
1 file changed, 8 insertions(+), 33 deletions(-)
diff --git a/tests/test_client.py b/tests/test_client.py
index bab6b172..d399aae6 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -23,9 +23,7 @@
from maisa import Maisa, AsyncMaisa, APIResponseValidationError
from maisa._types import Omit
-from maisa._utils import maybe_transform
from maisa._models import BaseModel, FinalRequestOptions
-from maisa._constants import RAW_RESPONSE_HEADER
from maisa._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError
from maisa._base_client import (
DEFAULT_TIMEOUT,
@@ -35,7 +33,6 @@
DefaultAsyncHttpxClient,
make_request_options,
)
-from maisa.types.capability_summarize_params import CapabilitySummarizeParams
from .utils import update_env
@@ -703,32 +700,21 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str
@mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
+ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Maisa) -> None:
respx_mock.post("/v1/capabilities/summarize").mock(side_effect=httpx.TimeoutException("Test timeout error"))
with pytest.raises(APITimeoutError):
- self.client.post(
- "/v1/capabilities/summarize",
- body=cast(object, maybe_transform(dict(text="Example long text..."), CapabilitySummarizeParams)),
- cast_to=httpx.Response,
- options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
- )
+ client.capabilities.with_streaming_response.summarize(text="Example long text...").__enter__()
assert _get_open_connections(self.client) == 0
@mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
+ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Maisa) -> None:
respx_mock.post("/v1/capabilities/summarize").mock(return_value=httpx.Response(500))
with pytest.raises(APIStatusError):
- self.client.post(
- "/v1/capabilities/summarize",
- body=cast(object, maybe_transform(dict(text="Example long text..."), CapabilitySummarizeParams)),
- cast_to=httpx.Response,
- options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
- )
-
+ client.capabilities.with_streaming_response.summarize(text="Example long text...").__enter__()
assert _get_open_connections(self.client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@@ -1518,32 +1504,21 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte
@mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
+ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncMaisa) -> None:
respx_mock.post("/v1/capabilities/summarize").mock(side_effect=httpx.TimeoutException("Test timeout error"))
with pytest.raises(APITimeoutError):
- await self.client.post(
- "/v1/capabilities/summarize",
- body=cast(object, maybe_transform(dict(text="Example long text..."), CapabilitySummarizeParams)),
- cast_to=httpx.Response,
- options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
- )
+ await async_client.capabilities.with_streaming_response.summarize(text="Example long text...").__aenter__()
assert _get_open_connections(self.client) == 0
@mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
+ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncMaisa) -> None:
respx_mock.post("/v1/capabilities/summarize").mock(return_value=httpx.Response(500))
with pytest.raises(APIStatusError):
- await self.client.post(
- "/v1/capabilities/summarize",
- body=cast(object, maybe_transform(dict(text="Example long text..."), CapabilitySummarizeParams)),
- cast_to=httpx.Response,
- options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
- )
-
+ await async_client.capabilities.with_streaming_response.summarize(text="Example long text...").__aenter__()
assert _get_open_connections(self.client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
From 52ab33896a8fd2166bf0510766038bd69a67b2a0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 19 Jun 2025 02:56:48 +0000
Subject: [PATCH 103/200] docs(client): fix httpx.Timeout documentation
reference
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index a8360311..d07313fe 100644
--- a/README.md
+++ b/README.md
@@ -164,7 +164,7 @@ client.with_options(max_retries=5).capabilities.summarize(
### Timeouts
By default requests time out after 1 minute. You can configure this with a `timeout` option,
-which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object:
+which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object:
```python
from maisa import Maisa
From 393e393190cdf75a676f6d786837fe25a3e1978e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 21 Jun 2025 04:16:54 +0000
Subject: [PATCH 104/200] feat(client): add support for aiohttp
---
README.md | 34 +++++++++++++++
pyproject.toml | 2 +
requirements-dev.lock | 27 ++++++++++++
requirements.lock | 27 ++++++++++++
src/maisa/__init__.py | 3 +-
src/maisa/_base_client.py | 22 ++++++++++
.../api_resources/capabilities/test_media.py | 4 +-
.../file_interpreter/test_from_audio.py | 4 +-
.../file_interpreter/test_from_docx.py | 4 +-
.../file_interpreter/test_from_html.py | 4 +-
.../file_interpreter/test_from_image.py | 4 +-
.../file_interpreter/test_from_pdf.py | 4 +-
tests/api_resources/models/test_embeddings.py | 4 +-
tests/api_resources/test_capabilities.py | 4 +-
tests/api_resources/test_kpu.py | 4 +-
tests/conftest.py | 43 ++++++++++++++++---
16 files changed, 178 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
index d07313fe..23ee5e85 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,40 @@ asyncio.run(main())
Functionality between the synchronous and asynchronous clients is otherwise identical.
+### With aiohttp
+
+By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend.
+
+You can enable this by installing `aiohttp`:
+
+```sh
+# install from PyPI
+pip install --pre maisa[aiohttp]
+```
+
+Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
+
+```python
+import os
+import asyncio
+from maisa import DefaultAioHttpClient
+from maisa import AsyncMaisa
+
+
+async def main() -> None:
+ async with AsyncMaisa(
+ api_key=os.environ.get("MAISA_API_KEY"), # This is the default and can be omitted
+ http_client=DefaultAioHttpClient(),
+ ) as client:
+ text_summary = await client.capabilities.summarize(
+ text="Example long text...",
+ )
+ print(text_summary.summary)
+
+
+asyncio.run(main())
+```
+
## Using types
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:
diff --git a/pyproject.toml b/pyproject.toml
index e3db0410..ca817c8f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,6 +37,8 @@ classifiers = [
Homepage = "https://github.com/maisaai/python-sdk"
Repository = "https://github.com/maisaai/python-sdk"
+[project.optional-dependencies]
+aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"]
[tool.rye]
managed = true
diff --git a/requirements-dev.lock b/requirements-dev.lock
index ad16a7f8..03064f0a 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -10,6 +10,13 @@
# universal: false
-e file:.
+aiohappyeyeballs==2.6.1
+ # via aiohttp
+aiohttp==3.12.8
+ # via httpx-aiohttp
+ # via maisa
+aiosignal==1.3.2
+ # via aiohttp
annotated-types==0.6.0
# via pydantic
anyio==4.4.0
@@ -17,6 +24,10 @@ anyio==4.4.0
# via maisa
argcomplete==3.1.2
# via nox
+async-timeout==5.0.1
+ # via aiohttp
+attrs==25.3.0
+ # via aiohttp
certifi==2023.7.22
# via httpcore
# via httpx
@@ -34,16 +45,23 @@ execnet==2.1.1
# via pytest-xdist
filelock==3.12.4
# via virtualenv
+frozenlist==1.6.2
+ # via aiohttp
+ # via aiosignal
h11==0.14.0
# via httpcore
httpcore==1.0.2
# via httpx
httpx==0.28.1
+ # via httpx-aiohttp
# via maisa
# via respx
+httpx-aiohttp==0.1.6
+ # via maisa
idna==3.4
# via anyio
# via httpx
+ # via yarl
importlib-metadata==7.0.0
iniconfig==2.0.0
# via pytest
@@ -51,6 +69,9 @@ markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
+multidict==6.4.4
+ # via aiohttp
+ # via yarl
mypy==1.14.1
mypy-extensions==1.0.0
# via mypy
@@ -65,6 +86,9 @@ platformdirs==3.11.0
# via virtualenv
pluggy==1.5.0
# via pytest
+propcache==0.3.1
+ # via aiohttp
+ # via yarl
pydantic==2.10.3
# via maisa
pydantic-core==2.27.1
@@ -98,11 +122,14 @@ tomli==2.0.2
typing-extensions==4.12.2
# via anyio
# via maisa
+ # via multidict
# via mypy
# via pydantic
# via pydantic-core
# via pyright
virtualenv==20.24.5
# via nox
+yarl==1.20.0
+ # via aiohttp
zipp==3.17.0
# via importlib-metadata
diff --git a/requirements.lock b/requirements.lock
index 2d3958f0..08759f06 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -10,11 +10,22 @@
# universal: false
-e file:.
+aiohappyeyeballs==2.6.1
+ # via aiohttp
+aiohttp==3.12.8
+ # via httpx-aiohttp
+ # via maisa
+aiosignal==1.3.2
+ # via aiohttp
annotated-types==0.6.0
# via pydantic
anyio==4.4.0
# via httpx
# via maisa
+async-timeout==5.0.1
+ # via aiohttp
+attrs==25.3.0
+ # via aiohttp
certifi==2023.7.22
# via httpcore
# via httpx
@@ -22,15 +33,28 @@ distro==1.8.0
# via maisa
exceptiongroup==1.2.2
# via anyio
+frozenlist==1.6.2
+ # via aiohttp
+ # via aiosignal
h11==0.14.0
# via httpcore
httpcore==1.0.2
# via httpx
httpx==0.28.1
+ # via httpx-aiohttp
+ # via maisa
+httpx-aiohttp==0.1.6
# via maisa
idna==3.4
# via anyio
# via httpx
+ # via yarl
+multidict==6.4.4
+ # via aiohttp
+ # via yarl
+propcache==0.3.1
+ # via aiohttp
+ # via yarl
pydantic==2.10.3
# via maisa
pydantic-core==2.27.1
@@ -41,5 +65,8 @@ sniffio==1.3.0
typing-extensions==4.12.2
# via anyio
# via maisa
+ # via multidict
# via pydantic
# via pydantic-core
+yarl==1.20.0
+ # via aiohttp
diff --git a/src/maisa/__init__.py b/src/maisa/__init__.py
index 430b9c4a..58d2bf2b 100644
--- a/src/maisa/__init__.py
+++ b/src/maisa/__init__.py
@@ -26,7 +26,7 @@
UnprocessableEntityError,
APIResponseValidationError,
)
-from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient
+from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient
from ._utils._logs import setup_logging as _setup_logging
__all__ = [
@@ -68,6 +68,7 @@
"DEFAULT_CONNECTION_LIMITS",
"DefaultHttpxClient",
"DefaultAsyncHttpxClient",
+ "DefaultAioHttpClient",
]
if not _t.TYPE_CHECKING:
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index 9bc17acb..70de3e4e 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -1289,6 +1289,24 @@ def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
+try:
+ import httpx_aiohttp
+except ImportError:
+
+ class _DefaultAioHttpClient(httpx.AsyncClient):
+ def __init__(self, **_kwargs: Any) -> None:
+ raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra")
+else:
+
+ class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore
+ def __init__(self, **kwargs: Any) -> None:
+ kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
+ kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS)
+ kwargs.setdefault("follow_redirects", True)
+
+ super().__init__(**kwargs)
+
+
if TYPE_CHECKING:
DefaultAsyncHttpxClient = httpx.AsyncClient
"""An alias to `httpx.AsyncClient` that provides the same defaults that this SDK
@@ -1297,8 +1315,12 @@ def __init__(self, **kwargs: Any) -> None:
This is useful because overriding the `http_client` with your own instance of
`httpx.AsyncClient` will result in httpx's defaults being used, not ours.
"""
+
+ DefaultAioHttpClient = httpx.AsyncClient
+ """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`."""
else:
DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient
+ DefaultAioHttpClient = _DefaultAioHttpClient
class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient):
diff --git a/tests/api_resources/capabilities/test_media.py b/tests/api_resources/capabilities/test_media.py
index 65429e03..e160e63f 100644
--- a/tests/api_resources/capabilities/test_media.py
+++ b/tests/api_resources/capabilities/test_media.py
@@ -168,7 +168,9 @@ def test_streaming_response_summarize(self, client: Maisa) -> None:
class TestAsyncMedia:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_compare(self, async_client: AsyncMaisa) -> None:
diff --git a/tests/api_resources/file_interpreter/test_from_audio.py b/tests/api_resources/file_interpreter/test_from_audio.py
index 9bbcd1db..9f010b48 100644
--- a/tests/api_resources/file_interpreter/test_from_audio.py
+++ b/tests/api_resources/file_interpreter/test_from_audio.py
@@ -49,7 +49,9 @@ def test_streaming_response_create(self, client: Maisa) -> None:
class TestAsyncFromAudio:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncMaisa) -> None:
diff --git a/tests/api_resources/file_interpreter/test_from_docx.py b/tests/api_resources/file_interpreter/test_from_docx.py
index cbe686ef..dd7dd534 100644
--- a/tests/api_resources/file_interpreter/test_from_docx.py
+++ b/tests/api_resources/file_interpreter/test_from_docx.py
@@ -49,7 +49,9 @@ def test_streaming_response_create(self, client: Maisa) -> None:
class TestAsyncFromDocx:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncMaisa) -> None:
diff --git a/tests/api_resources/file_interpreter/test_from_html.py b/tests/api_resources/file_interpreter/test_from_html.py
index a08ea59b..5fed8217 100644
--- a/tests/api_resources/file_interpreter/test_from_html.py
+++ b/tests/api_resources/file_interpreter/test_from_html.py
@@ -49,7 +49,9 @@ def test_streaming_response_create(self, client: Maisa) -> None:
class TestAsyncFromHTML:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncMaisa) -> None:
diff --git a/tests/api_resources/file_interpreter/test_from_image.py b/tests/api_resources/file_interpreter/test_from_image.py
index 9d81ce0d..4cb88359 100644
--- a/tests/api_resources/file_interpreter/test_from_image.py
+++ b/tests/api_resources/file_interpreter/test_from_image.py
@@ -49,7 +49,9 @@ def test_streaming_response_create(self, client: Maisa) -> None:
class TestAsyncFromImage:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncMaisa) -> None:
diff --git a/tests/api_resources/file_interpreter/test_from_pdf.py b/tests/api_resources/file_interpreter/test_from_pdf.py
index 2aa4b8d0..bd216b5d 100644
--- a/tests/api_resources/file_interpreter/test_from_pdf.py
+++ b/tests/api_resources/file_interpreter/test_from_pdf.py
@@ -57,7 +57,9 @@ def test_streaming_response_create(self, client: Maisa) -> None:
class TestAsyncFromPdf:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncMaisa) -> None:
diff --git a/tests/api_resources/models/test_embeddings.py b/tests/api_resources/models/test_embeddings.py
index ab820d70..110daa4b 100644
--- a/tests/api_resources/models/test_embeddings.py
+++ b/tests/api_resources/models/test_embeddings.py
@@ -50,7 +50,9 @@ def test_streaming_response_create(self, client: Maisa) -> None:
class TestAsyncEmbeddings:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncMaisa) -> None:
diff --git a/tests/api_resources/test_capabilities.py b/tests/api_resources/test_capabilities.py
index 913e52c8..d84c5e14 100644
--- a/tests/api_resources/test_capabilities.py
+++ b/tests/api_resources/test_capabilities.py
@@ -192,7 +192,9 @@ def test_streaming_response_summarize(self, client: Maisa) -> None:
class TestAsyncCapabilities:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_compare(self, async_client: AsyncMaisa) -> None:
diff --git a/tests/api_resources/test_kpu.py b/tests/api_resources/test_kpu.py
index c5c5115d..f38a52e5 100644
--- a/tests/api_resources/test_kpu.py
+++ b/tests/api_resources/test_kpu.py
@@ -61,7 +61,9 @@ def test_streaming_response_run(self, client: Maisa) -> None:
class TestAsyncKpu:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_run(self, async_client: AsyncMaisa) -> None:
diff --git a/tests/conftest.py b/tests/conftest.py
index 1789d2ef..67cf5206 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -6,10 +6,12 @@
import logging
from typing import TYPE_CHECKING, Iterator, AsyncIterator
+import httpx
import pytest
from pytest_asyncio import is_async_test
-from maisa import Maisa, AsyncMaisa
+from maisa import Maisa, AsyncMaisa, DefaultAioHttpClient
+from maisa._utils import is_dict
if TYPE_CHECKING:
from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage]
@@ -27,6 +29,19 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None:
for async_test in pytest_asyncio_tests:
async_test.add_marker(session_scope_marker, append=False)
+ # We skip tests that use both the aiohttp client and respx_mock as respx_mock
+ # doesn't support custom transports.
+ for item in items:
+ if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames:
+ continue
+
+ if not hasattr(item, "callspec"):
+ continue
+
+ async_client_param = item.callspec.params.get("async_client")
+ if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp":
+ item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock"))
+
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -45,9 +60,25 @@ def client(request: FixtureRequest) -> Iterator[Maisa]:
@pytest.fixture(scope="session")
async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncMaisa]:
- strict = getattr(request, "param", True)
- if not isinstance(strict, bool):
- raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}")
-
- async with AsyncMaisa(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client:
+ param = getattr(request, "param", True)
+
+ # defaults
+ strict = True
+ http_client: None | httpx.AsyncClient = None
+
+ if isinstance(param, bool):
+ strict = param
+ elif is_dict(param):
+ strict = param.get("strict", True)
+ assert isinstance(strict, bool)
+
+ http_client_type = param.get("http_client", "httpx")
+ if http_client_type == "aiohttp":
+ http_client = DefaultAioHttpClient()
+ else:
+ raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict")
+
+ async with AsyncMaisa(
+ base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client
+ ) as client:
yield client
From 4b5162257aea9f1875b7a01213446cb56eeb17ca Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 24 Jun 2025 04:24:17 +0000
Subject: [PATCH 105/200] chore(tests): skip some failing tests on the latest
python versions
---
tests/test_client.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tests/test_client.py b/tests/test_client.py
index d399aae6..a7681eb2 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -191,6 +191,7 @@ def test_copy_signature(self) -> None:
copy_param = copy_signature.parameters.get(name)
assert copy_param is not None, f"copy() signature is missing the {name} param"
+ @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
def test_copy_build_request(self) -> None:
options = FinalRequestOptions(method="get", url="/foo")
@@ -981,6 +982,7 @@ def test_copy_signature(self) -> None:
copy_param = copy_signature.parameters.get(name)
assert copy_param is not None, f"copy() signature is missing the {name} param"
+ @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
def test_copy_build_request(self) -> None:
options = FinalRequestOptions(method="get", url="/foo")
From a22ef2e16a3862b31350e11f330a05dcee2fe723 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 27 Jun 2025 02:40:40 +0000
Subject: [PATCH 106/200] =?UTF-8?q?fix(ci):=20release-doctor=20=E2=80=94?=
=?UTF-8?q?=20report=20correct=20token=20name?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
bin/check-release-environment | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bin/check-release-environment b/bin/check-release-environment
index 5af9c3e6..b845b0f4 100644
--- a/bin/check-release-environment
+++ b/bin/check-release-environment
@@ -3,7 +3,7 @@
errors=()
if [ -z "${PYPI_TOKEN}" ]; then
- errors+=("The MAISA_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.")
+ errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.")
fi
lenErrors=${#errors[@]}
From cb8d889f01ce3e4b516b45218bcfeb556970992a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 28 Jun 2025 08:50:20 +0000
Subject: [PATCH 107/200] chore(ci): only run for pushes and fork pull requests
---
.github/workflows/ci.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1643341c..ba0f2ce1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,6 +17,7 @@ jobs:
timeout-minutes: 10
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/maisa-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
@@ -42,6 +43,7 @@ jobs:
contents: read
id-token: write
runs-on: depot-ubuntu-24.04
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
@@ -62,6 +64,7 @@ jobs:
timeout-minutes: 10
name: test
runs-on: ${{ github.repository == 'stainless-sdks/maisa-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
From df34ac81de4f6cdbf96190ebe4b8e03149551d2f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 30 Jun 2025 02:35:07 +0000
Subject: [PATCH 108/200] fix(ci): correct conditional
---
.github/workflows/ci.yml | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ba0f2ce1..5af76953 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -36,14 +36,13 @@ jobs:
run: ./scripts/lint
upload:
- if: github.repository == 'stainless-sdks/maisa-python'
+ if: github.repository == 'stainless-sdks/maisa-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork)
timeout-minutes: 10
name: upload
permissions:
contents: read
id-token: write
runs-on: depot-ubuntu-24.04
- if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
From d6ecb98f9571ba8513deb7cb640f2ca60f473b32 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 2 Jul 2025 05:10:46 +0000
Subject: [PATCH 109/200] chore(ci): change upload type
---
.github/workflows/ci.yml | 18 ++++++++++++++++--
scripts/utils/upload-artifact.sh | 12 +++++++-----
2 files changed, 23 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5af76953..711b716b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -35,10 +35,10 @@ jobs:
- name: Run lints
run: ./scripts/lint
- upload:
+ build:
if: github.repository == 'stainless-sdks/maisa-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork)
timeout-minutes: 10
- name: upload
+ name: build
permissions:
contents: read
id-token: write
@@ -46,6 +46,20 @@ jobs:
steps:
- uses: actions/checkout@v4
+ - name: Install Rye
+ run: |
+ curl -sSf https://rye.astral.sh/get | bash
+ echo "$HOME/.rye/shims" >> $GITHUB_PATH
+ env:
+ RYE_VERSION: '0.44.0'
+ RYE_INSTALL_OPTION: '--yes'
+
+ - name: Install dependencies
+ run: rye sync --all-features
+
+ - name: Run build
+ run: rye build
+
- name: Get GitHub OIDC Token
id: github-oidc
uses: actions/github-script@v6
diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh
index 9af38144..a7725849 100755
--- a/scripts/utils/upload-artifact.sh
+++ b/scripts/utils/upload-artifact.sh
@@ -1,7 +1,9 @@
#!/usr/bin/env bash
set -exuo pipefail
-RESPONSE=$(curl -X POST "$URL" \
+FILENAME=$(basename dist/*.whl)
+
+RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \
-H "Authorization: Bearer $AUTH" \
-H "Content-Type: application/json")
@@ -12,13 +14,13 @@ if [[ "$SIGNED_URL" == "null" ]]; then
exit 1
fi
-UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \
- -H "Content-Type: application/gzip" \
- --data-binary @- "$SIGNED_URL" 2>&1)
+UPLOAD_RESPONSE=$(curl -v -X PUT \
+ -H "Content-Type: binary/octet-stream" \
+ --data-binary "@dist/$FILENAME" "$SIGNED_URL" 2>&1)
if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then
echo -e "\033[32mUploaded build to Stainless storage.\033[0m"
- echo -e "\033[32mInstallation: pip install --pre 'https://pkg.stainless.com/s/maisa-python/$SHA'\033[0m"
+ echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/maisa-python/$SHA/$FILENAME'\033[0m"
else
echo -e "\033[31mFailed to upload artifact.\033[0m"
exit 1
From d0979d5cfa0a424672e519264469b35be2bcdd0e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 8 Jul 2025 02:19:51 +0000
Subject: [PATCH 110/200] chore(internal): codegen related update
---
requirements-dev.lock | 2 +-
requirements.lock | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 03064f0a..59233b5e 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -56,7 +56,7 @@ httpx==0.28.1
# via httpx-aiohttp
# via maisa
# via respx
-httpx-aiohttp==0.1.6
+httpx-aiohttp==0.1.8
# via maisa
idna==3.4
# via anyio
diff --git a/requirements.lock b/requirements.lock
index 08759f06..260936ff 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -43,7 +43,7 @@ httpcore==1.0.2
httpx==0.28.1
# via httpx-aiohttp
# via maisa
-httpx-aiohttp==0.1.6
+httpx-aiohttp==0.1.8
# via maisa
idna==3.4
# via anyio
From 57ff8290fbf16fb995158780c5a01f9d30b27ce5 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 9 Jul 2025 02:50:17 +0000
Subject: [PATCH 111/200] chore(internal): bump pinned h11 dep
---
requirements-dev.lock | 4 ++--
requirements.lock | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 59233b5e..89083658 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -48,9 +48,9 @@ filelock==3.12.4
frozenlist==1.6.2
# via aiohttp
# via aiosignal
-h11==0.14.0
+h11==0.16.0
# via httpcore
-httpcore==1.0.2
+httpcore==1.0.9
# via httpx
httpx==0.28.1
# via httpx-aiohttp
diff --git a/requirements.lock b/requirements.lock
index 260936ff..ab65a0e9 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -36,9 +36,9 @@ exceptiongroup==1.2.2
frozenlist==1.6.2
# via aiohttp
# via aiosignal
-h11==0.14.0
+h11==0.16.0
# via httpcore
-httpcore==1.0.2
+httpcore==1.0.9
# via httpx
httpx==0.28.1
# via httpx-aiohttp
From 70d59fd2d01ab63d402305473eacd669c37b3406 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 9 Jul 2025 03:13:40 +0000
Subject: [PATCH 112/200] chore(package): mark python 3.13 as supported
---
pyproject.toml | 1 +
1 file changed, 1 insertion(+)
diff --git a/pyproject.toml b/pyproject.toml
index ca817c8f..32b39f37 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,6 +24,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"Operating System :: OS Independent",
"Operating System :: POSIX",
"Operating System :: MacOS",
From 4a0795e78702d1851afacb07c8e853a71821f6e2 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 10 Jul 2025 02:51:25 +0000
Subject: [PATCH 113/200] fix(parsing): correctly handle nested discriminated
unions
---
src/maisa/_models.py | 13 ++++++++-----
tests/test_models.py | 45 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 53 insertions(+), 5 deletions(-)
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index 4f214980..528d5680 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -2,9 +2,10 @@
import os
import inspect
-from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast
+from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
from datetime import date, datetime
from typing_extensions import (
+ List,
Unpack,
Literal,
ClassVar,
@@ -366,7 +367,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object:
if type_ is None:
raise RuntimeError(f"Unexpected field type is None for {key}")
- return construct_type(value=value, type_=type_)
+ return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None))
def is_basemodel(type_: type) -> bool:
@@ -420,7 +421,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T:
return cast(_T, construct_type(value=value, type_=type_))
-def construct_type(*, value: object, type_: object) -> object:
+def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object:
"""Loose coercion to the expected type with construction of nested values.
If the given value does not match the expected type then it is returned as-is.
@@ -438,8 +439,10 @@ def construct_type(*, value: object, type_: object) -> object:
type_ = type_.__value__ # type: ignore[unreachable]
# unwrap `Annotated[T, ...]` -> `T`
- if is_annotated_type(type_):
- meta: tuple[Any, ...] = get_args(type_)[1:]
+ if metadata is not None:
+ meta: tuple[Any, ...] = tuple(metadata)
+ elif is_annotated_type(type_):
+ meta = get_args(type_)[1:]
type_ = extract_type_arg(type_, 0)
else:
meta = tuple()
diff --git a/tests/test_models.py b/tests/test_models.py
index d52ab260..c7f61f5a 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -889,3 +889,48 @@ class ModelB(BaseModel):
)
assert isinstance(m, ModelB)
+
+
+def test_nested_discriminated_union() -> None:
+ class InnerType1(BaseModel):
+ type: Literal["type_1"]
+
+ class InnerModel(BaseModel):
+ inner_value: str
+
+ class InnerType2(BaseModel):
+ type: Literal["type_2"]
+ some_inner_model: InnerModel
+
+ class Type1(BaseModel):
+ base_type: Literal["base_type_1"]
+ value: Annotated[
+ Union[
+ InnerType1,
+ InnerType2,
+ ],
+ PropertyInfo(discriminator="type"),
+ ]
+
+ class Type2(BaseModel):
+ base_type: Literal["base_type_2"]
+
+ T = Annotated[
+ Union[
+ Type1,
+ Type2,
+ ],
+ PropertyInfo(discriminator="base_type"),
+ ]
+
+ model = construct_type(
+ type_=T,
+ value={
+ "base_type": "base_type_1",
+ "value": {
+ "type": "type_2",
+ },
+ },
+ )
+ assert isinstance(model, Type1)
+ assert isinstance(model.value, InnerType2)
From e05102abfe4c503445afeecc74efa09e58cef23c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 11 Jul 2025 03:10:37 +0000
Subject: [PATCH 114/200] chore(readme): fix version rendering on pypi
---
README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 23ee5e85..60eaa50a 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
# Maisa Python API library
-[![PyPI version]()](https://pypi.org/project/maisa/)
+
+[)](https://pypi.org/project/maisa/)
The Maisa Python library provides convenient access to the Maisa REST API from any Python 3.8+
application. The library includes type definitions for all request params and response fields,
From 791e4fcef7112f4089aa115deca5c16a938255b7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 12 Jul 2025 02:16:52 +0000
Subject: [PATCH 115/200] fix(client): don't send Content-Type header on GET
requests
---
pyproject.toml | 2 +-
src/maisa/_base_client.py | 11 +++++++++--
tests/test_client.py | 4 ++--
3 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 32b39f37..40d44c75 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,7 +39,7 @@ Homepage = "https://github.com/maisaai/python-sdk"
Repository = "https://github.com/maisaai/python-sdk"
[project.optional-dependencies]
-aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"]
+aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"]
[tool.rye]
managed = true
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index 70de3e4e..ea96cf9a 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -529,6 +529,15 @@ def _build_request(
# work around https://github.com/encode/httpx/discussions/2880
kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")}
+ is_body_allowed = options.method.lower() != "get"
+
+ if is_body_allowed:
+ kwargs["json"] = json_data if is_given(json_data) else None
+ kwargs["files"] = files
+ else:
+ headers.pop("Content-Type", None)
+ kwargs.pop("data", None)
+
# TODO: report this error to httpx
return self._client.build_request( # pyright: ignore[reportUnknownMemberType]
headers=headers,
@@ -540,8 +549,6 @@ def _build_request(
# so that passing a `TypedDict` doesn't cause an error.
# https://github.com/microsoft/pyright/issues/3526#event-6715453066
params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None,
- json=json_data if is_given(json_data) else None,
- files=files,
**kwargs,
)
diff --git a/tests/test_client.py b/tests/test_client.py
index a7681eb2..870afd1e 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -452,7 +452,7 @@ def test_request_extra_query(self) -> None:
def test_multipart_repeating_array(self, client: Maisa) -> None:
request = client._build_request(
FinalRequestOptions.construct(
- method="get",
+ method="post",
url="/foo",
headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"},
json_data={"array": ["foo", "bar"]},
@@ -1245,7 +1245,7 @@ def test_request_extra_query(self) -> None:
def test_multipart_repeating_array(self, async_client: AsyncMaisa) -> None:
request = async_client._build_request(
FinalRequestOptions.construct(
- method="get",
+ method="post",
url="/foo",
headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"},
json_data={"array": ["foo", "bar"]},
From ad83736f3015d86a20ebc71886a0e817d243f6f8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 15 Jul 2025 02:16:32 +0000
Subject: [PATCH 116/200] feat: clean up environment call outs
---
README.md | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 60eaa50a..abfefd34 100644
--- a/README.md
+++ b/README.md
@@ -83,7 +83,6 @@ pip install --pre maisa[aiohttp]
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
```python
-import os
import asyncio
from maisa import DefaultAioHttpClient
from maisa import AsyncMaisa
@@ -91,7 +90,7 @@ from maisa import AsyncMaisa
async def main() -> None:
async with AsyncMaisa(
- api_key=os.environ.get("MAISA_API_KEY"), # This is the default and can be omitted
+ api_key="My API Key",
http_client=DefaultAioHttpClient(),
) as client:
text_summary = await client.capabilities.summarize(
From 8babd3cc1a7a134467ec0dc009b7a1ba6cb99ad1 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 22 Jul 2025 02:20:46 +0000
Subject: [PATCH 117/200] fix(parsing): ignore empty metadata
---
src/maisa/_models.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index 528d5680..ffcbf67b 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -439,7 +439,7 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]
type_ = type_.__value__ # type: ignore[unreachable]
# unwrap `Annotated[T, ...]` -> `T`
- if metadata is not None:
+ if metadata is not None and len(metadata) > 0:
meta: tuple[Any, ...] = tuple(metadata)
elif is_annotated_type(type_):
meta = get_args(type_)[1:]
From da719a30f9c3425e2ab68efc80be15ceba4dfad6 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 23 Jul 2025 02:23:37 +0000
Subject: [PATCH 118/200] fix(parsing): parse extra field types
---
src/maisa/_models.py | 25 +++++++++++++++++++++++--
tests/test_models.py | 29 ++++++++++++++++++++++++++++-
2 files changed, 51 insertions(+), 3 deletions(-)
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index ffcbf67b..b8387ce9 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -208,14 +208,18 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride]
else:
fields_values[name] = field_get_default(field)
+ extra_field_type = _get_extra_fields_type(__cls)
+
_extra = {}
for key, value in values.items():
if key not in model_fields:
+ parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value
+
if PYDANTIC_V2:
- _extra[key] = value
+ _extra[key] = parsed
else:
_fields_set.add(key)
- fields_values[key] = value
+ fields_values[key] = parsed
object.__setattr__(m, "__dict__", fields_values)
@@ -370,6 +374,23 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object:
return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None))
+def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None:
+ if not PYDANTIC_V2:
+ # TODO
+ return None
+
+ schema = cls.__pydantic_core_schema__
+ if schema["type"] == "model":
+ fields = schema["schema"]
+ if fields["type"] == "model-fields":
+ extras = fields.get("extras_schema")
+ if extras and "cls" in extras:
+ # mypy can't narrow the type
+ return extras["cls"] # type: ignore[no-any-return]
+
+ return None
+
+
def is_basemodel(type_: type) -> bool:
"""Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`"""
if is_union(type_):
diff --git a/tests/test_models.py b/tests/test_models.py
index c7f61f5a..e9006164 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,5 +1,5 @@
import json
-from typing import Any, Dict, List, Union, Optional, cast
+from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast
from datetime import datetime, timezone
from typing_extensions import Literal, Annotated, TypeAliasType
@@ -934,3 +934,30 @@ class Type2(BaseModel):
)
assert isinstance(model, Type1)
assert isinstance(model.value, InnerType2)
+
+
+@pytest.mark.skipif(not PYDANTIC_V2, reason="this is only supported in pydantic v2 for now")
+def test_extra_properties() -> None:
+ class Item(BaseModel):
+ prop: int
+
+ class Model(BaseModel):
+ __pydantic_extra__: Dict[str, Item] = Field(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
+
+ other: str
+
+ if TYPE_CHECKING:
+
+ def __getattr__(self, attr: str) -> Item: ...
+
+ model = construct_type(
+ type_=Model,
+ value={
+ "a": {"prop": 1},
+ "other": "foo",
+ },
+ )
+ assert isinstance(model, Model)
+ assert model.a.prop == 1
+ assert isinstance(model.a, Item)
+ assert model.other == "foo"
From f9ac05aaaa7a6f0e50249200aab9a017332d2b57 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 25 Jul 2025 05:28:59 +0000
Subject: [PATCH 119/200] chore(project): add settings file for vscode
---
.gitignore | 1 -
.vscode/settings.json | 3 +++
2 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 .vscode/settings.json
diff --git a/.gitignore b/.gitignore
index 87797408..95ceb189 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
.prism.log
-.vscode
_dev
__pycache__
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..5b010307
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "python.analysis.importFormat": "relative",
+}
From 09e05f0a711d7852e36aa117a55f8fb8ca33a252 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 31 Jul 2025 07:16:00 +0000
Subject: [PATCH 120/200] feat(client): support file upload requests
---
src/maisa/_base_client.py | 5 ++++-
src/maisa/_files.py | 8 ++++----
2 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index ea96cf9a..858dd6e1 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -532,7 +532,10 @@ def _build_request(
is_body_allowed = options.method.lower() != "get"
if is_body_allowed:
- kwargs["json"] = json_data if is_given(json_data) else None
+ if isinstance(json_data, bytes):
+ kwargs["content"] = json_data
+ else:
+ kwargs["json"] = json_data if is_given(json_data) else None
kwargs["files"] = files
else:
headers.pop("Content-Type", None)
diff --git a/src/maisa/_files.py b/src/maisa/_files.py
index 4b635213..5bff5118 100644
--- a/src/maisa/_files.py
+++ b/src/maisa/_files.py
@@ -69,12 +69,12 @@ def _transform_file(file: FileTypes) -> HttpxFileTypes:
return file
if is_tuple_t(file):
- return (file[0], _read_file_content(file[1]), *file[2:])
+ return (file[0], read_file_content(file[1]), *file[2:])
raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
-def _read_file_content(file: FileContent) -> HttpxFileContent:
+def read_file_content(file: FileContent) -> HttpxFileContent:
if isinstance(file, os.PathLike):
return pathlib.Path(file).read_bytes()
return file
@@ -111,12 +111,12 @@ async def _async_transform_file(file: FileTypes) -> HttpxFileTypes:
return file
if is_tuple_t(file):
- return (file[0], await _async_read_file_content(file[1]), *file[2:])
+ return (file[0], await async_read_file_content(file[1]), *file[2:])
raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
-async def _async_read_file_content(file: FileContent) -> HttpxFileContent:
+async def async_read_file_content(file: FileContent) -> HttpxFileContent:
if isinstance(file, os.PathLike):
return await anyio.Path(file).read_bytes()
From e16c3d81dd8d05db7e17bbd0f2318da894caf835 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 6 Aug 2025 09:02:29 +0000
Subject: [PATCH 121/200] chore(internal): fix ruff target version
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 40d44c75..8397d6a8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -159,7 +159,7 @@ reportPrivateUsage = false
[tool.ruff]
line-length = 120
output-format = "grouped"
-target-version = "py37"
+target-version = "py38"
[tool.ruff.format]
docstring-code-format = true
From 0fd434f70bd220e5413429c31e532d50b5de9578 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 9 Aug 2025 05:16:30 +0000
Subject: [PATCH 122/200] chore: update @stainless-api/prism-cli to v5.15.0
---
scripts/mock | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/scripts/mock b/scripts/mock
index d2814ae6..0b28f6ea 100755
--- a/scripts/mock
+++ b/scripts/mock
@@ -21,7 +21,7 @@ echo "==> Starting mock server with URL ${URL}"
# Run prism mock on the given spec
if [ "$1" == "--daemon" ]; then
- npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" &> .prism.log &
+ npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log &
# Wait for server to come online
echo -n "Waiting for server"
@@ -37,5 +37,5 @@ if [ "$1" == "--daemon" ]; then
echo
else
- npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL"
+ npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL"
fi
From 8735ebcba8abd47d3bf01f45908015a737e8ee6b Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 9 Aug 2025 05:46:15 +0000
Subject: [PATCH 123/200] chore(internal): update comment in script
---
scripts/test | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/test b/scripts/test
index 2b878456..dbeda2d2 100755
--- a/scripts/test
+++ b/scripts/test
@@ -43,7 +43,7 @@ elif ! prism_is_running ; then
echo -e "To run the server, pass in the path or url of your OpenAPI"
echo -e "spec to the prism command:"
echo
- echo -e " \$ ${YELLOW}npm exec --package=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}"
+ echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}"
echo
exit 1
From 37deecf9e54363de62ea3a55c2b2313a6241c6b8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 22 Aug 2025 07:31:02 +0000
Subject: [PATCH 124/200] chore: update github action
---
.github/workflows/ci.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 711b716b..b77652e1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -36,7 +36,7 @@ jobs:
run: ./scripts/lint
build:
- if: github.repository == 'stainless-sdks/maisa-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork)
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
timeout-minutes: 10
name: build
permissions:
@@ -61,12 +61,14 @@ jobs:
run: rye build
- name: Get GitHub OIDC Token
+ if: github.repository == 'stainless-sdks/maisa-python'
id: github-oidc
uses: actions/github-script@v6
with:
script: core.setOutput('github_token', await core.getIDToken());
- name: Upload tarball
+ if: github.repository == 'stainless-sdks/maisa-python'
env:
URL: https://pkg.stainless.com/s
AUTH: ${{ steps.github-oidc.outputs.github_token }}
From 8615982869fb3c28b01783d53e90fa3d174359cb Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 26 Aug 2025 05:57:44 +0000
Subject: [PATCH 125/200] chore(internal): change ci workflow machines
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b77652e1..f7499ee9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -42,7 +42,7 @@ jobs:
permissions:
contents: read
id-token: write
- runs-on: depot-ubuntu-24.04
+ runs-on: ${{ github.repository == 'stainless-sdks/maisa-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v4
From 1bed5391d47d402c8a2e77685db12221538b2071 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 27 Aug 2025 09:02:06 +0000
Subject: [PATCH 126/200] fix: avoid newer type syntax
---
src/maisa/_models.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index b8387ce9..92f7c10b 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -304,7 +304,7 @@ def model_dump(
exclude_none=exclude_none,
)
- return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped
+ return cast("dict[str, Any]", json_safe(dumped)) if mode == "json" else dumped
@override
def model_dump_json(
From d5f61458a707cd450d1433febf3a9550dc103643 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 27 Aug 2025 09:05:57 +0000
Subject: [PATCH 127/200] chore(internal): update pyright exclude list
---
pyproject.toml | 1 +
1 file changed, 1 insertion(+)
diff --git a/pyproject.toml b/pyproject.toml
index 8397d6a8..331c50a0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -148,6 +148,7 @@ exclude = [
"_dev",
".venv",
".nox",
+ ".git",
]
reportImplicitOverride = true
From f0d287110342ec657ec4023387f51d203b2d5a48 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 30 Aug 2025 04:25:14 +0000
Subject: [PATCH 128/200] chore(internal): add Sequence related utils
---
src/maisa/_types.py | 36 +++++++++++++++++++++++++++++++++++-
src/maisa/_utils/__init__.py | 1 +
src/maisa/_utils/_typing.py | 5 +++++
tests/utils.py | 10 +++++++++-
4 files changed, 50 insertions(+), 2 deletions(-)
diff --git a/src/maisa/_types.py b/src/maisa/_types.py
index eb1f7534..81ec6b72 100644
--- a/src/maisa/_types.py
+++ b/src/maisa/_types.py
@@ -13,10 +13,21 @@
Mapping,
TypeVar,
Callable,
+ Iterator,
Optional,
Sequence,
)
-from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable
+from typing_extensions import (
+ Set,
+ Literal,
+ Protocol,
+ TypeAlias,
+ TypedDict,
+ SupportsIndex,
+ overload,
+ override,
+ runtime_checkable,
+)
import httpx
import pydantic
@@ -217,3 +228,26 @@ class _GenericAlias(Protocol):
class HttpxSendArgs(TypedDict, total=False):
auth: httpx.Auth
follow_redirects: bool
+
+
+_T_co = TypeVar("_T_co", covariant=True)
+
+
+if TYPE_CHECKING:
+ # This works because str.__contains__ does not accept object (either in typeshed or at runtime)
+ # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285
+ class SequenceNotStr(Protocol[_T_co]):
+ @overload
+ def __getitem__(self, index: SupportsIndex, /) -> _T_co: ...
+ @overload
+ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ...
+ def __contains__(self, value: object, /) -> bool: ...
+ def __len__(self) -> int: ...
+ def __iter__(self) -> Iterator[_T_co]: ...
+ def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ...
+ def count(self, value: Any, /) -> int: ...
+ def __reversed__(self) -> Iterator[_T_co]: ...
+else:
+ # just point this to a normal `Sequence` at runtime to avoid having to special case
+ # deserializing our custom sequence type
+ SequenceNotStr = Sequence
diff --git a/src/maisa/_utils/__init__.py b/src/maisa/_utils/__init__.py
index d4fda26f..ca547ce5 100644
--- a/src/maisa/_utils/__init__.py
+++ b/src/maisa/_utils/__init__.py
@@ -38,6 +38,7 @@
extract_type_arg as extract_type_arg,
is_iterable_type as is_iterable_type,
is_required_type as is_required_type,
+ is_sequence_type as is_sequence_type,
is_annotated_type as is_annotated_type,
is_type_alias_type as is_type_alias_type,
strip_annotated_type as strip_annotated_type,
diff --git a/src/maisa/_utils/_typing.py b/src/maisa/_utils/_typing.py
index 1bac9542..845cd6b2 100644
--- a/src/maisa/_utils/_typing.py
+++ b/src/maisa/_utils/_typing.py
@@ -26,6 +26,11 @@ def is_list_type(typ: type) -> bool:
return (get_origin(typ) or typ) == list
+def is_sequence_type(typ: type) -> bool:
+ origin = get_origin(typ) or typ
+ return origin == typing_extensions.Sequence or origin == typing.Sequence or origin == _c_abc.Sequence
+
+
def is_iterable_type(typ: type) -> bool:
"""If the given type is `typing.Iterable[T]`"""
origin = get_origin(typ) or typ
diff --git a/tests/utils.py b/tests/utils.py
index 4b9ed39c..c9edc070 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -4,7 +4,7 @@
import inspect
import traceback
import contextlib
-from typing import Any, TypeVar, Iterator, cast
+from typing import Any, TypeVar, Iterator, Sequence, cast
from datetime import date, datetime
from typing_extensions import Literal, get_args, get_origin, assert_type
@@ -15,6 +15,7 @@
is_list_type,
is_union_type,
extract_type_arg,
+ is_sequence_type,
is_annotated_type,
is_type_alias_type,
)
@@ -71,6 +72,13 @@ def assert_matches_type(
if is_list_type(type_):
return _assert_list_type(type_, value)
+ if is_sequence_type(type_):
+ assert isinstance(value, Sequence)
+ inner_type = get_args(type_)[0]
+ for entry in value: # type: ignore
+ assert_type(inner_type, entry) # type: ignore
+ return
+
if origin == str:
assert isinstance(value, str)
elif origin == int:
From ae478dfce2707ec1b9133c1d5c5d2cc418b2e1df Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Sep 2025 03:51:01 +0000
Subject: [PATCH 129/200] feat(types): replace List[str] with SequenceNotStr in
params
---
src/maisa/_utils/_transform.py | 6 ++++++
src/maisa/resources/kpu.py | 8 ++++----
src/maisa/resources/models/embeddings.py | 8 +++-----
src/maisa/types/kpu_run_params.py | 6 +++---
src/maisa/types/models/embedding_create_params.py | 5 +++--
5 files changed, 19 insertions(+), 14 deletions(-)
diff --git a/src/maisa/_utils/_transform.py b/src/maisa/_utils/_transform.py
index b0cc20a7..f0bcefd4 100644
--- a/src/maisa/_utils/_transform.py
+++ b/src/maisa/_utils/_transform.py
@@ -16,6 +16,7 @@
lru_cache,
is_mapping,
is_iterable,
+ is_sequence,
)
from .._files import is_base64_file_input
from ._typing import (
@@ -24,6 +25,7 @@
extract_type_arg,
is_iterable_type,
is_required_type,
+ is_sequence_type,
is_annotated_type,
strip_annotated_type,
)
@@ -184,6 +186,8 @@ def _transform_recursive(
(is_list_type(stripped_type) and is_list(data))
# Iterable[T]
or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str))
+ # Sequence[T]
+ or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str))
):
# dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually
# intended as an iterable, so we don't transform it.
@@ -346,6 +350,8 @@ async def _async_transform_recursive(
(is_list_type(stripped_type) and is_list(data))
# Iterable[T]
or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str))
+ # Sequence[T]
+ or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str))
):
# dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually
# intended as an iterable, so we don't transform it.
diff --git a/src/maisa/resources/kpu.py b/src/maisa/resources/kpu.py
index 006e4d11..e6970a12 100644
--- a/src/maisa/resources/kpu.py
+++ b/src/maisa/resources/kpu.py
@@ -2,13 +2,13 @@
from __future__ import annotations
-from typing import List, Mapping, Optional, cast
+from typing import Mapping, Optional, cast
from typing_extensions import Literal
import httpx
from ..types import kpu_run_params
-from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
+from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes, SequenceNotStr
from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
@@ -49,7 +49,7 @@ def run(
query: str,
explain_steps: bool | NotGiven = NOT_GIVEN,
retries: int | NotGiven = NOT_GIVEN,
- file: List[FileTypes] | NotGiven = NOT_GIVEN,
+ file: SequenceNotStr[FileTypes] | NotGiven = NOT_GIVEN,
reasoner_model: Optional[
Literal[
"gpt-4-turbo",
@@ -156,7 +156,7 @@ async def run(
query: str,
explain_steps: bool | NotGiven = NOT_GIVEN,
retries: int | NotGiven = NOT_GIVEN,
- file: List[FileTypes] | NotGiven = NOT_GIVEN,
+ file: SequenceNotStr[FileTypes] | NotGiven = NOT_GIVEN,
reasoner_model: Optional[
Literal[
"gpt-4-turbo",
diff --git a/src/maisa/resources/models/embeddings.py b/src/maisa/resources/models/embeddings.py
index 974cfa29..f56ea053 100644
--- a/src/maisa/resources/models/embeddings.py
+++ b/src/maisa/resources/models/embeddings.py
@@ -2,11 +2,9 @@
from __future__ import annotations
-from typing import List
-
import httpx
-from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
+from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr
from ..._utils import maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
@@ -46,7 +44,7 @@ def with_streaming_response(self) -> EmbeddingsResourceWithStreamingResponse:
def create(
self,
*,
- texts: List[str],
+ texts: SequenceNotStr[str],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -101,7 +99,7 @@ def with_streaming_response(self) -> AsyncEmbeddingsResourceWithStreamingRespons
async def create(
self,
*,
- texts: List[str],
+ texts: SequenceNotStr[str],
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
diff --git a/src/maisa/types/kpu_run_params.py b/src/maisa/types/kpu_run_params.py
index a1d36388..6ba61dcd 100644
--- a/src/maisa/types/kpu_run_params.py
+++ b/src/maisa/types/kpu_run_params.py
@@ -2,10 +2,10 @@
from __future__ import annotations
-from typing import List, Optional
+from typing import Optional
from typing_extensions import Literal, Required, TypedDict
-from .._types import FileTypes
+from .._types import FileTypes, SequenceNotStr
__all__ = ["KpuRunParams"]
@@ -28,7 +28,7 @@ class KpuRunParams(TypedDict, total=False):
intent. This feature is experimental.
"""
- file: List[FileTypes]
+ file: SequenceNotStr[FileTypes]
"""Files to be used in the KPU execution. Files can be of any type."""
reasoner_model: Optional[
diff --git a/src/maisa/types/models/embedding_create_params.py b/src/maisa/types/models/embedding_create_params.py
index a09951d7..2db9f1b0 100644
--- a/src/maisa/types/models/embedding_create_params.py
+++ b/src/maisa/types/models/embedding_create_params.py
@@ -2,12 +2,13 @@
from __future__ import annotations
-from typing import List
from typing_extensions import Required, TypedDict
+from ..._types import SequenceNotStr
+
__all__ = ["EmbeddingCreateParams"]
class EmbeddingCreateParams(TypedDict, total=False):
- texts: Required[List[str]]
+ texts: Required[SequenceNotStr[str]]
"""A list of texts from which we will generate the embeddings."""
From 93d8fb9eb67510674a5c60a96cd9e9eca1ef0330 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 4 Sep 2025 03:53:59 +0000
Subject: [PATCH 130/200] feat: improve future compat with pydantic v3
---
src/maisa/_base_client.py | 6 +-
src/maisa/_compat.py | 96 ++++++++---------
src/maisa/_models.py | 80 +++++++-------
src/maisa/_utils/__init__.py | 10 +-
src/maisa/_utils/_compat.py | 45 ++++++++
src/maisa/_utils/_datetime_parse.py | 136 ++++++++++++++++++++++++
src/maisa/_utils/_transform.py | 6 +-
src/maisa/_utils/_typing.py | 2 +-
src/maisa/_utils/_utils.py | 1 -
tests/test_models.py | 48 ++++-----
tests/test_transform.py | 16 +--
tests/test_utils/test_datetime_parse.py | 110 +++++++++++++++++++
tests/utils.py | 8 +-
13 files changed, 432 insertions(+), 132 deletions(-)
create mode 100644 src/maisa/_utils/_compat.py
create mode 100644 src/maisa/_utils/_datetime_parse.py
create mode 100644 tests/test_utils/test_datetime_parse.py
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index 858dd6e1..4de0f3c0 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -59,7 +59,7 @@
ModelBuilderProtocol,
)
from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
-from ._compat import PYDANTIC_V2, model_copy, model_dump
+from ._compat import PYDANTIC_V1, model_copy, model_dump
from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type
from ._response import (
APIResponse,
@@ -232,7 +232,7 @@ def _set_private_attributes(
model: Type[_T],
options: FinalRequestOptions,
) -> None:
- if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None:
+ if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None:
self.__pydantic_private__ = {}
self._model = model
@@ -320,7 +320,7 @@ def _set_private_attributes(
client: AsyncAPIClient,
options: FinalRequestOptions,
) -> None:
- if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None:
+ if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None:
self.__pydantic_private__ = {}
self._model = model
diff --git a/src/maisa/_compat.py b/src/maisa/_compat.py
index 92d9ee61..bdef67f0 100644
--- a/src/maisa/_compat.py
+++ b/src/maisa/_compat.py
@@ -12,14 +12,13 @@
_T = TypeVar("_T")
_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel)
-# --------------- Pydantic v2 compatibility ---------------
+# --------------- Pydantic v2, v3 compatibility ---------------
# Pyright incorrectly reports some of our functions as overriding a method when they don't
# pyright: reportIncompatibleMethodOverride=false
-PYDANTIC_V2 = pydantic.VERSION.startswith("2.")
+PYDANTIC_V1 = pydantic.VERSION.startswith("1.")
-# v1 re-exports
if TYPE_CHECKING:
def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001
@@ -44,90 +43,92 @@ def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001
...
else:
- if PYDANTIC_V2:
- from pydantic.v1.typing import (
+ # v1 re-exports
+ if PYDANTIC_V1:
+ from pydantic.typing import (
get_args as get_args,
is_union as is_union,
get_origin as get_origin,
is_typeddict as is_typeddict,
is_literal_type as is_literal_type,
)
- from pydantic.v1.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime
+ from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime
else:
- from pydantic.typing import (
+ from ._utils import (
get_args as get_args,
is_union as is_union,
get_origin as get_origin,
+ parse_date as parse_date,
is_typeddict as is_typeddict,
+ parse_datetime as parse_datetime,
is_literal_type as is_literal_type,
)
- from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime
# refactored config
if TYPE_CHECKING:
from pydantic import ConfigDict as ConfigDict
else:
- if PYDANTIC_V2:
- from pydantic import ConfigDict
- else:
+ if PYDANTIC_V1:
# TODO: provide an error message here?
ConfigDict = None
+ else:
+ from pydantic import ConfigDict as ConfigDict
# renamed methods / properties
def parse_obj(model: type[_ModelT], value: object) -> _ModelT:
- if PYDANTIC_V2:
- return model.model_validate(value)
- else:
+ if PYDANTIC_V1:
return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
+ else:
+ return model.model_validate(value)
def field_is_required(field: FieldInfo) -> bool:
- if PYDANTIC_V2:
- return field.is_required()
- return field.required # type: ignore
+ if PYDANTIC_V1:
+ return field.required # type: ignore
+ return field.is_required()
def field_get_default(field: FieldInfo) -> Any:
value = field.get_default()
- if PYDANTIC_V2:
- from pydantic_core import PydanticUndefined
-
- if value == PydanticUndefined:
- return None
+ if PYDANTIC_V1:
return value
+ from pydantic_core import PydanticUndefined
+
+ if value == PydanticUndefined:
+ return None
return value
def field_outer_type(field: FieldInfo) -> Any:
- if PYDANTIC_V2:
- return field.annotation
- return field.outer_type_ # type: ignore
+ if PYDANTIC_V1:
+ return field.outer_type_ # type: ignore
+ return field.annotation
def get_model_config(model: type[pydantic.BaseModel]) -> Any:
- if PYDANTIC_V2:
- return model.model_config
- return model.__config__ # type: ignore
+ if PYDANTIC_V1:
+ return model.__config__ # type: ignore
+ return model.model_config
def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]:
- if PYDANTIC_V2:
- return model.model_fields
- return model.__fields__ # type: ignore
+ if PYDANTIC_V1:
+ return model.__fields__ # type: ignore
+ return model.model_fields
def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT:
- if PYDANTIC_V2:
- return model.model_copy(deep=deep)
- return model.copy(deep=deep) # type: ignore
+ if PYDANTIC_V1:
+ return model.copy(deep=deep) # type: ignore
+ return model.model_copy(deep=deep)
def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str:
- if PYDANTIC_V2:
- return model.model_dump_json(indent=indent)
- return model.json(indent=indent) # type: ignore
+ if PYDANTIC_V1:
+ return model.json(indent=indent) # type: ignore
+ return model.model_dump_json(indent=indent)
def model_dump(
@@ -139,14 +140,14 @@ def model_dump(
warnings: bool = True,
mode: Literal["json", "python"] = "python",
) -> dict[str, Any]:
- if PYDANTIC_V2 or hasattr(model, "model_dump"):
+ if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
return model.model_dump(
mode=mode,
exclude=exclude,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
# warnings are not supported in Pydantic v1
- warnings=warnings if PYDANTIC_V2 else True,
+ warnings=True if PYDANTIC_V1 else warnings,
)
return cast(
"dict[str, Any]",
@@ -159,9 +160,9 @@ def model_dump(
def model_parse(model: type[_ModelT], data: Any) -> _ModelT:
- if PYDANTIC_V2:
- return model.model_validate(data)
- return model.parse_obj(data) # pyright: ignore[reportDeprecated]
+ if PYDANTIC_V1:
+ return model.parse_obj(data) # pyright: ignore[reportDeprecated]
+ return model.model_validate(data)
# generic models
@@ -170,17 +171,16 @@ def model_parse(model: type[_ModelT], data: Any) -> _ModelT:
class GenericModel(pydantic.BaseModel): ...
else:
- if PYDANTIC_V2:
+ if PYDANTIC_V1:
+ import pydantic.generics
+
+ class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ...
+ else:
# there no longer needs to be a distinction in v2 but
# we still have to create our own subclass to avoid
# inconsistent MRO ordering errors
class GenericModel(pydantic.BaseModel): ...
- else:
- import pydantic.generics
-
- class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ...
-
# cached properties
if TYPE_CHECKING:
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index 92f7c10b..3a6017ef 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -50,7 +50,7 @@
strip_annotated_type,
)
from ._compat import (
- PYDANTIC_V2,
+ PYDANTIC_V1,
ConfigDict,
GenericModel as BaseGenericModel,
get_args,
@@ -81,11 +81,7 @@ class _ConfigProtocol(Protocol):
class BaseModel(pydantic.BaseModel):
- if PYDANTIC_V2:
- model_config: ClassVar[ConfigDict] = ConfigDict(
- extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true"))
- )
- else:
+ if PYDANTIC_V1:
@property
@override
@@ -95,6 +91,10 @@ def model_fields_set(self) -> set[str]:
class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
extra: Any = pydantic.Extra.allow # type: ignore
+ else:
+ model_config: ClassVar[ConfigDict] = ConfigDict(
+ extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true"))
+ )
def to_dict(
self,
@@ -215,25 +215,25 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride]
if key not in model_fields:
parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value
- if PYDANTIC_V2:
- _extra[key] = parsed
- else:
+ if PYDANTIC_V1:
_fields_set.add(key)
fields_values[key] = parsed
+ else:
+ _extra[key] = parsed
object.__setattr__(m, "__dict__", fields_values)
- if PYDANTIC_V2:
- # these properties are copied from Pydantic's `model_construct()` method
- object.__setattr__(m, "__pydantic_private__", None)
- object.__setattr__(m, "__pydantic_extra__", _extra)
- object.__setattr__(m, "__pydantic_fields_set__", _fields_set)
- else:
+ if PYDANTIC_V1:
# init_private_attributes() does not exist in v2
m._init_private_attributes() # type: ignore
# copied from Pydantic v1's `construct()` method
object.__setattr__(m, "__fields_set__", _fields_set)
+ else:
+ # these properties are copied from Pydantic's `model_construct()` method
+ object.__setattr__(m, "__pydantic_private__", None)
+ object.__setattr__(m, "__pydantic_extra__", _extra)
+ object.__setattr__(m, "__pydantic_fields_set__", _fields_set)
return m
@@ -243,7 +243,7 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride]
# although not in practice
model_construct = construct
- if not PYDANTIC_V2:
+ if PYDANTIC_V1:
# we define aliases for some of the new pydantic v2 methods so
# that we can just document these methods without having to specify
# a specific pydantic version as some users may not know which
@@ -363,10 +363,10 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object:
if value is None:
return field_get_default(field)
- if PYDANTIC_V2:
- type_ = field.annotation
- else:
+ if PYDANTIC_V1:
type_ = cast(type, field.outer_type_) # type: ignore
+ else:
+ type_ = field.annotation # type: ignore
if type_ is None:
raise RuntimeError(f"Unexpected field type is None for {key}")
@@ -375,7 +375,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object:
def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None:
- if not PYDANTIC_V2:
+ if PYDANTIC_V1:
# TODO
return None
@@ -628,30 +628,30 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
for variant in get_args(union):
variant = strip_annotated_type(variant)
if is_basemodel_type(variant):
- if PYDANTIC_V2:
- field = _extract_field_schema_pv2(variant, discriminator_field_name)
- if not field:
+ if PYDANTIC_V1:
+ field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
+ if not field_info:
continue
# Note: if one variant defines an alias then they all should
- discriminator_alias = field.get("serialization_alias")
-
- field_schema = field["schema"]
+ discriminator_alias = field_info.alias
- if field_schema["type"] == "literal":
- for entry in cast("LiteralSchema", field_schema)["expected"]:
+ if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation):
+ for entry in get_args(annotation):
if isinstance(entry, str):
mapping[entry] = variant
else:
- field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
- if not field_info:
+ field = _extract_field_schema_pv2(variant, discriminator_field_name)
+ if not field:
continue
# Note: if one variant defines an alias then they all should
- discriminator_alias = field_info.alias
+ discriminator_alias = field.get("serialization_alias")
- if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation):
- for entry in get_args(annotation):
+ field_schema = field["schema"]
+
+ if field_schema["type"] == "literal":
+ for entry in cast("LiteralSchema", field_schema)["expected"]:
if isinstance(entry, str):
mapping[entry] = variant
@@ -714,7 +714,7 @@ class GenericModel(BaseGenericModel, BaseModel):
pass
-if PYDANTIC_V2:
+if not PYDANTIC_V1:
from pydantic import TypeAdapter as _TypeAdapter
_CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter))
@@ -782,12 +782,12 @@ class FinalRequestOptions(pydantic.BaseModel):
json_data: Union[Body, None] = None
extra_json: Union[AnyMapping, None] = None
- if PYDANTIC_V2:
- model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True)
- else:
+ if PYDANTIC_V1:
class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
arbitrary_types_allowed: bool = True
+ else:
+ model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True)
def get_max_retries(self, max_retries: int) -> int:
if isinstance(self.max_retries, NotGiven):
@@ -820,9 +820,9 @@ def construct( # type: ignore
key: strip_not_given(value)
for key, value in values.items()
}
- if PYDANTIC_V2:
- return super().model_construct(_fields_set, **kwargs)
- return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated]
+ if PYDANTIC_V1:
+ return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated]
+ return super().model_construct(_fields_set, **kwargs)
if not TYPE_CHECKING:
# type checkers incorrectly complain about this assignment
diff --git a/src/maisa/_utils/__init__.py b/src/maisa/_utils/__init__.py
index ca547ce5..dc64e29a 100644
--- a/src/maisa/_utils/__init__.py
+++ b/src/maisa/_utils/__init__.py
@@ -10,7 +10,6 @@
lru_cache as lru_cache,
is_mapping as is_mapping,
is_tuple_t as is_tuple_t,
- parse_date as parse_date,
is_iterable as is_iterable,
is_sequence as is_sequence,
coerce_float as coerce_float,
@@ -23,7 +22,6 @@
coerce_boolean as coerce_boolean,
coerce_integer as coerce_integer,
file_from_path as file_from_path,
- parse_datetime as parse_datetime,
strip_not_given as strip_not_given,
deepcopy_minimal as deepcopy_minimal,
get_async_library as get_async_library,
@@ -32,6 +30,13 @@
maybe_coerce_boolean as maybe_coerce_boolean,
maybe_coerce_integer as maybe_coerce_integer,
)
+from ._compat import (
+ get_args as get_args,
+ is_union as is_union,
+ get_origin as get_origin,
+ is_typeddict as is_typeddict,
+ is_literal_type as is_literal_type,
+)
from ._typing import (
is_list_type as is_list_type,
is_union_type as is_union_type,
@@ -56,3 +61,4 @@
function_has_argument as function_has_argument,
assert_signatures_in_sync as assert_signatures_in_sync,
)
+from ._datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime
diff --git a/src/maisa/_utils/_compat.py b/src/maisa/_utils/_compat.py
new file mode 100644
index 00000000..dd703233
--- /dev/null
+++ b/src/maisa/_utils/_compat.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+import sys
+import typing_extensions
+from typing import Any, Type, Union, Literal, Optional
+from datetime import date, datetime
+from typing_extensions import get_args as _get_args, get_origin as _get_origin
+
+from .._types import StrBytesIntFloat
+from ._datetime_parse import parse_date as _parse_date, parse_datetime as _parse_datetime
+
+_LITERAL_TYPES = {Literal, typing_extensions.Literal}
+
+
+def get_args(tp: type[Any]) -> tuple[Any, ...]:
+ return _get_args(tp)
+
+
+def get_origin(tp: type[Any]) -> type[Any] | None:
+ return _get_origin(tp)
+
+
+def is_union(tp: Optional[Type[Any]]) -> bool:
+ if sys.version_info < (3, 10):
+ return tp is Union # type: ignore[comparison-overlap]
+ else:
+ import types
+
+ return tp is Union or tp is types.UnionType
+
+
+def is_typeddict(tp: Type[Any]) -> bool:
+ return typing_extensions.is_typeddict(tp)
+
+
+def is_literal_type(tp: Type[Any]) -> bool:
+ return get_origin(tp) in _LITERAL_TYPES
+
+
+def parse_date(value: Union[date, StrBytesIntFloat]) -> date:
+ return _parse_date(value)
+
+
+def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime:
+ return _parse_datetime(value)
diff --git a/src/maisa/_utils/_datetime_parse.py b/src/maisa/_utils/_datetime_parse.py
new file mode 100644
index 00000000..7cb9d9e6
--- /dev/null
+++ b/src/maisa/_utils/_datetime_parse.py
@@ -0,0 +1,136 @@
+"""
+This file contains code from https://github.com/pydantic/pydantic/blob/main/pydantic/v1/datetime_parse.py
+without the Pydantic v1 specific errors.
+"""
+
+from __future__ import annotations
+
+import re
+from typing import Dict, Union, Optional
+from datetime import date, datetime, timezone, timedelta
+
+from .._types import StrBytesIntFloat
+
+date_expr = r"(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})"
+time_expr = (
+ r"(?P\d{1,2}):(?P\d{1,2})"
+ r"(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?"
+ r"(?PZ|[+-]\d{2}(?::?\d{2})?)?$"
+)
+
+date_re = re.compile(f"{date_expr}$")
+datetime_re = re.compile(f"{date_expr}[T ]{time_expr}")
+
+
+EPOCH = datetime(1970, 1, 1)
+# if greater than this, the number is in ms, if less than or equal it's in seconds
+# (in seconds this is 11th October 2603, in ms it's 20th August 1970)
+MS_WATERSHED = int(2e10)
+# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9
+MAX_NUMBER = int(3e20)
+
+
+def _get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]:
+ if isinstance(value, (int, float)):
+ return value
+ try:
+ return float(value)
+ except ValueError:
+ return None
+ except TypeError:
+ raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from None
+
+
+def _from_unix_seconds(seconds: Union[int, float]) -> datetime:
+ if seconds > MAX_NUMBER:
+ return datetime.max
+ elif seconds < -MAX_NUMBER:
+ return datetime.min
+
+ while abs(seconds) > MS_WATERSHED:
+ seconds /= 1000
+ dt = EPOCH + timedelta(seconds=seconds)
+ return dt.replace(tzinfo=timezone.utc)
+
+
+def _parse_timezone(value: Optional[str]) -> Union[None, int, timezone]:
+ if value == "Z":
+ return timezone.utc
+ elif value is not None:
+ offset_mins = int(value[-2:]) if len(value) > 3 else 0
+ offset = 60 * int(value[1:3]) + offset_mins
+ if value[0] == "-":
+ offset = -offset
+ return timezone(timedelta(minutes=offset))
+ else:
+ return None
+
+
+def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime:
+ """
+ Parse a datetime/int/float/string and return a datetime.datetime.
+
+ This function supports time zone offsets. When the input contains one,
+ the output uses a timezone with a fixed offset from UTC.
+
+ Raise ValueError if the input is well formatted but not a valid datetime.
+ Raise ValueError if the input isn't well formatted.
+ """
+ if isinstance(value, datetime):
+ return value
+
+ number = _get_numeric(value, "datetime")
+ if number is not None:
+ return _from_unix_seconds(number)
+
+ if isinstance(value, bytes):
+ value = value.decode()
+
+ assert not isinstance(value, (float, int))
+
+ match = datetime_re.match(value)
+ if match is None:
+ raise ValueError("invalid datetime format")
+
+ kw = match.groupdict()
+ if kw["microsecond"]:
+ kw["microsecond"] = kw["microsecond"].ljust(6, "0")
+
+ tzinfo = _parse_timezone(kw.pop("tzinfo"))
+ kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None}
+ kw_["tzinfo"] = tzinfo
+
+ return datetime(**kw_) # type: ignore
+
+
+def parse_date(value: Union[date, StrBytesIntFloat]) -> date:
+ """
+ Parse a date/int/float/string and return a datetime.date.
+
+ Raise ValueError if the input is well formatted but not a valid date.
+ Raise ValueError if the input isn't well formatted.
+ """
+ if isinstance(value, date):
+ if isinstance(value, datetime):
+ return value.date()
+ else:
+ return value
+
+ number = _get_numeric(value, "date")
+ if number is not None:
+ return _from_unix_seconds(number).date()
+
+ if isinstance(value, bytes):
+ value = value.decode()
+
+ assert not isinstance(value, (float, int))
+ match = date_re.match(value)
+ if match is None:
+ raise ValueError("invalid date format")
+
+ kw = {k: int(v) for k, v in match.groupdict().items()}
+
+ try:
+ return date(**kw)
+ except ValueError:
+ raise ValueError("invalid date format") from None
diff --git a/src/maisa/_utils/_transform.py b/src/maisa/_utils/_transform.py
index f0bcefd4..c19124f0 100644
--- a/src/maisa/_utils/_transform.py
+++ b/src/maisa/_utils/_transform.py
@@ -19,6 +19,7 @@
is_sequence,
)
from .._files import is_base64_file_input
+from ._compat import get_origin, is_typeddict
from ._typing import (
is_list_type,
is_union_type,
@@ -29,7 +30,6 @@
is_annotated_type,
strip_annotated_type,
)
-from .._compat import get_origin, model_dump, is_typeddict
_T = TypeVar("_T")
@@ -169,6 +169,8 @@ def _transform_recursive(
Defaults to the same value as the `annotation` argument.
"""
+ from .._compat import model_dump
+
if inner_type is None:
inner_type = annotation
@@ -333,6 +335,8 @@ async def _async_transform_recursive(
Defaults to the same value as the `annotation` argument.
"""
+ from .._compat import model_dump
+
if inner_type is None:
inner_type = annotation
diff --git a/src/maisa/_utils/_typing.py b/src/maisa/_utils/_typing.py
index 845cd6b2..193109f3 100644
--- a/src/maisa/_utils/_typing.py
+++ b/src/maisa/_utils/_typing.py
@@ -15,7 +15,7 @@
from ._utils import lru_cache
from .._types import InheritsGeneric
-from .._compat import is_union as _is_union
+from ._compat import is_union as _is_union
def is_annotated_type(typ: type) -> bool:
diff --git a/src/maisa/_utils/_utils.py b/src/maisa/_utils/_utils.py
index ea3cf3f2..f0818595 100644
--- a/src/maisa/_utils/_utils.py
+++ b/src/maisa/_utils/_utils.py
@@ -22,7 +22,6 @@
import sniffio
from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike
-from .._compat import parse_date as parse_date, parse_datetime as parse_datetime
_T = TypeVar("_T")
_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...])
diff --git a/tests/test_models.py b/tests/test_models.py
index e9006164..630f6e0e 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -8,7 +8,7 @@
from pydantic import Field
from maisa._utils import PropertyInfo
-from maisa._compat import PYDANTIC_V2, parse_obj, model_dump, model_json
+from maisa._compat import PYDANTIC_V1, parse_obj, model_dump, model_json
from maisa._models import BaseModel, construct_type
@@ -294,12 +294,12 @@ class Model(BaseModel):
assert cast(bool, m.foo) is True
m = Model.construct(foo={"name": 3})
- if PYDANTIC_V2:
- assert isinstance(m.foo, Submodel1)
- assert m.foo.name == 3 # type: ignore
- else:
+ if PYDANTIC_V1:
assert isinstance(m.foo, Submodel2)
assert m.foo.name == "3"
+ else:
+ assert isinstance(m.foo, Submodel1)
+ assert m.foo.name == 3 # type: ignore
def test_list_of_unions() -> None:
@@ -426,10 +426,10 @@ class Model(BaseModel):
expected = datetime(2019, 12, 27, 18, 11, 19, 117000, tzinfo=timezone.utc)
- if PYDANTIC_V2:
- expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}'
- else:
+ if PYDANTIC_V1:
expected_json = '{"created_at": "2019-12-27T18:11:19.117000+00:00"}'
+ else:
+ expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}'
model = Model.construct(created_at="2019-12-27T18:11:19.117Z")
assert model.created_at == expected
@@ -531,7 +531,7 @@ class Model2(BaseModel):
assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)}
assert m4.to_dict(mode="json") == {"created_at": time_str}
- if not PYDANTIC_V2:
+ if PYDANTIC_V1:
with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"):
m.to_dict(warnings=False)
@@ -556,7 +556,7 @@ class Model(BaseModel):
assert m3.model_dump() == {"foo": None}
assert m3.model_dump(exclude_none=True) == {}
- if not PYDANTIC_V2:
+ if PYDANTIC_V1:
with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"):
m.model_dump(round_trip=True)
@@ -580,10 +580,10 @@ class Model(BaseModel):
assert json.loads(m.to_json()) == {"FOO": "hello"}
assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"}
- if PYDANTIC_V2:
- assert m.to_json(indent=None) == '{"FOO":"hello"}'
- else:
+ if PYDANTIC_V1:
assert m.to_json(indent=None) == '{"FOO": "hello"}'
+ else:
+ assert m.to_json(indent=None) == '{"FOO":"hello"}'
m2 = Model()
assert json.loads(m2.to_json()) == {}
@@ -595,7 +595,7 @@ class Model(BaseModel):
assert json.loads(m3.to_json()) == {"FOO": None}
assert json.loads(m3.to_json(exclude_none=True)) == {}
- if not PYDANTIC_V2:
+ if PYDANTIC_V1:
with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"):
m.to_json(warnings=False)
@@ -622,7 +622,7 @@ class Model(BaseModel):
assert json.loads(m3.model_dump_json()) == {"foo": None}
assert json.loads(m3.model_dump_json(exclude_none=True)) == {}
- if not PYDANTIC_V2:
+ if PYDANTIC_V1:
with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"):
m.model_dump_json(round_trip=True)
@@ -679,12 +679,12 @@ class B(BaseModel):
)
assert isinstance(m, A)
assert m.type == "a"
- if PYDANTIC_V2:
- assert m.data == 100 # type: ignore[comparison-overlap]
- else:
+ if PYDANTIC_V1:
# pydantic v1 automatically converts inputs to strings
# if the expected type is a str
assert m.data == "100"
+ else:
+ assert m.data == 100 # type: ignore[comparison-overlap]
def test_discriminated_unions_unknown_variant() -> None:
@@ -768,12 +768,12 @@ class B(BaseModel):
)
assert isinstance(m, A)
assert m.foo_type == "a"
- if PYDANTIC_V2:
- assert m.data == 100 # type: ignore[comparison-overlap]
- else:
+ if PYDANTIC_V1:
# pydantic v1 automatically converts inputs to strings
# if the expected type is a str
assert m.data == "100"
+ else:
+ assert m.data == 100 # type: ignore[comparison-overlap]
def test_discriminated_unions_overlapping_discriminators_invalid_data() -> None:
@@ -833,7 +833,7 @@ class B(BaseModel):
assert UnionType.__discriminator__ is discriminator
-@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1")
+@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1")
def test_type_alias_type() -> None:
Alias = TypeAliasType("Alias", str) # pyright: ignore
@@ -849,7 +849,7 @@ class Model(BaseModel):
assert m.union == "bar"
-@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1")
+@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1")
def test_field_named_cls() -> None:
class Model(BaseModel):
cls: str
@@ -936,7 +936,7 @@ class Type2(BaseModel):
assert isinstance(model.value, InnerType2)
-@pytest.mark.skipif(not PYDANTIC_V2, reason="this is only supported in pydantic v2 for now")
+@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2 for now")
def test_extra_properties() -> None:
class Item(BaseModel):
prop: int
diff --git a/tests/test_transform.py b/tests/test_transform.py
index 12d050be..83ab663a 100644
--- a/tests/test_transform.py
+++ b/tests/test_transform.py
@@ -15,7 +15,7 @@
parse_datetime,
async_transform as _async_transform,
)
-from maisa._compat import PYDANTIC_V2
+from maisa._compat import PYDANTIC_V1
from maisa._models import BaseModel
_T = TypeVar("_T")
@@ -189,7 +189,7 @@ class DateModel(BaseModel):
@pytest.mark.asyncio
async def test_iso8601_format(use_async: bool) -> None:
dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00")
- tz = "Z" if PYDANTIC_V2 else "+00:00"
+ tz = "+00:00" if PYDANTIC_V1 else "Z"
assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap]
assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692" + tz} # type: ignore[comparison-overlap]
@@ -297,11 +297,11 @@ async def test_pydantic_unknown_field(use_async: bool) -> None:
@pytest.mark.asyncio
async def test_pydantic_mismatched_types(use_async: bool) -> None:
model = MyModel.construct(foo=True)
- if PYDANTIC_V2:
+ if PYDANTIC_V1:
+ params = await transform(model, Any, use_async)
+ else:
with pytest.warns(UserWarning):
params = await transform(model, Any, use_async)
- else:
- params = await transform(model, Any, use_async)
assert cast(Any, params) == {"foo": True}
@@ -309,11 +309,11 @@ async def test_pydantic_mismatched_types(use_async: bool) -> None:
@pytest.mark.asyncio
async def test_pydantic_mismatched_object_type(use_async: bool) -> None:
model = MyModel.construct(foo=MyModel.construct(hello="world"))
- if PYDANTIC_V2:
+ if PYDANTIC_V1:
+ params = await transform(model, Any, use_async)
+ else:
with pytest.warns(UserWarning):
params = await transform(model, Any, use_async)
- else:
- params = await transform(model, Any, use_async)
assert cast(Any, params) == {"foo": {"hello": "world"}}
diff --git a/tests/test_utils/test_datetime_parse.py b/tests/test_utils/test_datetime_parse.py
new file mode 100644
index 00000000..713ccba4
--- /dev/null
+++ b/tests/test_utils/test_datetime_parse.py
@@ -0,0 +1,110 @@
+"""
+Copied from https://github.com/pydantic/pydantic/blob/v1.10.22/tests/test_datetime_parse.py
+with modifications so it works without pydantic v1 imports.
+"""
+
+from typing import Type, Union
+from datetime import date, datetime, timezone, timedelta
+
+import pytest
+
+from maisa._utils import parse_date, parse_datetime
+
+
+def create_tz(minutes: int) -> timezone:
+ return timezone(timedelta(minutes=minutes))
+
+
+@pytest.mark.parametrize(
+ "value,result",
+ [
+ # Valid inputs
+ ("1494012444.883309", date(2017, 5, 5)),
+ (b"1494012444.883309", date(2017, 5, 5)),
+ (1_494_012_444.883_309, date(2017, 5, 5)),
+ ("1494012444", date(2017, 5, 5)),
+ (1_494_012_444, date(2017, 5, 5)),
+ (0, date(1970, 1, 1)),
+ ("2012-04-23", date(2012, 4, 23)),
+ (b"2012-04-23", date(2012, 4, 23)),
+ ("2012-4-9", date(2012, 4, 9)),
+ (date(2012, 4, 9), date(2012, 4, 9)),
+ (datetime(2012, 4, 9, 12, 15), date(2012, 4, 9)),
+ # Invalid inputs
+ ("x20120423", ValueError),
+ ("2012-04-56", ValueError),
+ (19_999_999_999, date(2603, 10, 11)), # just before watershed
+ (20_000_000_001, date(1970, 8, 20)), # just after watershed
+ (1_549_316_052, date(2019, 2, 4)), # nowish in s
+ (1_549_316_052_104, date(2019, 2, 4)), # nowish in ms
+ (1_549_316_052_104_324, date(2019, 2, 4)), # nowish in μs
+ (1_549_316_052_104_324_096, date(2019, 2, 4)), # nowish in ns
+ ("infinity", date(9999, 12, 31)),
+ ("inf", date(9999, 12, 31)),
+ (float("inf"), date(9999, 12, 31)),
+ ("infinity ", date(9999, 12, 31)),
+ (int("1" + "0" * 100), date(9999, 12, 31)),
+ (1e1000, date(9999, 12, 31)),
+ ("-infinity", date(1, 1, 1)),
+ ("-inf", date(1, 1, 1)),
+ ("nan", ValueError),
+ ],
+)
+def test_date_parsing(value: Union[str, bytes, int, float], result: Union[date, Type[Exception]]) -> None:
+ if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance]
+ with pytest.raises(result):
+ parse_date(value)
+ else:
+ assert parse_date(value) == result
+
+
+@pytest.mark.parametrize(
+ "value,result",
+ [
+ # Valid inputs
+ # values in seconds
+ ("1494012444.883309", datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)),
+ (1_494_012_444.883_309, datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)),
+ ("1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)),
+ (b"1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)),
+ (1_494_012_444, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)),
+ # values in ms
+ ("1494012444000.883309", datetime(2017, 5, 5, 19, 27, 24, 883, tzinfo=timezone.utc)),
+ ("-1494012444000.883309", datetime(1922, 8, 29, 4, 32, 35, 999117, tzinfo=timezone.utc)),
+ (1_494_012_444_000, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)),
+ ("2012-04-23T09:15:00", datetime(2012, 4, 23, 9, 15)),
+ ("2012-4-9 4:8:16", datetime(2012, 4, 9, 4, 8, 16)),
+ ("2012-04-23T09:15:00Z", datetime(2012, 4, 23, 9, 15, 0, 0, timezone.utc)),
+ ("2012-4-9 4:8:16-0320", datetime(2012, 4, 9, 4, 8, 16, 0, create_tz(-200))),
+ ("2012-04-23T10:20:30.400+02:30", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(150))),
+ ("2012-04-23T10:20:30.400+02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(120))),
+ ("2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))),
+ (b"2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))),
+ (datetime(2017, 5, 5), datetime(2017, 5, 5)),
+ (0, datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc)),
+ # Invalid inputs
+ ("x20120423091500", ValueError),
+ ("2012-04-56T09:15:90", ValueError),
+ ("2012-04-23T11:05:00-25:00", ValueError),
+ (19_999_999_999, datetime(2603, 10, 11, 11, 33, 19, tzinfo=timezone.utc)), # just before watershed
+ (20_000_000_001, datetime(1970, 8, 20, 11, 33, 20, 1000, tzinfo=timezone.utc)), # just after watershed
+ (1_549_316_052, datetime(2019, 2, 4, 21, 34, 12, 0, tzinfo=timezone.utc)), # nowish in s
+ (1_549_316_052_104, datetime(2019, 2, 4, 21, 34, 12, 104_000, tzinfo=timezone.utc)), # nowish in ms
+ (1_549_316_052_104_324, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in μs
+ (1_549_316_052_104_324_096, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in ns
+ ("infinity", datetime(9999, 12, 31, 23, 59, 59, 999999)),
+ ("inf", datetime(9999, 12, 31, 23, 59, 59, 999999)),
+ ("inf ", datetime(9999, 12, 31, 23, 59, 59, 999999)),
+ (1e50, datetime(9999, 12, 31, 23, 59, 59, 999999)),
+ (float("inf"), datetime(9999, 12, 31, 23, 59, 59, 999999)),
+ ("-infinity", datetime(1, 1, 1, 0, 0)),
+ ("-inf", datetime(1, 1, 1, 0, 0)),
+ ("nan", ValueError),
+ ],
+)
+def test_datetime_parsing(value: Union[str, bytes, int, float], result: Union[datetime, Type[Exception]]) -> None:
+ if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance]
+ with pytest.raises(result):
+ parse_datetime(value)
+ else:
+ assert parse_datetime(value) == result
diff --git a/tests/utils.py b/tests/utils.py
index c9edc070..ec6506d2 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -19,7 +19,7 @@
is_annotated_type,
is_type_alias_type,
)
-from maisa._compat import PYDANTIC_V2, field_outer_type, get_model_fields
+from maisa._compat import PYDANTIC_V1, field_outer_type, get_model_fields
from maisa._models import BaseModel
BaseModelT = TypeVar("BaseModelT", bound=BaseModel)
@@ -28,12 +28,12 @@
def assert_matches_model(model: type[BaseModelT], value: BaseModelT, *, path: list[str]) -> bool:
for name, field in get_model_fields(model).items():
field_value = getattr(value, name)
- if PYDANTIC_V2:
- allow_none = False
- else:
+ if PYDANTIC_V1:
# in v1 nullability was structured differently
# https://docs.pydantic.dev/2.0/migration/#required-optional-and-nullable-fields
allow_none = getattr(field, "allow_none", False)
+ else:
+ allow_none = False
assert_matches_type(
field_outer_type(field),
From cd89671fe53d2e31916a53f5bbd3c7bc9c7ab56d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 5 Sep 2025 15:52:38 +0000
Subject: [PATCH 131/200] chore(internal): codegen related update
---
mypy.ini | 50 ------------------------------------------------
pyproject.toml | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 52 insertions(+), 50 deletions(-)
delete mode 100644 mypy.ini
diff --git a/mypy.ini b/mypy.ini
deleted file mode 100644
index 602e0dc5..00000000
--- a/mypy.ini
+++ /dev/null
@@ -1,50 +0,0 @@
-[mypy]
-pretty = True
-show_error_codes = True
-
-# Exclude _files.py because mypy isn't smart enough to apply
-# the correct type narrowing and as this is an internal module
-# it's fine to just use Pyright.
-#
-# We also exclude our `tests` as mypy doesn't always infer
-# types correctly and Pyright will still catch any type errors.
-exclude = ^(src/maisa/_files\.py|_dev/.*\.py|tests/.*)$
-
-strict_equality = True
-implicit_reexport = True
-check_untyped_defs = True
-no_implicit_optional = True
-
-warn_return_any = True
-warn_unreachable = True
-warn_unused_configs = True
-
-# Turn these options off as it could cause conflicts
-# with the Pyright options.
-warn_unused_ignores = False
-warn_redundant_casts = False
-
-disallow_any_generics = True
-disallow_untyped_defs = True
-disallow_untyped_calls = True
-disallow_subclassing_any = True
-disallow_incomplete_defs = True
-disallow_untyped_decorators = True
-cache_fine_grained = True
-
-# By default, mypy reports an error if you assign a value to the result
-# of a function call that doesn't return anything. We do this in our test
-# cases:
-# ```
-# result = ...
-# assert result is None
-# ```
-# Changing this codegen to make mypy happy would increase complexity
-# and would not be worth it.
-disable_error_code = func-returns-value,overload-cannot-match
-
-# https://github.com/python/mypy/issues/12162
-[mypy.overrides]
-module = "black.files.*"
-ignore_errors = true
-ignore_missing_imports = true
diff --git a/pyproject.toml b/pyproject.toml
index 331c50a0..101edf40 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -157,6 +157,58 @@ reportOverlappingOverload = false
reportImportCycles = false
reportPrivateUsage = false
+[tool.mypy]
+pretty = true
+show_error_codes = true
+
+# Exclude _files.py because mypy isn't smart enough to apply
+# the correct type narrowing and as this is an internal module
+# it's fine to just use Pyright.
+#
+# We also exclude our `tests` as mypy doesn't always infer
+# types correctly and Pyright will still catch any type errors.
+exclude = ['src/maisa/_files.py', '_dev/.*.py', 'tests/.*']
+
+strict_equality = true
+implicit_reexport = true
+check_untyped_defs = true
+no_implicit_optional = true
+
+warn_return_any = true
+warn_unreachable = true
+warn_unused_configs = true
+
+# Turn these options off as it could cause conflicts
+# with the Pyright options.
+warn_unused_ignores = false
+warn_redundant_casts = false
+
+disallow_any_generics = true
+disallow_untyped_defs = true
+disallow_untyped_calls = true
+disallow_subclassing_any = true
+disallow_incomplete_defs = true
+disallow_untyped_decorators = true
+cache_fine_grained = true
+
+# By default, mypy reports an error if you assign a value to the result
+# of a function call that doesn't return anything. We do this in our test
+# cases:
+# ```
+# result = ...
+# assert result is None
+# ```
+# Changing this codegen to make mypy happy would increase complexity
+# and would not be worth it.
+disable_error_code = "func-returns-value,overload-cannot-match"
+
+# https://github.com/python/mypy/issues/12162
+[[tool.mypy.overrides]]
+module = "black.files.*"
+ignore_errors = true
+ignore_missing_imports = true
+
+
[tool.ruff]
line-length = 120
output-format = "grouped"
From d6ec18d85859eb1069e30e57450e55e0ecfd89df Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 6 Sep 2025 05:13:23 +0000
Subject: [PATCH 132/200] chore(tests): simplify `get_platform` test
`nest_asyncio` is archived and broken on some platforms so it's
not worth keeping in our test suite.
---
pyproject.toml | 1 -
requirements-dev.lock | 1 -
tests/test_client.py | 53 +++++--------------------------------------
3 files changed, 6 insertions(+), 49 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 101edf40..c0a2f23a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -56,7 +56,6 @@ dev-dependencies = [
"dirty-equals>=0.6.0",
"importlib-metadata>=6.7.0",
"rich>=13.7.1",
- "nest_asyncio==1.6.0",
"pytest-xdist>=3.6.1",
]
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 89083658..51b24b0a 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -75,7 +75,6 @@ multidict==6.4.4
mypy==1.14.1
mypy-extensions==1.0.0
# via mypy
-nest-asyncio==1.6.0
nodeenv==1.8.0
# via pyright
nox==2023.4.22
diff --git a/tests/test_client.py b/tests/test_client.py
index 870afd1e..320919e0 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -6,13 +6,10 @@
import os
import sys
import json
-import time
import asyncio
import inspect
-import subprocess
import tracemalloc
from typing import Any, Union, cast
-from textwrap import dedent
from unittest import mock
from typing_extensions import Literal
@@ -23,14 +20,17 @@
from maisa import Maisa, AsyncMaisa, APIResponseValidationError
from maisa._types import Omit
+from maisa._utils import asyncify
from maisa._models import BaseModel, FinalRequestOptions
from maisa._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError
from maisa._base_client import (
DEFAULT_TIMEOUT,
HTTPX_DEFAULT_TIMEOUT,
BaseClient,
+ OtherPlatform,
DefaultHttpxClient,
DefaultAsyncHttpxClient,
+ get_platform,
make_request_options,
)
@@ -1607,50 +1607,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
assert response.http_request.headers.get("x-stainless-retry-count") == "42"
- def test_get_platform(self) -> None:
- # A previous implementation of asyncify could leave threads unterminated when
- # used with nest_asyncio.
- #
- # Since nest_asyncio.apply() is global and cannot be un-applied, this
- # test is run in a separate process to avoid affecting other tests.
- test_code = dedent("""
- import asyncio
- import nest_asyncio
- import threading
-
- from maisa._utils import asyncify
- from maisa._base_client import get_platform
-
- async def test_main() -> None:
- result = await asyncify(get_platform)()
- print(result)
- for thread in threading.enumerate():
- print(thread.name)
-
- nest_asyncio.apply()
- asyncio.run(test_main())
- """)
- with subprocess.Popen(
- [sys.executable, "-c", test_code],
- text=True,
- ) as process:
- timeout = 10 # seconds
-
- start_time = time.monotonic()
- while True:
- return_code = process.poll()
- if return_code is not None:
- if return_code != 0:
- raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code")
-
- # success
- break
-
- if time.monotonic() - start_time > timeout:
- process.kill()
- raise AssertionError("calling get_platform using asyncify resulted in a hung process")
-
- time.sleep(0.1)
+ async def test_get_platform(self) -> None:
+ platform = await asyncify(get_platform)()
+ assert isinstance(platform, (str, OtherPlatform))
async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Test that the proxy environment variables are set correctly
From 1d8189553d8b258cf613335bab65e2bf26bad23f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 17 Sep 2025 03:16:30 +0000
Subject: [PATCH 133/200] chore(internal): update pydantic dependency
---
requirements-dev.lock | 7 +++++--
requirements.lock | 7 +++++--
src/maisa/_models.py | 14 ++++++++++----
3 files changed, 20 insertions(+), 8 deletions(-)
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 51b24b0a..f89cf419 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -88,9 +88,9 @@ pluggy==1.5.0
propcache==0.3.1
# via aiohttp
# via yarl
-pydantic==2.10.3
+pydantic==2.11.9
# via maisa
-pydantic-core==2.27.1
+pydantic-core==2.33.2
# via pydantic
pygments==2.18.0
# via rich
@@ -126,6 +126,9 @@ typing-extensions==4.12.2
# via pydantic
# via pydantic-core
# via pyright
+ # via typing-inspection
+typing-inspection==0.4.1
+ # via pydantic
virtualenv==20.24.5
# via nox
yarl==1.20.0
diff --git a/requirements.lock b/requirements.lock
index ab65a0e9..cd52c1ce 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -55,9 +55,9 @@ multidict==6.4.4
propcache==0.3.1
# via aiohttp
# via yarl
-pydantic==2.10.3
+pydantic==2.11.9
# via maisa
-pydantic-core==2.27.1
+pydantic-core==2.33.2
# via pydantic
sniffio==1.3.0
# via anyio
@@ -68,5 +68,8 @@ typing-extensions==4.12.2
# via multidict
# via pydantic
# via pydantic-core
+ # via typing-inspection
+typing-inspection==0.4.1
+ # via pydantic
yarl==1.20.0
# via aiohttp
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index 3a6017ef..6a3cd1d2 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -256,7 +256,7 @@ def model_dump(
mode: Literal["json", "python"] | str = "python",
include: IncEx | None = None,
exclude: IncEx | None = None,
- by_alias: bool = False,
+ by_alias: bool | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
@@ -264,6 +264,7 @@ def model_dump(
warnings: bool | Literal["none", "warn", "error"] = True,
context: dict[str, Any] | None = None,
serialize_as_any: bool = False,
+ fallback: Callable[[Any], Any] | None = None,
) -> dict[str, Any]:
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump
@@ -295,10 +296,12 @@ def model_dump(
raise ValueError("context is only supported in Pydantic v2")
if serialize_as_any != False:
raise ValueError("serialize_as_any is only supported in Pydantic v2")
+ if fallback is not None:
+ raise ValueError("fallback is only supported in Pydantic v2")
dumped = super().dict( # pyright: ignore[reportDeprecated]
include=include,
exclude=exclude,
- by_alias=by_alias,
+ by_alias=by_alias if by_alias is not None else False,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
@@ -313,13 +316,14 @@ def model_dump_json(
indent: int | None = None,
include: IncEx | None = None,
exclude: IncEx | None = None,
- by_alias: bool = False,
+ by_alias: bool | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
round_trip: bool = False,
warnings: bool | Literal["none", "warn", "error"] = True,
context: dict[str, Any] | None = None,
+ fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
) -> str:
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json
@@ -348,11 +352,13 @@ def model_dump_json(
raise ValueError("context is only supported in Pydantic v2")
if serialize_as_any != False:
raise ValueError("serialize_as_any is only supported in Pydantic v2")
+ if fallback is not None:
+ raise ValueError("fallback is only supported in Pydantic v2")
return super().json( # type: ignore[reportDeprecated]
indent=indent,
include=include,
exclude=exclude,
- by_alias=by_alias,
+ by_alias=by_alias if by_alias is not None else False,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
From 6abb39527de16f19c4187c1ea6dceda1c891ec13 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 19 Sep 2025 03:40:16 +0000
Subject: [PATCH 134/200] chore(types): change optional parameter type from
NotGiven to Omit
---
src/maisa/__init__.py | 4 +-
src/maisa/_base_client.py | 18 +--
src/maisa/_client.py | 16 +-
src/maisa/_qs.py | 14 +-
src/maisa/_types.py | 29 ++--
src/maisa/_utils/_transform.py | 4 +-
src/maisa/_utils/_utils.py | 8 +-
.../resources/capabilities/capabilities.py | 42 +++---
src/maisa/resources/capabilities/media.py | 138 +++++++++---------
.../resources/file_interpreter/from_audio.py | 6 +-
.../resources/file_interpreter/from_docx.py | 6 +-
.../resources/file_interpreter/from_html.py | 6 +-
.../resources/file_interpreter/from_image.py | 6 +-
.../resources/file_interpreter/from_pdf.py | 10 +-
src/maisa/resources/kpu.py | 26 ++--
src/maisa/resources/models/embeddings.py | 6 +-
tests/test_transform.py | 11 +-
17 files changed, 183 insertions(+), 167 deletions(-)
diff --git a/src/maisa/__init__.py b/src/maisa/__init__.py
index 58d2bf2b..726750c7 100644
--- a/src/maisa/__init__.py
+++ b/src/maisa/__init__.py
@@ -3,7 +3,7 @@
import typing as _t
from . import types
-from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes
+from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given
from ._utils import file_from_path
from ._client import Maisa, Client, Stream, Timeout, Transport, AsyncMaisa, AsyncClient, AsyncStream, RequestOptions
from ._models import BaseModel
@@ -38,7 +38,9 @@
"ProxiesTypes",
"NotGiven",
"NOT_GIVEN",
+ "not_given",
"Omit",
+ "omit",
"MaisaError",
"APIError",
"APIStatusError",
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index 4de0f3c0..13da7d81 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -42,7 +42,6 @@
from ._qs import Querystring
from ._files import to_httpx_files, async_to_httpx_files
from ._types import (
- NOT_GIVEN,
Body,
Omit,
Query,
@@ -57,6 +56,7 @@
RequestOptions,
HttpxRequestFiles,
ModelBuilderProtocol,
+ not_given,
)
from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
from ._compat import PYDANTIC_V1, model_copy, model_dump
@@ -145,9 +145,9 @@ def __init__(
def __init__(
self,
*,
- url: URL | NotGiven = NOT_GIVEN,
- json: Body | NotGiven = NOT_GIVEN,
- params: Query | NotGiven = NOT_GIVEN,
+ url: URL | NotGiven = not_given,
+ json: Body | NotGiven = not_given,
+ params: Query | NotGiven = not_given,
) -> None:
self.url = url
self.json = json
@@ -595,7 +595,7 @@ def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalReques
# we internally support defining a temporary header to override the
# default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response`
# see _response.py for implementation details
- override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, NOT_GIVEN)
+ override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, not_given)
if is_given(override_cast_to):
options.headers = headers
return cast(Type[ResponseT], override_cast_to)
@@ -825,7 +825,7 @@ def __init__(
version: str,
base_url: str | URL,
max_retries: int = DEFAULT_MAX_RETRIES,
- timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.Client | None = None,
custom_headers: Mapping[str, str] | None = None,
custom_query: Mapping[str, object] | None = None,
@@ -1356,7 +1356,7 @@ def __init__(
base_url: str | URL,
_strict_response_validation: bool,
max_retries: int = DEFAULT_MAX_RETRIES,
- timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.AsyncClient | None = None,
custom_headers: Mapping[str, str] | None = None,
custom_query: Mapping[str, object] | None = None,
@@ -1818,8 +1818,8 @@ def make_request_options(
extra_query: Query | None = None,
extra_body: Body | None = None,
idempotency_key: str | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- post_parser: PostParser | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ post_parser: PostParser | NotGiven = not_given,
) -> RequestOptions:
"""Create a dict of type RequestOptions without keys of NotGiven values."""
options: RequestOptions = {}
diff --git a/src/maisa/_client.py b/src/maisa/_client.py
index 7b112683..1e53be64 100644
--- a/src/maisa/_client.py
+++ b/src/maisa/_client.py
@@ -3,7 +3,7 @@
from __future__ import annotations
import os
-from typing import Any, Union, Mapping
+from typing import Any, Mapping
from typing_extensions import Self, override
import httpx
@@ -11,13 +11,13 @@
from . import _exceptions
from ._qs import Querystring
from ._types import (
- NOT_GIVEN,
Omit,
Timeout,
NotGiven,
Transport,
ProxiesTypes,
RequestOptions,
+ not_given,
)
from ._utils import is_given, get_async_library
from ._version import __version__
@@ -52,7 +52,7 @@ def __init__(
*,
api_key: str | None = None,
base_url: str | httpx.URL | None = None,
- timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN,
+ timeout: float | Timeout | None | NotGiven = not_given,
max_retries: int = DEFAULT_MAX_RETRIES,
default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
@@ -130,9 +130,9 @@ def copy(
*,
api_key: str | None = None,
base_url: str | httpx.URL | None = None,
- timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.Client | None = None,
- max_retries: int | NotGiven = NOT_GIVEN,
+ max_retries: int | NotGiven = not_given,
default_headers: Mapping[str, str] | None = None,
set_default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
@@ -226,7 +226,7 @@ def __init__(
*,
api_key: str | None = None,
base_url: str | httpx.URL | None = None,
- timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN,
+ timeout: float | Timeout | None | NotGiven = not_given,
max_retries: int = DEFAULT_MAX_RETRIES,
default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
@@ -304,9 +304,9 @@ def copy(
*,
api_key: str | None = None,
base_url: str | httpx.URL | None = None,
- timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.AsyncClient | None = None,
- max_retries: int | NotGiven = NOT_GIVEN,
+ max_retries: int | NotGiven = not_given,
default_headers: Mapping[str, str] | None = None,
set_default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
diff --git a/src/maisa/_qs.py b/src/maisa/_qs.py
index 274320ca..ada6fd3f 100644
--- a/src/maisa/_qs.py
+++ b/src/maisa/_qs.py
@@ -4,7 +4,7 @@
from urllib.parse import parse_qs, urlencode
from typing_extensions import Literal, get_args
-from ._types import NOT_GIVEN, NotGiven, NotGivenOr
+from ._types import NotGiven, not_given
from ._utils import flatten
_T = TypeVar("_T")
@@ -41,8 +41,8 @@ def stringify(
self,
params: Params,
*,
- array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN,
- nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN,
+ array_format: ArrayFormat | NotGiven = not_given,
+ nested_format: NestedFormat | NotGiven = not_given,
) -> str:
return urlencode(
self.stringify_items(
@@ -56,8 +56,8 @@ def stringify_items(
self,
params: Params,
*,
- array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN,
- nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN,
+ array_format: ArrayFormat | NotGiven = not_given,
+ nested_format: NestedFormat | NotGiven = not_given,
) -> list[tuple[str, str]]:
opts = Options(
qs=self,
@@ -143,8 +143,8 @@ def __init__(
self,
qs: Querystring = _qs,
*,
- array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN,
- nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN,
+ array_format: ArrayFormat | NotGiven = not_given,
+ nested_format: NestedFormat | NotGiven = not_given,
) -> None:
self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format
self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format
diff --git a/src/maisa/_types.py b/src/maisa/_types.py
index 81ec6b72..38a8779b 100644
--- a/src/maisa/_types.py
+++ b/src/maisa/_types.py
@@ -117,18 +117,21 @@ class RequestOptions(TypedDict, total=False):
# Sentinel class used until PEP 0661 is accepted
class NotGiven:
"""
- A sentinel singleton class used to distinguish omitted keyword arguments
- from those passed in with the value None (which may have different behavior).
+ For parameters with a meaningful None value, we need to distinguish between
+ the user explicitly passing None, and the user not passing the parameter at
+ all.
+
+ User code shouldn't need to use not_given directly.
For example:
```py
- def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ...
+ def create(timeout: Timeout | None | NotGiven = not_given): ...
- get(timeout=1) # 1s timeout
- get(timeout=None) # No timeout
- get() # Default timeout behavior, which may not be statically known at the method definition.
+ create(timeout=1) # 1s timeout
+ create(timeout=None) # No timeout
+ create() # Default timeout behavior
```
"""
@@ -140,13 +143,14 @@ def __repr__(self) -> str:
return "NOT_GIVEN"
-NotGivenOr = Union[_T, NotGiven]
+not_given = NotGiven()
+# for backwards compatibility:
NOT_GIVEN = NotGiven()
class Omit:
- """In certain situations you need to be able to represent a case where a default value has
- to be explicitly removed and `None` is not an appropriate substitute, for example:
+ """
+ To explicitly omit something from being sent in a request, use `omit`.
```py
# as the default `Content-Type` header is `application/json` that will be sent
@@ -156,8 +160,8 @@ class Omit:
# to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983'
client.post(..., headers={"Content-Type": "multipart/form-data"})
- # instead you can remove the default `application/json` header by passing Omit
- client.post(..., headers={"Content-Type": Omit()})
+ # instead you can remove the default `application/json` header by passing omit
+ client.post(..., headers={"Content-Type": omit})
```
"""
@@ -165,6 +169,9 @@ def __bool__(self) -> Literal[False]:
return False
+omit = Omit()
+
+
@runtime_checkable
class ModelBuilderProtocol(Protocol):
@classmethod
diff --git a/src/maisa/_utils/_transform.py b/src/maisa/_utils/_transform.py
index c19124f0..52075492 100644
--- a/src/maisa/_utils/_transform.py
+++ b/src/maisa/_utils/_transform.py
@@ -268,7 +268,7 @@ def _transform_typeddict(
annotations = get_type_hints(expected_type, include_extras=True)
for key, value in data.items():
if not is_given(value):
- # we don't need to include `NotGiven` values here as they'll
+ # we don't need to include omitted values here as they'll
# be stripped out before the request is sent anyway
continue
@@ -434,7 +434,7 @@ async def _async_transform_typeddict(
annotations = get_type_hints(expected_type, include_extras=True)
for key, value in data.items():
if not is_given(value):
- # we don't need to include `NotGiven` values here as they'll
+ # we don't need to include omitted values here as they'll
# be stripped out before the request is sent anyway
continue
diff --git a/src/maisa/_utils/_utils.py b/src/maisa/_utils/_utils.py
index f0818595..50d59269 100644
--- a/src/maisa/_utils/_utils.py
+++ b/src/maisa/_utils/_utils.py
@@ -21,7 +21,7 @@
import sniffio
-from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike
+from .._types import Omit, NotGiven, FileTypes, HeadersLike
_T = TypeVar("_T")
_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...])
@@ -63,7 +63,7 @@ def _extract_items(
try:
key = path[index]
except IndexError:
- if isinstance(obj, NotGiven):
+ if not is_given(obj):
# no value was provided - we can safely ignore
return []
@@ -126,8 +126,8 @@ def _extract_items(
return []
-def is_given(obj: NotGivenOr[_T]) -> TypeGuard[_T]:
- return not isinstance(obj, NotGiven)
+def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]:
+ return not isinstance(obj, NotGiven) and not isinstance(obj, Omit)
# Type safe methods for narrowing types with TypeVars.
diff --git a/src/maisa/resources/capabilities/capabilities.py b/src/maisa/resources/capabilities/capabilities.py
index 8412385e..5c4df7db 100644
--- a/src/maisa/resources/capabilities/capabilities.py
+++ b/src/maisa/resources/capabilities/capabilities.py
@@ -16,7 +16,7 @@
AsyncMediaResourceWithStreamingResponse,
)
from ...types import capability_compare_params, capability_extract_params, capability_summarize_params
-from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
+from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
from ..._utils import maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
@@ -64,14 +64,14 @@ def compare(
text1: str,
text2: str,
variables: Dict[str, capability_compare_params.Variables],
- lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
- prompt: str | NotGiven = NOT_GIVEN,
+ lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | Omit = omit,
+ prompt: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TextComparator:
"""
Compare extracts of text based on a specific data.
@@ -119,13 +119,13 @@ def extract(
*,
text: str,
variables: Dict[str, capability_extract_params.Variables],
- lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
+ lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TextExtractor:
"""Extracts structured data from text.
@@ -168,16 +168,16 @@ def summarize(
self,
*,
text: str,
- format: Literal["paragraph", "bullet"] | NotGiven = NOT_GIVEN,
- lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
- length: Literal["short", "medium", "long"] | NotGiven = NOT_GIVEN,
- summary_hint: str | NotGiven = NOT_GIVEN,
+ format: Literal["paragraph", "bullet"] | Omit = omit,
+ lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | Omit = omit,
+ length: Literal["short", "medium", "long"] | Omit = omit,
+ summary_hint: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TextSummary:
"""Summarizes a text.
@@ -253,14 +253,14 @@ async def compare(
text1: str,
text2: str,
variables: Dict[str, capability_compare_params.Variables],
- lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
- prompt: str | NotGiven = NOT_GIVEN,
+ lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | Omit = omit,
+ prompt: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TextComparator:
"""
Compare extracts of text based on a specific data.
@@ -308,13 +308,13 @@ async def extract(
*,
text: str,
variables: Dict[str, capability_extract_params.Variables],
- lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
+ lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TextExtractor:
"""Extracts structured data from text.
@@ -357,16 +357,16 @@ async def summarize(
self,
*,
text: str,
- format: Literal["paragraph", "bullet"] | NotGiven = NOT_GIVEN,
- lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
- length: Literal["short", "medium", "long"] | NotGiven = NOT_GIVEN,
- summary_hint: str | NotGiven = NOT_GIVEN,
+ format: Literal["paragraph", "bullet"] | Omit = omit,
+ lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | Omit = omit,
+ length: Literal["short", "medium", "long"] | Omit = omit,
+ summary_hint: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TextSummary:
"""Summarizes a text.
diff --git a/src/maisa/resources/capabilities/media.py b/src/maisa/resources/capabilities/media.py
index 2f3a399d..d53ac9a2 100644
--- a/src/maisa/resources/capabilities/media.py
+++ b/src/maisa/resources/capabilities/media.py
@@ -7,7 +7,7 @@
import httpx
-from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
+from ..._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given
from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
@@ -51,26 +51,26 @@ def compare(
*,
file1: FileTypes,
file2: FileTypes,
- lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
- prompt: str | NotGiven = NOT_GIVEN,
- variable1_description: str | NotGiven = NOT_GIVEN,
- variable1_name: str | NotGiven = NOT_GIVEN,
- variable1_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable2_description: str | NotGiven = NOT_GIVEN,
- variable2_name: str | NotGiven = NOT_GIVEN,
- variable2_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable3_description: str | NotGiven = NOT_GIVEN,
- variable3_name: str | NotGiven = NOT_GIVEN,
- variable3_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable4_description: str | NotGiven = NOT_GIVEN,
- variable4_name: str | NotGiven = NOT_GIVEN,
- variable4_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
+ lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | Omit = omit,
+ prompt: str | Omit = omit,
+ variable1_description: str | Omit = omit,
+ variable1_name: str | Omit = omit,
+ variable1_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
+ variable2_description: str | Omit = omit,
+ variable2_name: str | Omit = omit,
+ variable2_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
+ variable3_description: str | Omit = omit,
+ variable3_name: str | Omit = omit,
+ variable3_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
+ variable4_description: str | Omit = omit,
+ variable4_name: str | Omit = omit,
+ variable4_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TextComparator:
"""Compare extracts of media files based on a specific data.
@@ -155,25 +155,25 @@ def extract(
self,
*,
file: FileTypes,
- lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
- variable1_description: str | NotGiven = NOT_GIVEN,
- variable1_name: str | NotGiven = NOT_GIVEN,
- variable1_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable2_description: str | NotGiven = NOT_GIVEN,
- variable2_name: str | NotGiven = NOT_GIVEN,
- variable2_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable3_description: str | NotGiven = NOT_GIVEN,
- variable3_name: str | NotGiven = NOT_GIVEN,
- variable3_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable4_description: str | NotGiven = NOT_GIVEN,
- variable4_name: str | NotGiven = NOT_GIVEN,
- variable4_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
+ lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | Omit = omit,
+ variable1_description: str | Omit = omit,
+ variable1_name: str | Omit = omit,
+ variable1_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
+ variable2_description: str | Omit = omit,
+ variable2_name: str | Omit = omit,
+ variable2_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
+ variable3_description: str | Omit = omit,
+ variable3_name: str | Omit = omit,
+ variable3_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
+ variable4_description: str | Omit = omit,
+ variable4_name: str | Omit = omit,
+ variable4_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TextExtractor:
"""Extracts structured data from a file.
@@ -254,16 +254,16 @@ def summarize(
self,
*,
file: FileTypes,
- format: Literal["paragraph", "bullet"] | NotGiven = NOT_GIVEN,
- lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
- length: Literal["short", "medium", "long"] | NotGiven = NOT_GIVEN,
- summary_hint: str | NotGiven = NOT_GIVEN,
+ format: Literal["paragraph", "bullet"] | Omit = omit,
+ lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | Omit = omit,
+ length: Literal["short", "medium", "long"] | Omit = omit,
+ summary_hint: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TextSummary:
"""Summarizes a media file.
@@ -339,26 +339,26 @@ async def compare(
*,
file1: FileTypes,
file2: FileTypes,
- lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
- prompt: str | NotGiven = NOT_GIVEN,
- variable1_description: str | NotGiven = NOT_GIVEN,
- variable1_name: str | NotGiven = NOT_GIVEN,
- variable1_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable2_description: str | NotGiven = NOT_GIVEN,
- variable2_name: str | NotGiven = NOT_GIVEN,
- variable2_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable3_description: str | NotGiven = NOT_GIVEN,
- variable3_name: str | NotGiven = NOT_GIVEN,
- variable3_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable4_description: str | NotGiven = NOT_GIVEN,
- variable4_name: str | NotGiven = NOT_GIVEN,
- variable4_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
+ lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | Omit = omit,
+ prompt: str | Omit = omit,
+ variable1_description: str | Omit = omit,
+ variable1_name: str | Omit = omit,
+ variable1_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
+ variable2_description: str | Omit = omit,
+ variable2_name: str | Omit = omit,
+ variable2_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
+ variable3_description: str | Omit = omit,
+ variable3_name: str | Omit = omit,
+ variable3_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
+ variable4_description: str | Omit = omit,
+ variable4_name: str | Omit = omit,
+ variable4_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TextComparator:
"""Compare extracts of media files based on a specific data.
@@ -443,25 +443,25 @@ async def extract(
self,
*,
file: FileTypes,
- lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
- variable1_description: str | NotGiven = NOT_GIVEN,
- variable1_name: str | NotGiven = NOT_GIVEN,
- variable1_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable2_description: str | NotGiven = NOT_GIVEN,
- variable2_name: str | NotGiven = NOT_GIVEN,
- variable2_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable3_description: str | NotGiven = NOT_GIVEN,
- variable3_name: str | NotGiven = NOT_GIVEN,
- variable3_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
- variable4_description: str | NotGiven = NOT_GIVEN,
- variable4_name: str | NotGiven = NOT_GIVEN,
- variable4_type: Literal["string", "number", "date", "boolean"] | NotGiven = NOT_GIVEN,
+ lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | Omit = omit,
+ variable1_description: str | Omit = omit,
+ variable1_name: str | Omit = omit,
+ variable1_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
+ variable2_description: str | Omit = omit,
+ variable2_name: str | Omit = omit,
+ variable2_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
+ variable3_description: str | Omit = omit,
+ variable3_name: str | Omit = omit,
+ variable3_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
+ variable4_description: str | Omit = omit,
+ variable4_name: str | Omit = omit,
+ variable4_type: Literal["string", "number", "date", "boolean"] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TextExtractor:
"""Extracts structured data from a file.
@@ -542,16 +542,16 @@ async def summarize(
self,
*,
file: FileTypes,
- format: Literal["paragraph", "bullet"] | NotGiven = NOT_GIVEN,
- lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | NotGiven = NOT_GIVEN,
- length: Literal["short", "medium", "long"] | NotGiven = NOT_GIVEN,
- summary_hint: str | NotGiven = NOT_GIVEN,
+ format: Literal["paragraph", "bullet"] | Omit = omit,
+ lang: Literal["en", "es", "pt", "fr", "de", "it", "nl", "sv", "pl", "ro"] | Omit = omit,
+ length: Literal["short", "medium", "long"] | Omit = omit,
+ summary_hint: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> TextSummary:
"""Summarizes a media file.
diff --git a/src/maisa/resources/file_interpreter/from_audio.py b/src/maisa/resources/file_interpreter/from_audio.py
index a543de36..149ed68b 100644
--- a/src/maisa/resources/file_interpreter/from_audio.py
+++ b/src/maisa/resources/file_interpreter/from_audio.py
@@ -6,7 +6,7 @@
import httpx
-from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
+from ..._types import Body, Query, Headers, NotGiven, FileTypes, not_given
from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
@@ -51,7 +51,7 @@ def create(
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> object:
"""
Interprets an audio file and returns a description of the audio.
@@ -111,7 +111,7 @@ async def create(
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> object:
"""
Interprets an audio file and returns a description of the audio.
diff --git a/src/maisa/resources/file_interpreter/from_docx.py b/src/maisa/resources/file_interpreter/from_docx.py
index fbff89b8..c799b57b 100644
--- a/src/maisa/resources/file_interpreter/from_docx.py
+++ b/src/maisa/resources/file_interpreter/from_docx.py
@@ -6,7 +6,7 @@
import httpx
-from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
+from ..._types import Body, Query, Headers, NotGiven, FileTypes, not_given
from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
@@ -51,7 +51,7 @@ def create(
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> object:
"""
Interprets docx file and returns a zip file with the markdown and media files.
@@ -111,7 +111,7 @@ async def create(
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> object:
"""
Interprets docx file and returns a zip file with the markdown and media files.
diff --git a/src/maisa/resources/file_interpreter/from_html.py b/src/maisa/resources/file_interpreter/from_html.py
index ca2cbe08..38e4fe7b 100644
--- a/src/maisa/resources/file_interpreter/from_html.py
+++ b/src/maisa/resources/file_interpreter/from_html.py
@@ -6,7 +6,7 @@
import httpx
-from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
+from ..._types import Body, Query, Headers, NotGiven, FileTypes, not_given
from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
@@ -51,7 +51,7 @@ def create(
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> object:
"""
Interprets html file and returns a markdown file.
@@ -111,7 +111,7 @@ async def create(
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> object:
"""
Interprets html file and returns a markdown file.
diff --git a/src/maisa/resources/file_interpreter/from_image.py b/src/maisa/resources/file_interpreter/from_image.py
index 2d35cc0b..5686c97e 100644
--- a/src/maisa/resources/file_interpreter/from_image.py
+++ b/src/maisa/resources/file_interpreter/from_image.py
@@ -6,7 +6,7 @@
import httpx
-from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
+from ..._types import Body, Query, Headers, NotGiven, FileTypes, not_given
from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
@@ -51,7 +51,7 @@ def create(
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> object:
"""
Interprets an image file and returns a description of the image.
@@ -111,7 +111,7 @@ async def create(
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> object:
"""
Interprets an image file and returns a description of the image.
diff --git a/src/maisa/resources/file_interpreter/from_pdf.py b/src/maisa/resources/file_interpreter/from_pdf.py
index 3286c935..20191f10 100644
--- a/src/maisa/resources/file_interpreter/from_pdf.py
+++ b/src/maisa/resources/file_interpreter/from_pdf.py
@@ -6,7 +6,7 @@
import httpx
-from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes
+from ..._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given
from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
@@ -46,13 +46,13 @@ def create(
self,
*,
file: FileTypes,
- max_pages: Optional[int] | NotGiven = NOT_GIVEN,
+ max_pages: Optional[int] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> object:
"""
Transform PDF to Markdown
@@ -111,13 +111,13 @@ async def create(
self,
*,
file: FileTypes,
- max_pages: Optional[int] | NotGiven = NOT_GIVEN,
+ max_pages: Optional[int] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> object:
"""
Transform PDF to Markdown
diff --git a/src/maisa/resources/kpu.py b/src/maisa/resources/kpu.py
index e6970a12..e0c0f5de 100644
--- a/src/maisa/resources/kpu.py
+++ b/src/maisa/resources/kpu.py
@@ -8,7 +8,7 @@
import httpx
from ..types import kpu_run_params
-from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes, SequenceNotStr
+from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, SequenceNotStr, omit, not_given
from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
@@ -47,9 +47,9 @@ def run(
self,
*,
query: str,
- explain_steps: bool | NotGiven = NOT_GIVEN,
- retries: int | NotGiven = NOT_GIVEN,
- file: SequenceNotStr[FileTypes] | NotGiven = NOT_GIVEN,
+ explain_steps: bool | Omit = omit,
+ retries: int | Omit = omit,
+ file: SequenceNotStr[FileTypes] | Omit = omit,
reasoner_model: Optional[
Literal[
"gpt-4-turbo",
@@ -62,14 +62,14 @@ def run(
"openai/gpt-4-turbo",
]
]
- | NotGiven = NOT_GIVEN,
- reasoner_prompt: Optional[str] | NotGiven = NOT_GIVEN,
+ | Omit = omit,
+ reasoner_prompt: Optional[str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> object:
"""
Executes the KPU in sync, sending the response when the KPU execution is done.
@@ -154,9 +154,9 @@ async def run(
self,
*,
query: str,
- explain_steps: bool | NotGiven = NOT_GIVEN,
- retries: int | NotGiven = NOT_GIVEN,
- file: SequenceNotStr[FileTypes] | NotGiven = NOT_GIVEN,
+ explain_steps: bool | Omit = omit,
+ retries: int | Omit = omit,
+ file: SequenceNotStr[FileTypes] | Omit = omit,
reasoner_model: Optional[
Literal[
"gpt-4-turbo",
@@ -169,14 +169,14 @@ async def run(
"openai/gpt-4-turbo",
]
]
- | NotGiven = NOT_GIVEN,
- reasoner_prompt: Optional[str] | NotGiven = NOT_GIVEN,
+ | Omit = omit,
+ reasoner_prompt: Optional[str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> object:
"""
Executes the KPU in sync, sending the response when the KPU execution is done.
diff --git a/src/maisa/resources/models/embeddings.py b/src/maisa/resources/models/embeddings.py
index f56ea053..33c51c60 100644
--- a/src/maisa/resources/models/embeddings.py
+++ b/src/maisa/resources/models/embeddings.py
@@ -4,7 +4,7 @@
import httpx
-from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven, SequenceNotStr
+from ..._types import Body, Query, Headers, NotGiven, SequenceNotStr, not_given
from ..._utils import maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
@@ -50,7 +50,7 @@ def create(
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> Embeddings:
"""
Creates embeddings from pieces of text.
@@ -105,7 +105,7 @@ async def create(
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> Embeddings:
"""
Creates embeddings from pieces of text.
diff --git a/tests/test_transform.py b/tests/test_transform.py
index 83ab663a..32ca5ca8 100644
--- a/tests/test_transform.py
+++ b/tests/test_transform.py
@@ -8,7 +8,7 @@
import pytest
-from maisa._types import NOT_GIVEN, Base64FileInput
+from maisa._types import Base64FileInput, omit, not_given
from maisa._utils import (
PropertyInfo,
transform as _transform,
@@ -450,4 +450,11 @@ async def test_transform_skipping(use_async: bool) -> None:
@pytest.mark.asyncio
async def test_strips_notgiven(use_async: bool) -> None:
assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"}
- assert await transform({"foo_bar": NOT_GIVEN}, Foo1, use_async) == {}
+ assert await transform({"foo_bar": not_given}, Foo1, use_async) == {}
+
+
+@parametrize
+@pytest.mark.asyncio
+async def test_strips_omit(use_async: bool) -> None:
+ assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"}
+ assert await transform({"foo_bar": omit}, Foo1, use_async) == {}
From e355179123c014ed8cef32ac224a89a5e7532016 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 20 Sep 2025 03:45:42 +0000
Subject: [PATCH 135/200] chore: do not install brew dependencies in
./scripts/bootstrap by default
---
scripts/bootstrap | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/scripts/bootstrap b/scripts/bootstrap
index e84fe62c..b430fee3 100755
--- a/scripts/bootstrap
+++ b/scripts/bootstrap
@@ -4,10 +4,18 @@ set -e
cd "$(dirname "$0")/.."
-if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then
+if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then
brew bundle check >/dev/null 2>&1 || {
- echo "==> Installing Homebrew dependencies…"
- brew bundle
+ echo -n "==> Install Homebrew dependencies? (y/N): "
+ read -r response
+ case "$response" in
+ [yY][eE][sS]|[yY])
+ brew bundle
+ ;;
+ *)
+ ;;
+ esac
+ echo
}
fi
From 4dcaf540ad3b1d2bce32b24885b77daaf67a0ee2 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 11 Oct 2025 02:38:11 +0000
Subject: [PATCH 136/200] chore(internal): detect missing future annotations
with ruff
---
pyproject.toml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/pyproject.toml b/pyproject.toml
index c0a2f23a..02e47f10 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -224,6 +224,8 @@ select = [
"B",
# remove unused imports
"F401",
+ # check for missing future annotations
+ "FA102",
# bare except statements
"E722",
# unused arguments
@@ -246,6 +248,8 @@ unfixable = [
"T203",
]
+extend-safe-fixes = ["FA102"]
+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
"functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead"
From c632881aaf640e18337951f9a8ca0a7f219f45dc Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 18 Oct 2025 02:23:57 +0000
Subject: [PATCH 137/200] chore: bump `httpx-aiohttp` version to 0.1.9
---
pyproject.toml | 2 +-
requirements-dev.lock | 2 +-
requirements.lock | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 02e47f10..34aae564 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,7 +39,7 @@ Homepage = "https://github.com/maisaai/python-sdk"
Repository = "https://github.com/maisaai/python-sdk"
[project.optional-dependencies]
-aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"]
+aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"]
[tool.rye]
managed = true
diff --git a/requirements-dev.lock b/requirements-dev.lock
index f89cf419..19ad0058 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -56,7 +56,7 @@ httpx==0.28.1
# via httpx-aiohttp
# via maisa
# via respx
-httpx-aiohttp==0.1.8
+httpx-aiohttp==0.1.9
# via maisa
idna==3.4
# via anyio
diff --git a/requirements.lock b/requirements.lock
index cd52c1ce..d2cf9ef9 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -43,7 +43,7 @@ httpcore==1.0.9
httpx==0.28.1
# via httpx-aiohttp
# via maisa
-httpx-aiohttp==0.1.8
+httpx-aiohttp==0.1.9
# via maisa
idna==3.4
# via anyio
From 63670ce664ebc59df42945101b871968b9ae0a9c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 30 Oct 2025 02:53:45 +0000
Subject: [PATCH 138/200] fix(client): close streams without requiring full
consumption
---
src/maisa/_streaming.py | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/src/maisa/_streaming.py b/src/maisa/_streaming.py
index 4093e98c..044032c3 100644
--- a/src/maisa/_streaming.py
+++ b/src/maisa/_streaming.py
@@ -57,9 +57,8 @@ def __stream__(self) -> Iterator[_T]:
for sse in iterator:
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
- # Ensure the entire stream is consumed
- for _sse in iterator:
- ...
+ # As we might not fully consume the response stream, we need to close it explicitly
+ response.close()
def __enter__(self) -> Self:
return self
@@ -121,9 +120,8 @@ async def __stream__(self) -> AsyncIterator[_T]:
async for sse in iterator:
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
- # Ensure the entire stream is consumed
- async for _sse in iterator:
- ...
+ # As we might not fully consume the response stream, we need to close it explicitly
+ await response.aclose()
async def __aenter__(self) -> Self:
return self
From d913cd6fd031903cd15098974af2b16c86bd52ac Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 31 Oct 2025 04:12:33 +0000
Subject: [PATCH 139/200] chore(internal/tests): avoid race condition with
implicit client cleanup
---
tests/test_client.py | 362 +++++++++++++++++++++++--------------------
1 file changed, 198 insertions(+), 164 deletions(-)
diff --git a/tests/test_client.py b/tests/test_client.py
index 320919e0..ba5f66b4 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -59,51 +59,49 @@ def _get_open_connections(client: Maisa | AsyncMaisa) -> int:
class TestMaisa:
- client = Maisa(base_url=base_url, api_key=api_key, _strict_response_validation=True)
-
@pytest.mark.respx(base_url=base_url)
- def test_raw_response(self, respx_mock: MockRouter) -> None:
+ def test_raw_response(self, respx_mock: MockRouter, client: Maisa) -> None:
respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = self.client.post("/foo", cast_to=httpx.Response)
+ response = client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
@pytest.mark.respx(base_url=base_url)
- def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
+ def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Maisa) -> None:
respx_mock.post("/foo").mock(
return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
)
- response = self.client.post("/foo", cast_to=httpx.Response)
+ response = client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
- def test_copy(self) -> None:
- copied = self.client.copy()
- assert id(copied) != id(self.client)
+ def test_copy(self, client: Maisa) -> None:
+ copied = client.copy()
+ assert id(copied) != id(client)
- copied = self.client.copy(api_key="another My API Key")
+ copied = client.copy(api_key="another My API Key")
assert copied.api_key == "another My API Key"
- assert self.client.api_key == "My API Key"
+ assert client.api_key == "My API Key"
- def test_copy_default_options(self) -> None:
+ def test_copy_default_options(self, client: Maisa) -> None:
# options that have a default are overridden correctly
- copied = self.client.copy(max_retries=7)
+ copied = client.copy(max_retries=7)
assert copied.max_retries == 7
- assert self.client.max_retries == 2
+ assert client.max_retries == 2
copied2 = copied.copy(max_retries=6)
assert copied2.max_retries == 6
assert copied.max_retries == 7
# timeout
- assert isinstance(self.client.timeout, httpx.Timeout)
- copied = self.client.copy(timeout=None)
+ assert isinstance(client.timeout, httpx.Timeout)
+ copied = client.copy(timeout=None)
assert copied.timeout is None
- assert isinstance(self.client.timeout, httpx.Timeout)
+ assert isinstance(client.timeout, httpx.Timeout)
def test_copy_default_headers(self) -> None:
client = Maisa(
@@ -138,6 +136,7 @@ def test_copy_default_headers(self) -> None:
match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
):
client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
+ client.close()
def test_copy_default_query(self) -> None:
client = Maisa(
@@ -175,13 +174,15 @@ def test_copy_default_query(self) -> None:
):
client.copy(set_default_query={}, default_query={"foo": "Bar"})
- def test_copy_signature(self) -> None:
+ client.close()
+
+ def test_copy_signature(self, client: Maisa) -> None:
# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
init_signature = inspect.signature(
# mypy doesn't like that we access the `__init__` property.
- self.client.__init__, # type: ignore[misc]
+ client.__init__, # type: ignore[misc]
)
- copy_signature = inspect.signature(self.client.copy)
+ copy_signature = inspect.signature(client.copy)
exclude_params = {"transport", "proxies", "_strict_response_validation"}
for name in init_signature.parameters.keys():
@@ -192,12 +193,12 @@ def test_copy_signature(self) -> None:
assert copy_param is not None, f"copy() signature is missing the {name} param"
@pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
- def test_copy_build_request(self) -> None:
+ def test_copy_build_request(self, client: Maisa) -> None:
options = FinalRequestOptions(method="get", url="/foo")
def build_request(options: FinalRequestOptions) -> None:
- client = self.client.copy()
- client._build_request(options)
+ client_copy = client.copy()
+ client_copy._build_request(options)
# ensure that the machinery is warmed up before tracing starts.
build_request(options)
@@ -254,14 +255,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic
print(frame)
raise AssertionError()
- def test_request_timeout(self) -> None:
- request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
+ def test_request_timeout(self, client: Maisa) -> None:
+ request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
- request = self.client._build_request(
- FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
- )
+ request = client._build_request(FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)))
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(100.0)
@@ -272,6 +271,8 @@ def test_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(0)
+ client.close()
+
def test_http_client_timeout_option(self) -> None:
# custom timeout given to the httpx client should be used
with httpx.Client(timeout=None) as http_client:
@@ -283,6 +284,8 @@ def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(None)
+ client.close()
+
# no timeout given to the httpx client should not use the httpx default
with httpx.Client() as http_client:
client = Maisa(
@@ -293,6 +296,8 @@ def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
+ client.close()
+
# explicitly passing the default timeout currently results in it being ignored
with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
client = Maisa(
@@ -303,6 +308,8 @@ def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT # our default
+ client.close()
+
async def test_invalid_http_client(self) -> None:
with pytest.raises(TypeError, match="Invalid `http_client` arg"):
async with httpx.AsyncClient() as http_client:
@@ -314,14 +321,14 @@ async def test_invalid_http_client(self) -> None:
)
def test_default_headers_option(self) -> None:
- client = Maisa(
+ test_client = Maisa(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
)
- request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
+ request = test_client._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "bar"
assert request.headers.get("x-stainless-lang") == "python"
- client2 = Maisa(
+ test_client2 = Maisa(
base_url=base_url,
api_key=api_key,
_strict_response_validation=True,
@@ -330,10 +337,13 @@ def test_default_headers_option(self) -> None:
"X-Stainless-Lang": "my-overriding-header",
},
)
- request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
+ request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "stainless"
assert request.headers.get("x-stainless-lang") == "my-overriding-header"
+ test_client.close()
+ test_client2.close()
+
def test_default_query_option(self) -> None:
client = Maisa(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
@@ -352,8 +362,10 @@ def test_default_query_option(self) -> None:
url = httpx.URL(request.url)
assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
- def test_request_extra_json(self) -> None:
- request = self.client._build_request(
+ client.close()
+
+ def test_request_extra_json(self, client: Maisa) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -364,7 +376,7 @@ def test_request_extra_json(self) -> None:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": False}
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -375,7 +387,7 @@ def test_request_extra_json(self) -> None:
assert data == {"baz": False}
# `extra_json` takes priority over `json_data` when keys clash
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -386,8 +398,8 @@ def test_request_extra_json(self) -> None:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": None}
- def test_request_extra_headers(self) -> None:
- request = self.client._build_request(
+ def test_request_extra_headers(self, client: Maisa) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -397,7 +409,7 @@ def test_request_extra_headers(self) -> None:
assert request.headers.get("X-Foo") == "Foo"
# `extra_headers` takes priority over `default_headers` when keys clash
- request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
+ request = client.with_options(default_headers={"X-Bar": "true"})._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -408,8 +420,8 @@ def test_request_extra_headers(self) -> None:
)
assert request.headers.get("X-Bar") == "false"
- def test_request_extra_query(self) -> None:
- request = self.client._build_request(
+ def test_request_extra_query(self, client: Maisa) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -422,7 +434,7 @@ def test_request_extra_query(self) -> None:
assert params == {"my_query_param": "Foo"}
# if both `query` and `extra_query` are given, they are merged
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -436,7 +448,7 @@ def test_request_extra_query(self) -> None:
assert params == {"bar": "1", "foo": "2"}
# `extra_query` takes priority over `query` when keys clash
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -479,7 +491,7 @@ def test_multipart_repeating_array(self, client: Maisa) -> None:
]
@pytest.mark.respx(base_url=base_url)
- def test_basic_union_response(self, respx_mock: MockRouter) -> None:
+ def test_basic_union_response(self, respx_mock: MockRouter, client: Maisa) -> None:
class Model1(BaseModel):
name: str
@@ -488,12 +500,12 @@ class Model2(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
@pytest.mark.respx(base_url=base_url)
- def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
+ def test_union_response_different_types(self, respx_mock: MockRouter, client: Maisa) -> None:
"""Union of objects with the same field name using a different type"""
class Model1(BaseModel):
@@ -504,18 +516,18 @@ class Model2(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
- response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model1)
assert response.foo == 1
@pytest.mark.respx(base_url=base_url)
- def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
+ def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: Maisa) -> None:
"""
Response that sets Content-Type to something other than application/json but returns json data
"""
@@ -531,7 +543,7 @@ class Model(BaseModel):
)
)
- response = self.client.get("/foo", cast_to=Model)
+ response = client.get("/foo", cast_to=Model)
assert isinstance(response, Model)
assert response.foo == 2
@@ -543,6 +555,8 @@ def test_base_url_setter(self) -> None:
assert client.base_url == "https://example.com/from_setter/"
+ client.close()
+
def test_base_url_env(self) -> None:
with update_env(MAISA_BASE_URL="http://localhost:5000/from/env"):
client = Maisa(api_key=api_key, _strict_response_validation=True)
@@ -570,6 +584,7 @@ def test_base_url_trailing_slash(self, client: Maisa) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ client.close()
@pytest.mark.parametrize(
"client",
@@ -593,6 +608,7 @@ def test_base_url_no_trailing_slash(self, client: Maisa) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ client.close()
@pytest.mark.parametrize(
"client",
@@ -616,35 +632,36 @@ def test_absolute_request_url(self, client: Maisa) -> None:
),
)
assert request.url == "https://myapi.com/foo"
+ client.close()
def test_copied_client_does_not_close_http(self) -> None:
- client = Maisa(base_url=base_url, api_key=api_key, _strict_response_validation=True)
- assert not client.is_closed()
+ test_client = Maisa(base_url=base_url, api_key=api_key, _strict_response_validation=True)
+ assert not test_client.is_closed()
- copied = client.copy()
- assert copied is not client
+ copied = test_client.copy()
+ assert copied is not test_client
del copied
- assert not client.is_closed()
+ assert not test_client.is_closed()
def test_client_context_manager(self) -> None:
- client = Maisa(base_url=base_url, api_key=api_key, _strict_response_validation=True)
- with client as c2:
- assert c2 is client
+ test_client = Maisa(base_url=base_url, api_key=api_key, _strict_response_validation=True)
+ with test_client as c2:
+ assert c2 is test_client
assert not c2.is_closed()
- assert not client.is_closed()
- assert client.is_closed()
+ assert not test_client.is_closed()
+ assert test_client.is_closed()
@pytest.mark.respx(base_url=base_url)
- def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
+ def test_client_response_validation_error(self, respx_mock: MockRouter, client: Maisa) -> None:
class Model(BaseModel):
foo: str
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
with pytest.raises(APIResponseValidationError) as exc:
- self.client.get("/foo", cast_to=Model)
+ client.get("/foo", cast_to=Model)
assert isinstance(exc.value.__cause__, ValidationError)
@@ -664,11 +681,14 @@ class Model(BaseModel):
with pytest.raises(APIResponseValidationError):
strict_client.get("/foo", cast_to=Model)
- client = Maisa(base_url=base_url, api_key=api_key, _strict_response_validation=False)
+ non_strict_client = Maisa(base_url=base_url, api_key=api_key, _strict_response_validation=False)
- response = client.get("/foo", cast_to=Model)
+ response = non_strict_client.get("/foo", cast_to=Model)
assert isinstance(response, str) # type: ignore[unreachable]
+ strict_client.close()
+ non_strict_client.close()
+
@pytest.mark.parametrize(
"remaining_retries,retry_after,timeout",
[
@@ -691,9 +711,9 @@ class Model(BaseModel):
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
- def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
- client = Maisa(base_url=base_url, api_key=api_key, _strict_response_validation=True)
-
+ def test_parse_retry_after_header(
+ self, remaining_retries: int, retry_after: str, timeout: float, client: Maisa
+ ) -> None:
headers = httpx.Headers({"retry-after": retry_after})
options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
@@ -707,7 +727,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien
with pytest.raises(APITimeoutError):
client.capabilities.with_streaming_response.summarize(text="Example long text...").__enter__()
- assert _get_open_connections(self.client) == 0
+ assert _get_open_connections(client) == 0
@mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
@@ -716,7 +736,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client
with pytest.raises(APIStatusError):
client.capabilities.with_streaming_response.summarize(text="Example long text...").__enter__()
- assert _get_open_connections(self.client) == 0
+ assert _get_open_connections(client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@@ -820,83 +840,77 @@ def test_default_client_creation(self) -> None:
)
@pytest.mark.respx(base_url=base_url)
- def test_follow_redirects(self, respx_mock: MockRouter) -> None:
+ def test_follow_redirects(self, respx_mock: MockRouter, client: Maisa) -> None:
# Test that the default follow_redirects=True allows following redirects
respx_mock.post("/redirect").mock(
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
)
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
- response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
+ response = client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
assert response.status_code == 200
assert response.json() == {"status": "ok"}
@pytest.mark.respx(base_url=base_url)
- def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
+ def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Maisa) -> None:
# Test that follow_redirects=False prevents following redirects
respx_mock.post("/redirect").mock(
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
)
with pytest.raises(APIStatusError) as exc_info:
- self.client.post(
- "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
- )
+ client.post("/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response)
assert exc_info.value.response.status_code == 302
assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
class TestAsyncMaisa:
- client = AsyncMaisa(base_url=base_url, api_key=api_key, _strict_response_validation=True)
-
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
- async def test_raw_response(self, respx_mock: MockRouter) -> None:
+ async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncMaisa) -> None:
respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = await self.client.post("/foo", cast_to=httpx.Response)
+ response = await async_client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
- async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
+ async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncMaisa) -> None:
respx_mock.post("/foo").mock(
return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
)
- response = await self.client.post("/foo", cast_to=httpx.Response)
+ response = await async_client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
- def test_copy(self) -> None:
- copied = self.client.copy()
- assert id(copied) != id(self.client)
+ def test_copy(self, async_client: AsyncMaisa) -> None:
+ copied = async_client.copy()
+ assert id(copied) != id(async_client)
- copied = self.client.copy(api_key="another My API Key")
+ copied = async_client.copy(api_key="another My API Key")
assert copied.api_key == "another My API Key"
- assert self.client.api_key == "My API Key"
+ assert async_client.api_key == "My API Key"
- def test_copy_default_options(self) -> None:
+ def test_copy_default_options(self, async_client: AsyncMaisa) -> None:
# options that have a default are overridden correctly
- copied = self.client.copy(max_retries=7)
+ copied = async_client.copy(max_retries=7)
assert copied.max_retries == 7
- assert self.client.max_retries == 2
+ assert async_client.max_retries == 2
copied2 = copied.copy(max_retries=6)
assert copied2.max_retries == 6
assert copied.max_retries == 7
# timeout
- assert isinstance(self.client.timeout, httpx.Timeout)
- copied = self.client.copy(timeout=None)
+ assert isinstance(async_client.timeout, httpx.Timeout)
+ copied = async_client.copy(timeout=None)
assert copied.timeout is None
- assert isinstance(self.client.timeout, httpx.Timeout)
+ assert isinstance(async_client.timeout, httpx.Timeout)
- def test_copy_default_headers(self) -> None:
+ async def test_copy_default_headers(self) -> None:
client = AsyncMaisa(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
)
@@ -929,8 +943,9 @@ def test_copy_default_headers(self) -> None:
match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
):
client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
+ await client.close()
- def test_copy_default_query(self) -> None:
+ async def test_copy_default_query(self) -> None:
client = AsyncMaisa(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
)
@@ -966,13 +981,15 @@ def test_copy_default_query(self) -> None:
):
client.copy(set_default_query={}, default_query={"foo": "Bar"})
- def test_copy_signature(self) -> None:
+ await client.close()
+
+ def test_copy_signature(self, async_client: AsyncMaisa) -> None:
# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
init_signature = inspect.signature(
# mypy doesn't like that we access the `__init__` property.
- self.client.__init__, # type: ignore[misc]
+ async_client.__init__, # type: ignore[misc]
)
- copy_signature = inspect.signature(self.client.copy)
+ copy_signature = inspect.signature(async_client.copy)
exclude_params = {"transport", "proxies", "_strict_response_validation"}
for name in init_signature.parameters.keys():
@@ -983,12 +1000,12 @@ def test_copy_signature(self) -> None:
assert copy_param is not None, f"copy() signature is missing the {name} param"
@pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
- def test_copy_build_request(self) -> None:
+ def test_copy_build_request(self, async_client: AsyncMaisa) -> None:
options = FinalRequestOptions(method="get", url="/foo")
def build_request(options: FinalRequestOptions) -> None:
- client = self.client.copy()
- client._build_request(options)
+ client_copy = async_client.copy()
+ client_copy._build_request(options)
# ensure that the machinery is warmed up before tracing starts.
build_request(options)
@@ -1045,12 +1062,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic
print(frame)
raise AssertionError()
- async def test_request_timeout(self) -> None:
- request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
+ async def test_request_timeout(self, async_client: AsyncMaisa) -> None:
+ request = async_client._build_request(FinalRequestOptions(method="get", url="/foo"))
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
- request = self.client._build_request(
+ request = async_client._build_request(
FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
)
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
@@ -1065,6 +1082,8 @@ async def test_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(0)
+ await client.close()
+
async def test_http_client_timeout_option(self) -> None:
# custom timeout given to the httpx client should be used
async with httpx.AsyncClient(timeout=None) as http_client:
@@ -1076,6 +1095,8 @@ async def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(None)
+ await client.close()
+
# no timeout given to the httpx client should not use the httpx default
async with httpx.AsyncClient() as http_client:
client = AsyncMaisa(
@@ -1086,6 +1107,8 @@ async def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
+ await client.close()
+
# explicitly passing the default timeout currently results in it being ignored
async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
client = AsyncMaisa(
@@ -1096,6 +1119,8 @@ async def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT # our default
+ await client.close()
+
def test_invalid_http_client(self) -> None:
with pytest.raises(TypeError, match="Invalid `http_client` arg"):
with httpx.Client() as http_client:
@@ -1106,15 +1131,15 @@ def test_invalid_http_client(self) -> None:
http_client=cast(Any, http_client),
)
- def test_default_headers_option(self) -> None:
- client = AsyncMaisa(
+ async def test_default_headers_option(self) -> None:
+ test_client = AsyncMaisa(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
)
- request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
+ request = test_client._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "bar"
assert request.headers.get("x-stainless-lang") == "python"
- client2 = AsyncMaisa(
+ test_client2 = AsyncMaisa(
base_url=base_url,
api_key=api_key,
_strict_response_validation=True,
@@ -1123,11 +1148,14 @@ def test_default_headers_option(self) -> None:
"X-Stainless-Lang": "my-overriding-header",
},
)
- request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
+ request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "stainless"
assert request.headers.get("x-stainless-lang") == "my-overriding-header"
- def test_default_query_option(self) -> None:
+ await test_client.close()
+ await test_client2.close()
+
+ async def test_default_query_option(self) -> None:
client = AsyncMaisa(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
)
@@ -1145,8 +1173,10 @@ def test_default_query_option(self) -> None:
url = httpx.URL(request.url)
assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
- def test_request_extra_json(self) -> None:
- request = self.client._build_request(
+ await client.close()
+
+ def test_request_extra_json(self, client: Maisa) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1157,7 +1187,7 @@ def test_request_extra_json(self) -> None:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": False}
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1168,7 +1198,7 @@ def test_request_extra_json(self) -> None:
assert data == {"baz": False}
# `extra_json` takes priority over `json_data` when keys clash
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1179,8 +1209,8 @@ def test_request_extra_json(self) -> None:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": None}
- def test_request_extra_headers(self) -> None:
- request = self.client._build_request(
+ def test_request_extra_headers(self, client: Maisa) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1190,7 +1220,7 @@ def test_request_extra_headers(self) -> None:
assert request.headers.get("X-Foo") == "Foo"
# `extra_headers` takes priority over `default_headers` when keys clash
- request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
+ request = client.with_options(default_headers={"X-Bar": "true"})._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1201,8 +1231,8 @@ def test_request_extra_headers(self) -> None:
)
assert request.headers.get("X-Bar") == "false"
- def test_request_extra_query(self) -> None:
- request = self.client._build_request(
+ def test_request_extra_query(self, client: Maisa) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1215,7 +1245,7 @@ def test_request_extra_query(self) -> None:
assert params == {"my_query_param": "Foo"}
# if both `query` and `extra_query` are given, they are merged
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1229,7 +1259,7 @@ def test_request_extra_query(self) -> None:
assert params == {"bar": "1", "foo": "2"}
# `extra_query` takes priority over `query` when keys clash
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1272,7 +1302,7 @@ def test_multipart_repeating_array(self, async_client: AsyncMaisa) -> None:
]
@pytest.mark.respx(base_url=base_url)
- async def test_basic_union_response(self, respx_mock: MockRouter) -> None:
+ async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncMaisa) -> None:
class Model1(BaseModel):
name: str
@@ -1281,12 +1311,12 @@ class Model2(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
@pytest.mark.respx(base_url=base_url)
- async def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
+ async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncMaisa) -> None:
"""Union of objects with the same field name using a different type"""
class Model1(BaseModel):
@@ -1297,18 +1327,20 @@ class Model2(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
- response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model1)
assert response.foo == 1
@pytest.mark.respx(base_url=base_url)
- async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
+ async def test_non_application_json_content_type_for_json_data(
+ self, respx_mock: MockRouter, async_client: AsyncMaisa
+ ) -> None:
"""
Response that sets Content-Type to something other than application/json but returns json data
"""
@@ -1324,11 +1356,11 @@ class Model(BaseModel):
)
)
- response = await self.client.get("/foo", cast_to=Model)
+ response = await async_client.get("/foo", cast_to=Model)
assert isinstance(response, Model)
assert response.foo == 2
- def test_base_url_setter(self) -> None:
+ async def test_base_url_setter(self) -> None:
client = AsyncMaisa(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True)
assert client.base_url == "https://example.com/from_init/"
@@ -1336,7 +1368,9 @@ def test_base_url_setter(self) -> None:
assert client.base_url == "https://example.com/from_setter/"
- def test_base_url_env(self) -> None:
+ await client.close()
+
+ async def test_base_url_env(self) -> None:
with update_env(MAISA_BASE_URL="http://localhost:5000/from/env"):
client = AsyncMaisa(api_key=api_key, _strict_response_validation=True)
assert client.base_url == "http://localhost:5000/from/env/"
@@ -1356,7 +1390,7 @@ def test_base_url_env(self) -> None:
],
ids=["standard", "custom http client"],
)
- def test_base_url_trailing_slash(self, client: AsyncMaisa) -> None:
+ async def test_base_url_trailing_slash(self, client: AsyncMaisa) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
@@ -1365,6 +1399,7 @@ def test_base_url_trailing_slash(self, client: AsyncMaisa) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ await client.close()
@pytest.mark.parametrize(
"client",
@@ -1381,7 +1416,7 @@ def test_base_url_trailing_slash(self, client: AsyncMaisa) -> None:
],
ids=["standard", "custom http client"],
)
- def test_base_url_no_trailing_slash(self, client: AsyncMaisa) -> None:
+ async def test_base_url_no_trailing_slash(self, client: AsyncMaisa) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
@@ -1390,6 +1425,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncMaisa) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ await client.close()
@pytest.mark.parametrize(
"client",
@@ -1406,7 +1442,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncMaisa) -> None:
],
ids=["standard", "custom http client"],
)
- def test_absolute_request_url(self, client: AsyncMaisa) -> None:
+ async def test_absolute_request_url(self, client: AsyncMaisa) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
@@ -1415,37 +1451,37 @@ def test_absolute_request_url(self, client: AsyncMaisa) -> None:
),
)
assert request.url == "https://myapi.com/foo"
+ await client.close()
async def test_copied_client_does_not_close_http(self) -> None:
- client = AsyncMaisa(base_url=base_url, api_key=api_key, _strict_response_validation=True)
- assert not client.is_closed()
+ test_client = AsyncMaisa(base_url=base_url, api_key=api_key, _strict_response_validation=True)
+ assert not test_client.is_closed()
- copied = client.copy()
- assert copied is not client
+ copied = test_client.copy()
+ assert copied is not test_client
del copied
await asyncio.sleep(0.2)
- assert not client.is_closed()
+ assert not test_client.is_closed()
async def test_client_context_manager(self) -> None:
- client = AsyncMaisa(base_url=base_url, api_key=api_key, _strict_response_validation=True)
- async with client as c2:
- assert c2 is client
+ test_client = AsyncMaisa(base_url=base_url, api_key=api_key, _strict_response_validation=True)
+ async with test_client as c2:
+ assert c2 is test_client
assert not c2.is_closed()
- assert not client.is_closed()
- assert client.is_closed()
+ assert not test_client.is_closed()
+ assert test_client.is_closed()
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
- async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
+ async def test_client_response_validation_error(self, respx_mock: MockRouter, async_client: AsyncMaisa) -> None:
class Model(BaseModel):
foo: str
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
with pytest.raises(APIResponseValidationError) as exc:
- await self.client.get("/foo", cast_to=Model)
+ await async_client.get("/foo", cast_to=Model)
assert isinstance(exc.value.__cause__, ValidationError)
@@ -1456,7 +1492,6 @@ async def test_client_max_retries_validation(self) -> None:
)
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
class Model(BaseModel):
name: str
@@ -1468,11 +1503,14 @@ class Model(BaseModel):
with pytest.raises(APIResponseValidationError):
await strict_client.get("/foo", cast_to=Model)
- client = AsyncMaisa(base_url=base_url, api_key=api_key, _strict_response_validation=False)
+ non_strict_client = AsyncMaisa(base_url=base_url, api_key=api_key, _strict_response_validation=False)
- response = await client.get("/foo", cast_to=Model)
+ response = await non_strict_client.get("/foo", cast_to=Model)
assert isinstance(response, str) # type: ignore[unreachable]
+ await strict_client.close()
+ await non_strict_client.close()
+
@pytest.mark.parametrize(
"remaining_retries,retry_after,timeout",
[
@@ -1495,13 +1533,12 @@ class Model(BaseModel):
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
- @pytest.mark.asyncio
- async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
- client = AsyncMaisa(base_url=base_url, api_key=api_key, _strict_response_validation=True)
-
+ async def test_parse_retry_after_header(
+ self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncMaisa
+ ) -> None:
headers = httpx.Headers({"retry-after": retry_after})
options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
- calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
+ calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers)
assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
@mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@@ -1512,7 +1549,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter,
with pytest.raises(APITimeoutError):
await async_client.capabilities.with_streaming_response.summarize(text="Example long text...").__aenter__()
- assert _get_open_connections(self.client) == 0
+ assert _get_open_connections(async_client) == 0
@mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
@@ -1521,12 +1558,11 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter,
with pytest.raises(APIStatusError):
await async_client.capabilities.with_streaming_response.summarize(text="Example long text...").__aenter__()
- assert _get_open_connections(self.client) == 0
+ assert _get_open_connections(async_client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
@pytest.mark.parametrize("failure_mode", ["status", "exception"])
async def test_retries_taken(
self,
@@ -1558,7 +1594,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
async def test_omit_retry_count_header(
self, async_client: AsyncMaisa, failures_before_success: int, respx_mock: MockRouter
) -> None:
@@ -1584,7 +1619,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("maisa._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
async def test_overwrite_retry_count_header(
self, async_client: AsyncMaisa, failures_before_success: int, respx_mock: MockRouter
) -> None:
@@ -1634,26 +1668,26 @@ async def test_default_client_creation(self) -> None:
)
@pytest.mark.respx(base_url=base_url)
- async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
+ async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncMaisa) -> None:
# Test that the default follow_redirects=True allows following redirects
respx_mock.post("/redirect").mock(
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
)
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
- response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
+ response = await async_client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
assert response.status_code == 200
assert response.json() == {"status": "ok"}
@pytest.mark.respx(base_url=base_url)
- async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
+ async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncMaisa) -> None:
# Test that follow_redirects=False prevents following redirects
respx_mock.post("/redirect").mock(
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
)
with pytest.raises(APIStatusError) as exc_info:
- await self.client.post(
+ await async_client.post(
"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
)
From 359a8c23e2cbde6f5f2a255a09125f2043ba278c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 4 Nov 2025 06:08:09 +0000
Subject: [PATCH 140/200] chore(internal): grammar fix (it's -> its)
---
src/maisa/_utils/_utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/maisa/_utils/_utils.py b/src/maisa/_utils/_utils.py
index 50d59269..eec7f4a1 100644
--- a/src/maisa/_utils/_utils.py
+++ b/src/maisa/_utils/_utils.py
@@ -133,7 +133,7 @@ def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]:
# Type safe methods for narrowing types with TypeVars.
# The default narrowing for isinstance(obj, dict) is dict[unknown, unknown],
# however this cause Pyright to rightfully report errors. As we know we don't
-# care about the contained types we can safely use `object` in it's place.
+# care about the contained types we can safely use `object` in its place.
#
# There are two separate functions defined, `is_*` and `is_*_t` for different use cases.
# `is_*` is for when you're dealing with an unknown input
From a0fdbfdf095c289a505e0a9f0f5005556b9dea6f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 11 Nov 2025 05:58:34 +0000
Subject: [PATCH 141/200] chore(package): drop Python 3.8 support
---
README.md | 4 ++--
pyproject.toml | 5 ++---
src/maisa/_utils/_sync.py | 34 +++-------------------------------
3 files changed, 7 insertions(+), 36 deletions(-)
diff --git a/README.md b/README.md
index abfefd34..6449319e 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[)](https://pypi.org/project/maisa/)
-The Maisa Python library provides convenient access to the Maisa REST API from any Python 3.8+
+The Maisa Python library provides convenient access to the Maisa REST API from any Python 3.9+
application. The library includes type definitions for all request params and response fields,
and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).
@@ -390,7 +390,7 @@ print(maisa.__version__)
## Requirements
-Python 3.8 or higher.
+Python 3.9 or higher.
## Contributing
diff --git a/pyproject.toml b/pyproject.toml
index 34aae564..5f9b8ef6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,11 +15,10 @@ dependencies = [
"distro>=1.7.0, <2",
"sniffio",
]
-requires-python = ">= 3.8"
+requires-python = ">= 3.9"
classifiers = [
"Typing :: Typed",
"Intended Audience :: Developers",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
@@ -141,7 +140,7 @@ filterwarnings = [
# there are a couple of flags that are still disabled by
# default in strict mode as they are experimental and niche.
typeCheckingMode = "strict"
-pythonVersion = "3.8"
+pythonVersion = "3.9"
exclude = [
"_dev",
diff --git a/src/maisa/_utils/_sync.py b/src/maisa/_utils/_sync.py
index ad7ec71b..f6027c18 100644
--- a/src/maisa/_utils/_sync.py
+++ b/src/maisa/_utils/_sync.py
@@ -1,10 +1,8 @@
from __future__ import annotations
-import sys
import asyncio
import functools
-import contextvars
-from typing import Any, TypeVar, Callable, Awaitable
+from typing import TypeVar, Callable, Awaitable
from typing_extensions import ParamSpec
import anyio
@@ -15,34 +13,11 @@
T_ParamSpec = ParamSpec("T_ParamSpec")
-if sys.version_info >= (3, 9):
- _asyncio_to_thread = asyncio.to_thread
-else:
- # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
- # for Python 3.8 support
- async def _asyncio_to_thread(
- func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
- ) -> Any:
- """Asynchronously run function *func* in a separate thread.
-
- Any *args and **kwargs supplied for this function are directly passed
- to *func*. Also, the current :class:`contextvars.Context` is propagated,
- allowing context variables from the main thread to be accessed in the
- separate thread.
-
- Returns a coroutine that can be awaited to get the eventual result of *func*.
- """
- loop = asyncio.events.get_running_loop()
- ctx = contextvars.copy_context()
- func_call = functools.partial(ctx.run, func, *args, **kwargs)
- return await loop.run_in_executor(None, func_call)
-
-
async def to_thread(
func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
) -> T_Retval:
if sniffio.current_async_library() == "asyncio":
- return await _asyncio_to_thread(func, *args, **kwargs)
+ return await asyncio.to_thread(func, *args, **kwargs)
return await anyio.to_thread.run_sync(
functools.partial(func, *args, **kwargs),
@@ -53,10 +28,7 @@ async def to_thread(
def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
"""
Take a blocking function and create an async one that receives the same
- positional and keyword arguments. For python version 3.9 and above, it uses
- asyncio.to_thread to run the function in a separate thread. For python version
- 3.8, it uses locally defined copy of the asyncio.to_thread function which was
- introduced in python 3.9.
+ positional and keyword arguments.
Usage:
From 988f89c86baafbfeb4d718b01e3af07cc239a458 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 11 Nov 2025 05:59:07 +0000
Subject: [PATCH 142/200] fix: compat with Python 3.14
---
src/maisa/_models.py | 11 ++++++++---
tests/test_models.py | 8 ++++----
2 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index 6a3cd1d2..fcec2cf9 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -2,6 +2,7 @@
import os
import inspect
+import weakref
from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
from datetime import date, datetime
from typing_extensions import (
@@ -573,6 +574,9 @@ class CachedDiscriminatorType(Protocol):
__discriminator__: DiscriminatorDetails
+DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary()
+
+
class DiscriminatorDetails:
field_name: str
"""The name of the discriminator field in the variant class, e.g.
@@ -615,8 +619,9 @@ def __init__(
def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None:
- if isinstance(union, CachedDiscriminatorType):
- return union.__discriminator__
+ cached = DISCRIMINATOR_CACHE.get(union)
+ if cached is not None:
+ return cached
discriminator_field_name: str | None = None
@@ -669,7 +674,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
discriminator_field=discriminator_field_name,
discriminator_alias=discriminator_alias,
)
- cast(CachedDiscriminatorType, union).__discriminator__ = details
+ DISCRIMINATOR_CACHE.setdefault(union, details)
return details
diff --git a/tests/test_models.py b/tests/test_models.py
index 630f6e0e..855edf2c 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -9,7 +9,7 @@
from maisa._utils import PropertyInfo
from maisa._compat import PYDANTIC_V1, parse_obj, model_dump, model_json
-from maisa._models import BaseModel, construct_type
+from maisa._models import DISCRIMINATOR_CACHE, BaseModel, construct_type
class BasicModel(BaseModel):
@@ -809,7 +809,7 @@ class B(BaseModel):
UnionType = cast(Any, Union[A, B])
- assert not hasattr(UnionType, "__discriminator__")
+ assert not DISCRIMINATOR_CACHE.get(UnionType)
m = construct_type(
value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")])
@@ -818,7 +818,7 @@ class B(BaseModel):
assert m.type == "b"
assert m.data == "foo" # type: ignore[comparison-overlap]
- discriminator = UnionType.__discriminator__
+ discriminator = DISCRIMINATOR_CACHE.get(UnionType)
assert discriminator is not None
m = construct_type(
@@ -830,7 +830,7 @@ class B(BaseModel):
# if the discriminator details object stays the same between invocations then
# we hit the cache
- assert UnionType.__discriminator__ is discriminator
+ assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator
@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1")
From 7d0d7e45141ce9c33de4f842ca40fd8634af0cc5 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 12 Nov 2025 05:33:46 +0000
Subject: [PATCH 143/200] fix(compat): update signatures of `model_dump` and
`model_dump_json` for Pydantic v1
---
src/maisa/_models.py | 41 +++++++++++++++++++++++++++++------------
1 file changed, 29 insertions(+), 12 deletions(-)
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index fcec2cf9..ca9500b2 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -257,15 +257,16 @@ def model_dump(
mode: Literal["json", "python"] | str = "python",
include: IncEx | None = None,
exclude: IncEx | None = None,
+ context: Any | None = None,
by_alias: bool | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
+ exclude_computed_fields: bool = False,
round_trip: bool = False,
warnings: bool | Literal["none", "warn", "error"] = True,
- context: dict[str, Any] | None = None,
- serialize_as_any: bool = False,
fallback: Callable[[Any], Any] | None = None,
+ serialize_as_any: bool = False,
) -> dict[str, Any]:
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump
@@ -273,16 +274,24 @@ def model_dump(
Args:
mode: The mode in which `to_python` should run.
- If mode is 'json', the dictionary will only contain JSON serializable types.
- If mode is 'python', the dictionary may contain any Python objects.
- include: A list of fields to include in the output.
- exclude: A list of fields to exclude from the output.
+ If mode is 'json', the output will only contain JSON serializable types.
+ If mode is 'python', the output may contain non-JSON-serializable Python objects.
+ include: A set of fields to include in the output.
+ exclude: A set of fields to exclude from the output.
+ context: Additional context to pass to the serializer.
by_alias: Whether to use the field's alias in the dictionary key if defined.
- exclude_unset: Whether to exclude fields that are unset or None from the output.
- exclude_defaults: Whether to exclude fields that are set to their default value from the output.
- exclude_none: Whether to exclude fields that have a value of `None` from the output.
- round_trip: Whether to enable serialization and deserialization round-trip support.
- warnings: Whether to log warnings when invalid fields are encountered.
+ exclude_unset: Whether to exclude fields that have not been explicitly set.
+ exclude_defaults: Whether to exclude fields that are set to their default value.
+ exclude_none: Whether to exclude fields that have a value of `None`.
+ exclude_computed_fields: Whether to exclude computed fields.
+ While this can be useful for round-tripping, it is usually recommended to use the dedicated
+ `round_trip` parameter instead.
+ round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].
+ warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors,
+ "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].
+ fallback: A function to call when an unknown value is encountered. If not provided,
+ a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.
+ serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.
Returns:
A dictionary representation of the model.
@@ -299,6 +308,8 @@ def model_dump(
raise ValueError("serialize_as_any is only supported in Pydantic v2")
if fallback is not None:
raise ValueError("fallback is only supported in Pydantic v2")
+ if exclude_computed_fields != False:
+ raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
dumped = super().dict( # pyright: ignore[reportDeprecated]
include=include,
exclude=exclude,
@@ -315,15 +326,17 @@ def model_dump_json(
self,
*,
indent: int | None = None,
+ ensure_ascii: bool = False,
include: IncEx | None = None,
exclude: IncEx | None = None,
+ context: Any | None = None,
by_alias: bool | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
+ exclude_computed_fields: bool = False,
round_trip: bool = False,
warnings: bool | Literal["none", "warn", "error"] = True,
- context: dict[str, Any] | None = None,
fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
) -> str:
@@ -355,6 +368,10 @@ def model_dump_json(
raise ValueError("serialize_as_any is only supported in Pydantic v2")
if fallback is not None:
raise ValueError("fallback is only supported in Pydantic v2")
+ if ensure_ascii != False:
+ raise ValueError("ensure_ascii is only supported in Pydantic v2")
+ if exclude_computed_fields != False:
+ raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
return super().json( # type: ignore[reportDeprecated]
indent=indent,
include=include,
From 46aed18f409d684470aa0ebc0a13b5b6ff9bd0dc Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 22 Nov 2025 05:05:18 +0000
Subject: [PATCH 144/200] chore: add Python 3.14 classifier and testing
---
pyproject.toml | 1 +
1 file changed, 1 insertion(+)
diff --git a/pyproject.toml b/pyproject.toml
index 5f9b8ef6..50c5462c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,6 +24,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Operating System :: OS Independent",
"Operating System :: POSIX",
"Operating System :: MacOS",
From 53847ee697e0392299c5bd8c5b76cfb729b36d9c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 28 Nov 2025 03:45:34 +0000
Subject: [PATCH 145/200] fix: ensure streams are always closed
---
src/maisa/_streaming.py | 22 ++++++++++++----------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/src/maisa/_streaming.py b/src/maisa/_streaming.py
index 044032c3..36e65af2 100644
--- a/src/maisa/_streaming.py
+++ b/src/maisa/_streaming.py
@@ -54,11 +54,12 @@ def __stream__(self) -> Iterator[_T]:
process_data = self._client._process_response_data
iterator = self._iter_events()
- for sse in iterator:
- yield process_data(data=sse.json(), cast_to=cast_to, response=response)
-
- # As we might not fully consume the response stream, we need to close it explicitly
- response.close()
+ try:
+ for sse in iterator:
+ yield process_data(data=sse.json(), cast_to=cast_to, response=response)
+ finally:
+ # Ensure the response is closed even if the consumer doesn't read all data
+ response.close()
def __enter__(self) -> Self:
return self
@@ -117,11 +118,12 @@ async def __stream__(self) -> AsyncIterator[_T]:
process_data = self._client._process_response_data
iterator = self._iter_events()
- async for sse in iterator:
- yield process_data(data=sse.json(), cast_to=cast_to, response=response)
-
- # As we might not fully consume the response stream, we need to close it explicitly
- await response.aclose()
+ try:
+ async for sse in iterator:
+ yield process_data(data=sse.json(), cast_to=cast_to, response=response)
+ finally:
+ # Ensure the response is closed even if the consumer doesn't read all data
+ await response.aclose()
async def __aenter__(self) -> Self:
return self
From d98efa461f0f5f492281fc3605c8fc187b61e804 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 28 Nov 2025 03:46:24 +0000
Subject: [PATCH 146/200] chore(deps): mypy 1.18.1 has a regression, pin to
1.17
---
pyproject.toml | 2 +-
requirements-dev.lock | 4 +++-
requirements.lock | 8 ++++----
3 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 50c5462c..59aa427c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -46,7 +46,7 @@ managed = true
# version pins are in requirements-dev.lock
dev-dependencies = [
"pyright==1.1.399",
- "mypy",
+ "mypy==1.17",
"respx",
"pytest",
"pytest-asyncio",
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 19ad0058..2917a7d0 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -72,7 +72,7 @@ mdurl==0.1.2
multidict==6.4.4
# via aiohttp
# via yarl
-mypy==1.14.1
+mypy==1.17.0
mypy-extensions==1.0.0
# via mypy
nodeenv==1.8.0
@@ -81,6 +81,8 @@ nox==2023.4.22
packaging==23.2
# via nox
# via pytest
+pathspec==0.12.1
+ # via mypy
platformdirs==3.11.0
# via virtualenv
pluggy==1.5.0
diff --git a/requirements.lock b/requirements.lock
index d2cf9ef9..10ab5583 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -55,21 +55,21 @@ multidict==6.4.4
propcache==0.3.1
# via aiohttp
# via yarl
-pydantic==2.11.9
+pydantic==2.12.5
# via maisa
-pydantic-core==2.33.2
+pydantic-core==2.41.5
# via pydantic
sniffio==1.3.0
# via anyio
# via maisa
-typing-extensions==4.12.2
+typing-extensions==4.15.0
# via anyio
# via maisa
# via multidict
# via pydantic
# via pydantic-core
# via typing-inspection
-typing-inspection==0.4.1
+typing-inspection==0.4.2
# via pydantic
yarl==1.20.0
# via aiohttp
From 5e779fcdf6fdfbde16fd3d5a8b0beee30e893532 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Dec 2025 07:39:25 +0000
Subject: [PATCH 147/200] chore: update lockfile
---
pyproject.toml | 14 +++---
requirements-dev.lock | 108 +++++++++++++++++++++++-------------------
requirements.lock | 31 ++++++------
3 files changed, 83 insertions(+), 70 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 59aa427c..5dc6987f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,14 +7,16 @@ license = "Apache-2.0"
authors = [
{ name = "Maisa", email = "support@maisa.ai" },
]
+
dependencies = [
- "httpx>=0.23.0, <1",
- "pydantic>=1.9.0, <3",
- "typing-extensions>=4.10, <5",
- "anyio>=3.5.0, <5",
- "distro>=1.7.0, <2",
- "sniffio",
+ "httpx>=0.23.0, <1",
+ "pydantic>=1.9.0, <3",
+ "typing-extensions>=4.10, <5",
+ "anyio>=3.5.0, <5",
+ "distro>=1.7.0, <2",
+ "sniffio",
]
+
requires-python = ">= 3.9"
classifiers = [
"Typing :: Typed",
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 2917a7d0..e9495273 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -12,40 +12,45 @@
-e file:.
aiohappyeyeballs==2.6.1
# via aiohttp
-aiohttp==3.12.8
+aiohttp==3.13.2
# via httpx-aiohttp
# via maisa
-aiosignal==1.3.2
+aiosignal==1.4.0
# via aiohttp
-annotated-types==0.6.0
+annotated-types==0.7.0
# via pydantic
-anyio==4.4.0
+anyio==4.12.0
# via httpx
# via maisa
-argcomplete==3.1.2
+argcomplete==3.6.3
# via nox
async-timeout==5.0.1
# via aiohttp
-attrs==25.3.0
+attrs==25.4.0
# via aiohttp
-certifi==2023.7.22
+ # via nox
+backports-asyncio-runner==1.2.0
+ # via pytest-asyncio
+certifi==2025.11.12
# via httpcore
# via httpx
-colorlog==6.7.0
+colorlog==6.10.1
+ # via nox
+dependency-groups==1.3.1
# via nox
-dirty-equals==0.6.0
-distlib==0.3.7
+dirty-equals==0.11
+distlib==0.4.0
# via virtualenv
-distro==1.8.0
+distro==1.9.0
# via maisa
-exceptiongroup==1.2.2
+exceptiongroup==1.3.1
# via anyio
# via pytest
-execnet==2.1.1
+execnet==2.1.2
# via pytest-xdist
-filelock==3.12.4
+filelock==3.19.1
# via virtualenv
-frozenlist==1.6.2
+frozenlist==1.8.0
# via aiohttp
# via aiosignal
h11==0.16.0
@@ -58,82 +63,87 @@ httpx==0.28.1
# via respx
httpx-aiohttp==0.1.9
# via maisa
-idna==3.4
+humanize==4.13.0
+ # via nox
+idna==3.11
# via anyio
# via httpx
# via yarl
-importlib-metadata==7.0.0
-iniconfig==2.0.0
+importlib-metadata==8.7.0
+iniconfig==2.1.0
# via pytest
markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
-multidict==6.4.4
+multidict==6.7.0
# via aiohttp
# via yarl
mypy==1.17.0
-mypy-extensions==1.0.0
+mypy-extensions==1.1.0
# via mypy
-nodeenv==1.8.0
+nodeenv==1.9.1
# via pyright
-nox==2023.4.22
-packaging==23.2
+nox==2025.11.12
+packaging==25.0
+ # via dependency-groups
# via nox
# via pytest
pathspec==0.12.1
# via mypy
-platformdirs==3.11.0
+platformdirs==4.4.0
# via virtualenv
-pluggy==1.5.0
+pluggy==1.6.0
# via pytest
-propcache==0.3.1
+propcache==0.4.1
# via aiohttp
# via yarl
-pydantic==2.11.9
+pydantic==2.12.5
# via maisa
-pydantic-core==2.33.2
+pydantic-core==2.41.5
# via pydantic
-pygments==2.18.0
+pygments==2.19.2
+ # via pytest
# via rich
pyright==1.1.399
-pytest==8.3.3
+pytest==8.4.2
# via pytest-asyncio
# via pytest-xdist
-pytest-asyncio==0.24.0
-pytest-xdist==3.7.0
-python-dateutil==2.8.2
+pytest-asyncio==1.2.0
+pytest-xdist==3.8.0
+python-dateutil==2.9.0.post0
# via time-machine
-pytz==2023.3.post1
- # via dirty-equals
respx==0.22.0
-rich==13.7.1
-ruff==0.9.4
-setuptools==68.2.2
- # via nodeenv
-six==1.16.0
+rich==14.2.0
+ruff==0.14.7
+six==1.17.0
# via python-dateutil
-sniffio==1.3.0
- # via anyio
+sniffio==1.3.1
# via maisa
-time-machine==2.9.0
-tomli==2.0.2
+time-machine==2.19.0
+tomli==2.3.0
+ # via dependency-groups
# via mypy
+ # via nox
# via pytest
-typing-extensions==4.12.2
+typing-extensions==4.15.0
+ # via aiosignal
# via anyio
+ # via exceptiongroup
# via maisa
# via multidict
# via mypy
# via pydantic
# via pydantic-core
# via pyright
+ # via pytest-asyncio
# via typing-inspection
-typing-inspection==0.4.1
+ # via virtualenv
+typing-inspection==0.4.2
# via pydantic
-virtualenv==20.24.5
+virtualenv==20.35.4
# via nox
-yarl==1.20.0
+yarl==1.22.0
# via aiohttp
-zipp==3.17.0
+zipp==3.23.0
# via importlib-metadata
diff --git a/requirements.lock b/requirements.lock
index 10ab5583..031d68e0 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -12,28 +12,28 @@
-e file:.
aiohappyeyeballs==2.6.1
# via aiohttp
-aiohttp==3.12.8
+aiohttp==3.13.2
# via httpx-aiohttp
# via maisa
-aiosignal==1.3.2
+aiosignal==1.4.0
# via aiohttp
-annotated-types==0.6.0
+annotated-types==0.7.0
# via pydantic
-anyio==4.4.0
+anyio==4.12.0
# via httpx
# via maisa
async-timeout==5.0.1
# via aiohttp
-attrs==25.3.0
+attrs==25.4.0
# via aiohttp
-certifi==2023.7.22
+certifi==2025.11.12
# via httpcore
# via httpx
-distro==1.8.0
+distro==1.9.0
# via maisa
-exceptiongroup==1.2.2
+exceptiongroup==1.3.1
# via anyio
-frozenlist==1.6.2
+frozenlist==1.8.0
# via aiohttp
# via aiosignal
h11==0.16.0
@@ -45,25 +45,26 @@ httpx==0.28.1
# via maisa
httpx-aiohttp==0.1.9
# via maisa
-idna==3.4
+idna==3.11
# via anyio
# via httpx
# via yarl
-multidict==6.4.4
+multidict==6.7.0
# via aiohttp
# via yarl
-propcache==0.3.1
+propcache==0.4.1
# via aiohttp
# via yarl
pydantic==2.12.5
# via maisa
pydantic-core==2.41.5
# via pydantic
-sniffio==1.3.0
- # via anyio
+sniffio==1.3.1
# via maisa
typing-extensions==4.15.0
+ # via aiosignal
# via anyio
+ # via exceptiongroup
# via maisa
# via multidict
# via pydantic
@@ -71,5 +72,5 @@ typing-extensions==4.15.0
# via typing-inspection
typing-inspection==0.4.2
# via pydantic
-yarl==1.20.0
+yarl==1.22.0
# via aiohttp
From 01f64ef00b017e4f9da7e863b9beb64acc90eb3d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Dec 2025 07:46:42 +0000
Subject: [PATCH 148/200] chore(docs): use environment variables for
authentication in code snippets
---
README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 6449319e..8f557356 100644
--- a/README.md
+++ b/README.md
@@ -83,6 +83,7 @@ pip install --pre maisa[aiohttp]
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
```python
+import os
import asyncio
from maisa import DefaultAioHttpClient
from maisa import AsyncMaisa
@@ -90,7 +91,7 @@ from maisa import AsyncMaisa
async def main() -> None:
async with AsyncMaisa(
- api_key="My API Key",
+ api_key=os.environ.get("MAISA_API_KEY"), # This is the default and can be omitted
http_client=DefaultAioHttpClient(),
) as client:
text_summary = await client.capabilities.summarize(
From e03ff7f9b4857533b050ec72e13e9a397eb2f2fa Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 9 Dec 2025 05:36:02 +0000
Subject: [PATCH 149/200] fix(types): allow pyright to infer TypedDict types
within SequenceNotStr
---
src/maisa/_types.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/maisa/_types.py b/src/maisa/_types.py
index 38a8779b..90a90144 100644
--- a/src/maisa/_types.py
+++ b/src/maisa/_types.py
@@ -243,6 +243,9 @@ class HttpxSendArgs(TypedDict, total=False):
if TYPE_CHECKING:
# This works because str.__contains__ does not accept object (either in typeshed or at runtime)
# https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285
+ #
+ # Note: index() and count() methods are intentionally omitted to allow pyright to properly
+ # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr.
class SequenceNotStr(Protocol[_T_co]):
@overload
def __getitem__(self, index: SupportsIndex, /) -> _T_co: ...
@@ -251,8 +254,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ...
def __contains__(self, value: object, /) -> bool: ...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[_T_co]: ...
- def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ...
- def count(self, value: Any, /) -> int: ...
def __reversed__(self) -> Iterator[_T_co]: ...
else:
# just point this to a normal `Sequence` at runtime to avoid having to special case
From 109587c0f7075c7af5c4679318a44d5696a1627f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 9 Dec 2025 05:37:30 +0000
Subject: [PATCH 150/200] chore: add missing docstrings
---
src/maisa/types/capability_compare_params.py | 2 ++
src/maisa/types/capability_extract_params.py | 2 ++
src/maisa/types/shared/text_comparator.py | 2 ++
src/maisa/types/shared/text_extractor.py | 2 ++
src/maisa/types/shared/text_summary.py | 2 ++
5 files changed, 10 insertions(+)
diff --git a/src/maisa/types/capability_compare_params.py b/src/maisa/types/capability_compare_params.py
index 8bdc52ae..ae2b5f45 100644
--- a/src/maisa/types/capability_compare_params.py
+++ b/src/maisa/types/capability_compare_params.py
@@ -30,6 +30,8 @@ class CapabilityCompareParams(TypedDict, total=False):
class Variables(TypedDict, total=False):
+ """Text Extraction Request Variable."""
+
description: Required[str]
type: Required[Literal["string", "number", "date", "boolean"]]
diff --git a/src/maisa/types/capability_extract_params.py b/src/maisa/types/capability_extract_params.py
index 2ac4f0f8..d17bf7a4 100644
--- a/src/maisa/types/capability_extract_params.py
+++ b/src/maisa/types/capability_extract_params.py
@@ -24,6 +24,8 @@ class CapabilityExtractParams(TypedDict, total=False):
class Variables(TypedDict, total=False):
+ """Text Extraction Request Variable."""
+
description: Required[str]
"""The description of the variable."""
diff --git a/src/maisa/types/shared/text_comparator.py b/src/maisa/types/shared/text_comparator.py
index 1c7408f7..d462e214 100644
--- a/src/maisa/types/shared/text_comparator.py
+++ b/src/maisa/types/shared/text_comparator.py
@@ -6,5 +6,7 @@
class TextComparator(BaseModel):
+ """Texts Comparator Response."""
+
extracted_data: object
"""The extracted data from the text."""
diff --git a/src/maisa/types/shared/text_extractor.py b/src/maisa/types/shared/text_extractor.py
index 0b243200..356fc426 100644
--- a/src/maisa/types/shared/text_extractor.py
+++ b/src/maisa/types/shared/text_extractor.py
@@ -6,5 +6,7 @@
class TextExtractor(BaseModel):
+ """Text Extraction Response."""
+
extracted_data: object
"""The extracted data from the text."""
diff --git a/src/maisa/types/shared/text_summary.py b/src/maisa/types/shared/text_summary.py
index 53aed0de..9feb5ba9 100644
--- a/src/maisa/types/shared/text_summary.py
+++ b/src/maisa/types/shared/text_summary.py
@@ -6,5 +6,7 @@
class TextSummary(BaseModel):
+ """Text Summary Request."""
+
summary: str
"""The summarized version of the provided text."""
From 934bd9d81930acd5fc3a65ad9508c94c6e12f868 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 16 Dec 2025 05:17:04 +0000
Subject: [PATCH 151/200] chore(internal): add missing files argument to base
client
---
src/maisa/_base_client.py | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index 13da7d81..9949c404 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -1247,9 +1247,12 @@ def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options)
+ opts = FinalRequestOptions.construct(
+ method="patch", url=path, json_data=body, files=to_httpx_files(files), **options
+ )
return self.request(cast_to, opts)
def put(
@@ -1767,9 +1770,12 @@ async def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options)
+ opts = FinalRequestOptions.construct(
+ method="patch", url=path, json_data=body, files=to_httpx_files(files), **options
+ )
return await self.request(cast_to, opts)
async def put(
From 454784da0fadabddc20c97d189bdf9e0a7d411fb Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 17 Dec 2025 07:52:51 +0000
Subject: [PATCH 152/200] chore: speedup initial import
---
src/maisa/_client.py | 228 +++++++++++++++++++++++++++++++++----------
1 file changed, 179 insertions(+), 49 deletions(-)
diff --git a/src/maisa/_client.py b/src/maisa/_client.py
index 1e53be64..63907cb2 100644
--- a/src/maisa/_client.py
+++ b/src/maisa/_client.py
@@ -3,7 +3,7 @@
from __future__ import annotations
import os
-from typing import Any, Mapping
+from typing import TYPE_CHECKING, Any, Mapping
from typing_extensions import Self, override
import httpx
@@ -20,8 +20,8 @@
not_given,
)
from ._utils import is_given, get_async_library
+from ._compat import cached_property
from ._version import __version__
-from .resources import kpu
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
from ._exceptions import MaisaError, APIStatusError
from ._base_client import (
@@ -29,21 +29,18 @@
SyncAPIClient,
AsyncAPIClient,
)
-from .resources.models import models
-from .resources.capabilities import capabilities
-from .resources.file_interpreter import file_interpreter
+
+if TYPE_CHECKING:
+ from .resources import kpu, models, capabilities, file_interpreter
+ from .resources.kpu import KpuResource, AsyncKpuResource
+ from .resources.models.models import ModelsResource, AsyncModelsResource
+ from .resources.capabilities.capabilities import CapabilitiesResource, AsyncCapabilitiesResource
+ from .resources.file_interpreter.file_interpreter import FileInterpreterResource, AsyncFileInterpreterResource
__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Maisa", "AsyncMaisa", "Client", "AsyncClient"]
class Maisa(SyncAPIClient):
- capabilities: capabilities.CapabilitiesResource
- models: models.ModelsResource
- kpu: kpu.KpuResource
- file_interpreter: file_interpreter.FileInterpreterResource
- with_raw_response: MaisaWithRawResponse
- with_streaming_response: MaisaWithStreamedResponse
-
# client options
api_key: str
@@ -98,12 +95,37 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.capabilities = capabilities.CapabilitiesResource(self)
- self.models = models.ModelsResource(self)
- self.kpu = kpu.KpuResource(self)
- self.file_interpreter = file_interpreter.FileInterpreterResource(self)
- self.with_raw_response = MaisaWithRawResponse(self)
- self.with_streaming_response = MaisaWithStreamedResponse(self)
+ @cached_property
+ def capabilities(self) -> CapabilitiesResource:
+ from .resources.capabilities import CapabilitiesResource
+
+ return CapabilitiesResource(self)
+
+ @cached_property
+ def models(self) -> ModelsResource:
+ from .resources.models import ModelsResource
+
+ return ModelsResource(self)
+
+ @cached_property
+ def kpu(self) -> KpuResource:
+ from .resources.kpu import KpuResource
+
+ return KpuResource(self)
+
+ @cached_property
+ def file_interpreter(self) -> FileInterpreterResource:
+ from .resources.file_interpreter import FileInterpreterResource
+
+ return FileInterpreterResource(self)
+
+ @cached_property
+ def with_raw_response(self) -> MaisaWithRawResponse:
+ return MaisaWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> MaisaWithStreamedResponse:
+ return MaisaWithStreamedResponse(self)
@property
@override
@@ -211,13 +233,6 @@ def _make_status_error(
class AsyncMaisa(AsyncAPIClient):
- capabilities: capabilities.AsyncCapabilitiesResource
- models: models.AsyncModelsResource
- kpu: kpu.AsyncKpuResource
- file_interpreter: file_interpreter.AsyncFileInterpreterResource
- with_raw_response: AsyncMaisaWithRawResponse
- with_streaming_response: AsyncMaisaWithStreamedResponse
-
# client options
api_key: str
@@ -272,12 +287,37 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.capabilities = capabilities.AsyncCapabilitiesResource(self)
- self.models = models.AsyncModelsResource(self)
- self.kpu = kpu.AsyncKpuResource(self)
- self.file_interpreter = file_interpreter.AsyncFileInterpreterResource(self)
- self.with_raw_response = AsyncMaisaWithRawResponse(self)
- self.with_streaming_response = AsyncMaisaWithStreamedResponse(self)
+ @cached_property
+ def capabilities(self) -> AsyncCapabilitiesResource:
+ from .resources.capabilities import AsyncCapabilitiesResource
+
+ return AsyncCapabilitiesResource(self)
+
+ @cached_property
+ def models(self) -> AsyncModelsResource:
+ from .resources.models import AsyncModelsResource
+
+ return AsyncModelsResource(self)
+
+ @cached_property
+ def kpu(self) -> AsyncKpuResource:
+ from .resources.kpu import AsyncKpuResource
+
+ return AsyncKpuResource(self)
+
+ @cached_property
+ def file_interpreter(self) -> AsyncFileInterpreterResource:
+ from .resources.file_interpreter import AsyncFileInterpreterResource
+
+ return AsyncFileInterpreterResource(self)
+
+ @cached_property
+ def with_raw_response(self) -> AsyncMaisaWithRawResponse:
+ return AsyncMaisaWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncMaisaWithStreamedResponse:
+ return AsyncMaisaWithStreamedResponse(self)
@property
@override
@@ -385,37 +425,127 @@ def _make_status_error(
class MaisaWithRawResponse:
+ _client: Maisa
+
def __init__(self, client: Maisa) -> None:
- self.capabilities = capabilities.CapabilitiesResourceWithRawResponse(client.capabilities)
- self.models = models.ModelsResourceWithRawResponse(client.models)
- self.kpu = kpu.KpuResourceWithRawResponse(client.kpu)
- self.file_interpreter = file_interpreter.FileInterpreterResourceWithRawResponse(client.file_interpreter)
+ self._client = client
+
+ @cached_property
+ def capabilities(self) -> capabilities.CapabilitiesResourceWithRawResponse:
+ from .resources.capabilities import CapabilitiesResourceWithRawResponse
+
+ return CapabilitiesResourceWithRawResponse(self._client.capabilities)
+
+ @cached_property
+ def models(self) -> models.ModelsResourceWithRawResponse:
+ from .resources.models import ModelsResourceWithRawResponse
+
+ return ModelsResourceWithRawResponse(self._client.models)
+
+ @cached_property
+ def kpu(self) -> kpu.KpuResourceWithRawResponse:
+ from .resources.kpu import KpuResourceWithRawResponse
+
+ return KpuResourceWithRawResponse(self._client.kpu)
+
+ @cached_property
+ def file_interpreter(self) -> file_interpreter.FileInterpreterResourceWithRawResponse:
+ from .resources.file_interpreter import FileInterpreterResourceWithRawResponse
+
+ return FileInterpreterResourceWithRawResponse(self._client.file_interpreter)
class AsyncMaisaWithRawResponse:
+ _client: AsyncMaisa
+
def __init__(self, client: AsyncMaisa) -> None:
- self.capabilities = capabilities.AsyncCapabilitiesResourceWithRawResponse(client.capabilities)
- self.models = models.AsyncModelsResourceWithRawResponse(client.models)
- self.kpu = kpu.AsyncKpuResourceWithRawResponse(client.kpu)
- self.file_interpreter = file_interpreter.AsyncFileInterpreterResourceWithRawResponse(client.file_interpreter)
+ self._client = client
+
+ @cached_property
+ def capabilities(self) -> capabilities.AsyncCapabilitiesResourceWithRawResponse:
+ from .resources.capabilities import AsyncCapabilitiesResourceWithRawResponse
+
+ return AsyncCapabilitiesResourceWithRawResponse(self._client.capabilities)
+
+ @cached_property
+ def models(self) -> models.AsyncModelsResourceWithRawResponse:
+ from .resources.models import AsyncModelsResourceWithRawResponse
+
+ return AsyncModelsResourceWithRawResponse(self._client.models)
+
+ @cached_property
+ def kpu(self) -> kpu.AsyncKpuResourceWithRawResponse:
+ from .resources.kpu import AsyncKpuResourceWithRawResponse
+
+ return AsyncKpuResourceWithRawResponse(self._client.kpu)
+
+ @cached_property
+ def file_interpreter(self) -> file_interpreter.AsyncFileInterpreterResourceWithRawResponse:
+ from .resources.file_interpreter import AsyncFileInterpreterResourceWithRawResponse
+
+ return AsyncFileInterpreterResourceWithRawResponse(self._client.file_interpreter)
class MaisaWithStreamedResponse:
+ _client: Maisa
+
def __init__(self, client: Maisa) -> None:
- self.capabilities = capabilities.CapabilitiesResourceWithStreamingResponse(client.capabilities)
- self.models = models.ModelsResourceWithStreamingResponse(client.models)
- self.kpu = kpu.KpuResourceWithStreamingResponse(client.kpu)
- self.file_interpreter = file_interpreter.FileInterpreterResourceWithStreamingResponse(client.file_interpreter)
+ self._client = client
+
+ @cached_property
+ def capabilities(self) -> capabilities.CapabilitiesResourceWithStreamingResponse:
+ from .resources.capabilities import CapabilitiesResourceWithStreamingResponse
+
+ return CapabilitiesResourceWithStreamingResponse(self._client.capabilities)
+
+ @cached_property
+ def models(self) -> models.ModelsResourceWithStreamingResponse:
+ from .resources.models import ModelsResourceWithStreamingResponse
+
+ return ModelsResourceWithStreamingResponse(self._client.models)
+
+ @cached_property
+ def kpu(self) -> kpu.KpuResourceWithStreamingResponse:
+ from .resources.kpu import KpuResourceWithStreamingResponse
+
+ return KpuResourceWithStreamingResponse(self._client.kpu)
+
+ @cached_property
+ def file_interpreter(self) -> file_interpreter.FileInterpreterResourceWithStreamingResponse:
+ from .resources.file_interpreter import FileInterpreterResourceWithStreamingResponse
+
+ return FileInterpreterResourceWithStreamingResponse(self._client.file_interpreter)
class AsyncMaisaWithStreamedResponse:
+ _client: AsyncMaisa
+
def __init__(self, client: AsyncMaisa) -> None:
- self.capabilities = capabilities.AsyncCapabilitiesResourceWithStreamingResponse(client.capabilities)
- self.models = models.AsyncModelsResourceWithStreamingResponse(client.models)
- self.kpu = kpu.AsyncKpuResourceWithStreamingResponse(client.kpu)
- self.file_interpreter = file_interpreter.AsyncFileInterpreterResourceWithStreamingResponse(
- client.file_interpreter
- )
+ self._client = client
+
+ @cached_property
+ def capabilities(self) -> capabilities.AsyncCapabilitiesResourceWithStreamingResponse:
+ from .resources.capabilities import AsyncCapabilitiesResourceWithStreamingResponse
+
+ return AsyncCapabilitiesResourceWithStreamingResponse(self._client.capabilities)
+
+ @cached_property
+ def models(self) -> models.AsyncModelsResourceWithStreamingResponse:
+ from .resources.models import AsyncModelsResourceWithStreamingResponse
+
+ return AsyncModelsResourceWithStreamingResponse(self._client.models)
+
+ @cached_property
+ def kpu(self) -> kpu.AsyncKpuResourceWithStreamingResponse:
+ from .resources.kpu import AsyncKpuResourceWithStreamingResponse
+
+ return AsyncKpuResourceWithStreamingResponse(self._client.kpu)
+
+ @cached_property
+ def file_interpreter(self) -> file_interpreter.AsyncFileInterpreterResourceWithStreamingResponse:
+ from .resources.file_interpreter import AsyncFileInterpreterResourceWithStreamingResponse
+
+ return AsyncFileInterpreterResourceWithStreamingResponse(self._client.file_interpreter)
Client = Maisa
From 6a0bda4b3294efa0605f660582b837656049eb1b Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 18 Dec 2025 09:17:07 +0000
Subject: [PATCH 153/200] fix: use async_to_httpx_files in patch method
---
src/maisa/_base_client.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index 9949c404..ea7e92b0 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -1774,7 +1774,7 @@ async def patch(
options: RequestOptions = {},
) -> ResponseT:
opts = FinalRequestOptions.construct(
- method="patch", url=path, json_data=body, files=to_httpx_files(files), **options
+ method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options
)
return await self.request(cast_to, opts)
From e44e4092f427678acfb3b2d7a417a23124942a85 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 19 Dec 2025 07:53:13 +0000
Subject: [PATCH 154/200] chore(internal): add `--fix` argument to lint script
---
scripts/lint | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/scripts/lint b/scripts/lint
index 76a131b0..60a47d73 100755
--- a/scripts/lint
+++ b/scripts/lint
@@ -4,8 +4,13 @@ set -e
cd "$(dirname "$0")/.."
-echo "==> Running lints"
-rye run lint
+if [ "$1" = "--fix" ]; then
+ echo "==> Running lints with --fix"
+ rye run fix:ruff
+else
+ echo "==> Running lints"
+ rye run lint
+fi
echo "==> Making sure it imports"
rye run python -c 'import maisa'
From 451bed3131cbfed8c3ce56ce8ca040c05b5b0e1e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 6 Jan 2026 06:50:30 +0000
Subject: [PATCH 155/200] chore: rename some identifiers
---
LICENSE | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/LICENSE b/LICENSE
index cab49bf2..4f7064a1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2025 Maisa
+ Copyright 2026 Maisa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
From c0a96e09be09ba4aa690bec9de3359538b972a72 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 14 Jan 2026 10:03:30 +0000
Subject: [PATCH 156/200] chore(internal): codegen related update
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 8f557356..3a483b62 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ The REST API documentation can be found on [docs.maisa.ai](https://docs.maisa.ai
```sh
# install from PyPI
-pip install --pre maisa
+pip install '--pre maisa'
```
## Usage
@@ -77,7 +77,7 @@ You can enable this by installing `aiohttp`:
```sh
# install from PyPI
-pip install --pre maisa[aiohttp]
+pip install '--pre maisa[aiohttp]'
```
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
From 04eb2a7ffc3b23c07c88f0d0a4e97a07a0b0f803 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 14 Jan 2026 10:12:37 +0000
Subject: [PATCH 157/200] feat(client): add support for binary request
streaming
---
src/maisa/_base_client.py | 145 ++++++++++++++++++++++++++---
src/maisa/_models.py | 17 +++-
src/maisa/_types.py | 9 ++
tests/test_client.py | 187 +++++++++++++++++++++++++++++++++++++-
4 files changed, 344 insertions(+), 14 deletions(-)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index ea7e92b0..69aa6f23 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -9,6 +9,7 @@
import inspect
import logging
import platform
+import warnings
import email.utils
from types import TracebackType
from random import random
@@ -51,9 +52,11 @@
ResponseT,
AnyMapping,
PostParser,
+ BinaryTypes,
RequestFiles,
HttpxSendArgs,
RequestOptions,
+ AsyncBinaryTypes,
HttpxRequestFiles,
ModelBuilderProtocol,
not_given,
@@ -477,8 +480,19 @@ def _build_request(
retries_taken: int = 0,
) -> httpx.Request:
if log.isEnabledFor(logging.DEBUG):
- log.debug("Request options: %s", model_dump(options, exclude_unset=True))
-
+ log.debug(
+ "Request options: %s",
+ model_dump(
+ options,
+ exclude_unset=True,
+ # Pydantic v1 can't dump every type we support in content, so we exclude it for now.
+ exclude={
+ "content",
+ }
+ if PYDANTIC_V1
+ else {},
+ ),
+ )
kwargs: dict[str, Any] = {}
json_data = options.json_data
@@ -532,7 +546,13 @@ def _build_request(
is_body_allowed = options.method.lower() != "get"
if is_body_allowed:
- if isinstance(json_data, bytes):
+ if options.content is not None and json_data is not None:
+ raise TypeError("Passing both `content` and `json_data` is not supported")
+ if options.content is not None and files is not None:
+ raise TypeError("Passing both `content` and `files` is not supported")
+ if options.content is not None:
+ kwargs["content"] = options.content
+ elif isinstance(json_data, bytes):
kwargs["content"] = json_data
else:
kwargs["json"] = json_data if is_given(json_data) else None
@@ -1194,6 +1214,7 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: Literal[False] = False,
@@ -1206,6 +1227,7 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: Literal[True],
@@ -1219,6 +1241,7 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: bool,
@@ -1231,13 +1254,25 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: bool = False,
stream_cls: type[_StreamT] | None = None,
) -> ResponseT | _StreamT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="post", url=path, json_data=body, files=to_httpx_files(files), **options
+ method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options
)
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
@@ -1247,11 +1282,23 @@ def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="patch", url=path, json_data=body, files=to_httpx_files(files), **options
+ method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options
)
return self.request(cast_to, opts)
@@ -1261,11 +1308,23 @@ def put(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="put", url=path, json_data=body, files=to_httpx_files(files), **options
+ method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options
)
return self.request(cast_to, opts)
@@ -1275,9 +1334,19 @@ def delete(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options)
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options)
return self.request(cast_to, opts)
def get_api_list(
@@ -1717,6 +1786,7 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: Literal[False] = False,
@@ -1729,6 +1799,7 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: Literal[True],
@@ -1742,6 +1813,7 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: bool,
@@ -1754,13 +1826,25 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: bool = False,
stream_cls: type[_AsyncStreamT] | None = None,
) -> ResponseT | _AsyncStreamT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options
+ method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options
)
return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)
@@ -1770,11 +1854,28 @@ async def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options
+ method="patch",
+ url=path,
+ json_data=body,
+ content=content,
+ files=await async_to_httpx_files(files),
+ **options,
)
return await self.request(cast_to, opts)
@@ -1784,11 +1885,23 @@ async def put(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options
+ method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options
)
return await self.request(cast_to, opts)
@@ -1798,9 +1911,19 @@ async def delete(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options)
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options)
return await self.request(cast_to, opts)
def get_api_list(
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index ca9500b2..29070e05 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -3,7 +3,20 @@
import os
import inspect
import weakref
-from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
+from typing import (
+ IO,
+ TYPE_CHECKING,
+ Any,
+ Type,
+ Union,
+ Generic,
+ TypeVar,
+ Callable,
+ Iterable,
+ Optional,
+ AsyncIterable,
+ cast,
+)
from datetime import date, datetime
from typing_extensions import (
List,
@@ -787,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
timeout: float | Timeout | None
files: HttpxRequestFiles | None
idempotency_key: str
+ content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None]
json_data: Body
extra_json: AnyMapping
follow_redirects: bool
@@ -805,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel):
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
follow_redirects: Union[bool, None] = None
+ content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None
# It should be noted that we cannot use `json` here as that would override
# a BaseModel method in an incompatible fashion.
json_data: Union[Body, None] = None
diff --git a/src/maisa/_types.py b/src/maisa/_types.py
index 90a90144..e54fd14a 100644
--- a/src/maisa/_types.py
+++ b/src/maisa/_types.py
@@ -13,9 +13,11 @@
Mapping,
TypeVar,
Callable,
+ Iterable,
Iterator,
Optional,
Sequence,
+ AsyncIterable,
)
from typing_extensions import (
Set,
@@ -56,6 +58,13 @@
else:
Base64FileInput = Union[IO[bytes], PathLike]
FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8.
+
+
+# Used for sending raw binary data / streaming data in request bodies
+# e.g. for file uploads without multipart encoding
+BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]]
+AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]]
+
FileTypes = Union[
# file (or bytes)
FileContent,
diff --git a/tests/test_client.py b/tests/test_client.py
index ba5f66b4..26ffa6bc 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -8,10 +8,11 @@
import json
import asyncio
import inspect
+import dataclasses
import tracemalloc
-from typing import Any, Union, cast
+from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast
from unittest import mock
-from typing_extensions import Literal
+from typing_extensions import Literal, AsyncIterator, override
import httpx
import pytest
@@ -36,6 +37,7 @@
from .utils import update_env
+T = TypeVar("T")
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
api_key = "My API Key"
@@ -50,6 +52,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float:
return 0.1
+def mirror_request_content(request: httpx.Request) -> httpx.Response:
+ return httpx.Response(200, content=request.content)
+
+
+# note: we can't use the httpx.MockTransport class as it consumes the request
+# body itself, which means we can't test that the body is read lazily
+class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport):
+ def __init__(
+ self,
+ handler: Callable[[httpx.Request], httpx.Response]
+ | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]],
+ ) -> None:
+ self.handler = handler
+
+ @override
+ def handle_request(
+ self,
+ request: httpx.Request,
+ ) -> httpx.Response:
+ assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function"
+ assert inspect.isfunction(self.handler), "handler must be a function"
+ return self.handler(request)
+
+ @override
+ async def handle_async_request(
+ self,
+ request: httpx.Request,
+ ) -> httpx.Response:
+ assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function"
+ return await self.handler(request)
+
+
+@dataclasses.dataclass
+class Counter:
+ value: int = 0
+
+
+def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]:
+ for item in iterable:
+ if counter:
+ counter.value += 1
+ yield item
+
+
+async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]:
+ for item in iterable:
+ if counter:
+ counter.value += 1
+ yield item
+
+
def _get_open_connections(client: Maisa | AsyncMaisa) -> int:
transport = client._client._transport
assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport)
@@ -490,6 +543,70 @@ def test_multipart_repeating_array(self, client: Maisa) -> None:
b"",
]
+ @pytest.mark.respx(base_url=base_url)
+ def test_binary_content_upload(self, respx_mock: MockRouter, client: Maisa) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ response = client.post(
+ "/upload",
+ content=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
+ def test_binary_content_upload_with_iterator(self) -> None:
+ file_content = b"Hello, this is a test file."
+ counter = Counter()
+ iterator = _make_sync_iterator([file_content], counter=counter)
+
+ def mock_handler(request: httpx.Request) -> httpx.Response:
+ assert counter.value == 0, "the request body should not have been read"
+ return httpx.Response(200, content=request.read())
+
+ with Maisa(
+ base_url=base_url,
+ api_key=api_key,
+ _strict_response_validation=True,
+ http_client=httpx.Client(transport=MockTransport(handler=mock_handler)),
+ ) as client:
+ response = client.post(
+ "/upload",
+ content=iterator,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+ assert counter.value == 1
+
+ @pytest.mark.respx(base_url=base_url)
+ def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: Maisa) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ with pytest.deprecated_call(
+ match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead."
+ ):
+ response = client.post(
+ "/upload",
+ body=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
@pytest.mark.respx(base_url=base_url)
def test_basic_union_response(self, respx_mock: MockRouter, client: Maisa) -> None:
class Model1(BaseModel):
@@ -1301,6 +1418,72 @@ def test_multipart_repeating_array(self, async_client: AsyncMaisa) -> None:
b"",
]
+ @pytest.mark.respx(base_url=base_url)
+ async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncMaisa) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ response = await async_client.post(
+ "/upload",
+ content=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
+ async def test_binary_content_upload_with_asynciterator(self) -> None:
+ file_content = b"Hello, this is a test file."
+ counter = Counter()
+ iterator = _make_async_iterator([file_content], counter=counter)
+
+ async def mock_handler(request: httpx.Request) -> httpx.Response:
+ assert counter.value == 0, "the request body should not have been read"
+ return httpx.Response(200, content=await request.aread())
+
+ async with AsyncMaisa(
+ base_url=base_url,
+ api_key=api_key,
+ _strict_response_validation=True,
+ http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)),
+ ) as client:
+ response = await client.post(
+ "/upload",
+ content=iterator,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+ assert counter.value == 1
+
+ @pytest.mark.respx(base_url=base_url)
+ async def test_binary_content_upload_with_body_is_deprecated(
+ self, respx_mock: MockRouter, async_client: AsyncMaisa
+ ) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ with pytest.deprecated_call(
+ match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead."
+ ):
+ response = await async_client.post(
+ "/upload",
+ body=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
@pytest.mark.respx(base_url=base_url)
async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncMaisa) -> None:
class Model1(BaseModel):
From e5f90c964b7060e6a063937a4c1ae48b61721243 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 17 Jan 2026 07:23:01 +0000
Subject: [PATCH 158/200] chore(internal): update `actions/checkout` version
---
.github/workflows/ci.yml | 6 +++---
.github/workflows/publish-pypi.yml | 2 +-
.github/workflows/release-doctor.yml | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f7499ee9..0e1fda4f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,7 +19,7 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/maisa-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
@@ -44,7 +44,7 @@ jobs:
id-token: write
runs-on: ${{ github.repository == 'stainless-sdks/maisa-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
@@ -81,7 +81,7 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/maisa-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index a79667d3..b5a491fb 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index e2332a84..a0b29d24 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'maisaai/python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Check release environment
run: |
From 7ab563f9ddaad08e5fc28fc0a91eca7f2a9b048b Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 24 Jan 2026 06:23:59 +0000
Subject: [PATCH 159/200] chore(ci): upgrade `actions/github-script`
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0e1fda4f..fc4fda9b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -63,7 +63,7 @@ jobs:
- name: Get GitHub OIDC Token
if: github.repository == 'stainless-sdks/maisa-python'
id: github-oidc
- uses: actions/github-script@v6
+ uses: actions/github-script@v8
with:
script: core.setOutput('github_token', await core.getIDToken());
From 55a31ca42d8156d80dafa60b950f0eb786189c8e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 30 Jan 2026 06:06:14 +0000
Subject: [PATCH 160/200] feat(client): add custom JSON encoder for extended
type support
---
src/maisa/_base_client.py | 7 +-
src/maisa/_compat.py | 6 +-
src/maisa/_utils/_json.py | 35 ++++++++++
tests/test_utils/test_json.py | 126 ++++++++++++++++++++++++++++++++++
4 files changed, 169 insertions(+), 5 deletions(-)
create mode 100644 src/maisa/_utils/_json.py
create mode 100644 tests/test_utils/test_json.py
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index 69aa6f23..aa2f358d 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -86,6 +86,7 @@
APIConnectionError,
APIResponseValidationError,
)
+from ._utils._json import openapi_dumps
log: logging.Logger = logging.getLogger(__name__)
@@ -554,8 +555,10 @@ def _build_request(
kwargs["content"] = options.content
elif isinstance(json_data, bytes):
kwargs["content"] = json_data
- else:
- kwargs["json"] = json_data if is_given(json_data) else None
+ elif not files:
+ # Don't set content when JSON is sent as multipart/form-data,
+ # since httpx's content param overrides other body arguments
+ kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None
kwargs["files"] = files
else:
headers.pop("Content-Type", None)
diff --git a/src/maisa/_compat.py b/src/maisa/_compat.py
index bdef67f0..786ff42a 100644
--- a/src/maisa/_compat.py
+++ b/src/maisa/_compat.py
@@ -139,6 +139,7 @@ def model_dump(
exclude_defaults: bool = False,
warnings: bool = True,
mode: Literal["json", "python"] = "python",
+ by_alias: bool | None = None,
) -> dict[str, Any]:
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
return model.model_dump(
@@ -148,13 +149,12 @@ def model_dump(
exclude_defaults=exclude_defaults,
# warnings are not supported in Pydantic v1
warnings=True if PYDANTIC_V1 else warnings,
+ by_alias=by_alias,
)
return cast(
"dict[str, Any]",
model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
- exclude=exclude,
- exclude_unset=exclude_unset,
- exclude_defaults=exclude_defaults,
+ exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias)
),
)
diff --git a/src/maisa/_utils/_json.py b/src/maisa/_utils/_json.py
new file mode 100644
index 00000000..60584214
--- /dev/null
+++ b/src/maisa/_utils/_json.py
@@ -0,0 +1,35 @@
+import json
+from typing import Any
+from datetime import datetime
+from typing_extensions import override
+
+import pydantic
+
+from .._compat import model_dump
+
+
+def openapi_dumps(obj: Any) -> bytes:
+ """
+ Serialize an object to UTF-8 encoded JSON bytes.
+
+ Extends the standard json.dumps with support for additional types
+ commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc.
+ """
+ return json.dumps(
+ obj,
+ cls=_CustomEncoder,
+ # Uses the same defaults as httpx's JSON serialization
+ ensure_ascii=False,
+ separators=(",", ":"),
+ allow_nan=False,
+ ).encode()
+
+
+class _CustomEncoder(json.JSONEncoder):
+ @override
+ def default(self, o: Any) -> Any:
+ if isinstance(o, datetime):
+ return o.isoformat()
+ if isinstance(o, pydantic.BaseModel):
+ return model_dump(o, exclude_unset=True, mode="json", by_alias=True)
+ return super().default(o)
diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py
new file mode 100644
index 00000000..900fba25
--- /dev/null
+++ b/tests/test_utils/test_json.py
@@ -0,0 +1,126 @@
+from __future__ import annotations
+
+import datetime
+from typing import Union
+
+import pydantic
+
+from maisa import _compat
+from maisa._utils._json import openapi_dumps
+
+
+class TestOpenapiDumps:
+ def test_basic(self) -> None:
+ data = {"key": "value", "number": 42}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"key":"value","number":42}'
+
+ def test_datetime_serialization(self) -> None:
+ dt = datetime.datetime(2023, 1, 1, 12, 0, 0)
+ data = {"datetime": dt}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}'
+
+ def test_pydantic_model_serialization(self) -> None:
+ class User(pydantic.BaseModel):
+ first_name: str
+ last_name: str
+ age: int
+
+ model_instance = User(first_name="John", last_name="Kramer", age=83)
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}'
+
+ def test_pydantic_model_with_default_values(self) -> None:
+ class User(pydantic.BaseModel):
+ name: str
+ role: str = "user"
+ active: bool = True
+ score: int = 0
+
+ model_instance = User(name="Alice")
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Alice"}}'
+
+ def test_pydantic_model_with_default_values_overridden(self) -> None:
+ class User(pydantic.BaseModel):
+ name: str
+ role: str = "user"
+ active: bool = True
+
+ model_instance = User(name="Bob", role="admin", active=False)
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}'
+
+ def test_pydantic_model_with_alias(self) -> None:
+ class User(pydantic.BaseModel):
+ first_name: str = pydantic.Field(alias="firstName")
+ last_name: str = pydantic.Field(alias="lastName")
+
+ model_instance = User(firstName="John", lastName="Doe")
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}'
+
+ def test_pydantic_model_with_alias_and_default(self) -> None:
+ class User(pydantic.BaseModel):
+ user_name: str = pydantic.Field(alias="userName")
+ user_role: str = pydantic.Field(default="member", alias="userRole")
+ is_active: bool = pydantic.Field(default=True, alias="isActive")
+
+ model_instance = User(userName="charlie")
+ data = {"model": model_instance}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"userName":"charlie"}}'
+
+ model_with_overrides = User(userName="diana", userRole="admin", isActive=False)
+ data = {"model": model_with_overrides}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}'
+
+ def test_pydantic_model_with_nested_models_and_defaults(self) -> None:
+ class Address(pydantic.BaseModel):
+ street: str
+ city: str = "Unknown"
+
+ class User(pydantic.BaseModel):
+ name: str
+ address: Address
+ verified: bool = False
+
+ if _compat.PYDANTIC_V1:
+ # to handle forward references in Pydantic v1
+ User.update_forward_refs(**locals()) # type: ignore[reportDeprecated]
+
+ address = Address(street="123 Main St")
+ user = User(name="Diana", address=address)
+ data = {"user": user}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}'
+
+ address_with_city = Address(street="456 Oak Ave", city="Boston")
+ user_verified = User(name="Eve", address=address_with_city, verified=True)
+ data = {"user": user_verified}
+ json_bytes = openapi_dumps(data)
+ assert (
+ json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}'
+ )
+
+ def test_pydantic_model_with_optional_fields(self) -> None:
+ class User(pydantic.BaseModel):
+ name: str
+ email: Union[str, None]
+ phone: Union[str, None]
+
+ model_with_none = User(name="Eve", email=None, phone=None)
+ data = {"model": model_with_none}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}'
+
+ model_with_values = User(name="Frank", email="frank@example.com", phone=None)
+ data = {"model": model_with_values}
+ json_bytes = openapi_dumps(data)
+ assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}'
From 9e165dff04e1ecfb06f3413ce67ca5f748be831c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 10 Feb 2026 06:22:30 +0000
Subject: [PATCH 161/200] chore(internal): bump dependencies
---
requirements-dev.lock | 20 ++++++++++----------
requirements.lock | 8 ++++----
2 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/requirements-dev.lock b/requirements-dev.lock
index e9495273..6a73e3ea 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -12,14 +12,14 @@
-e file:.
aiohappyeyeballs==2.6.1
# via aiohttp
-aiohttp==3.13.2
+aiohttp==3.13.3
# via httpx-aiohttp
# via maisa
aiosignal==1.4.0
# via aiohttp
annotated-types==0.7.0
# via pydantic
-anyio==4.12.0
+anyio==4.12.1
# via httpx
# via maisa
argcomplete==3.6.3
@@ -31,7 +31,7 @@ attrs==25.4.0
# via nox
backports-asyncio-runner==1.2.0
# via pytest-asyncio
-certifi==2025.11.12
+certifi==2026.1.4
# via httpcore
# via httpx
colorlog==6.10.1
@@ -61,7 +61,7 @@ httpx==0.28.1
# via httpx-aiohttp
# via maisa
# via respx
-httpx-aiohttp==0.1.9
+httpx-aiohttp==0.1.12
# via maisa
humanize==4.13.0
# via nox
@@ -69,7 +69,7 @@ idna==3.11
# via anyio
# via httpx
# via yarl
-importlib-metadata==8.7.0
+importlib-metadata==8.7.1
iniconfig==2.1.0
# via pytest
markdown-it-py==3.0.0
@@ -82,14 +82,14 @@ multidict==6.7.0
mypy==1.17.0
mypy-extensions==1.1.0
# via mypy
-nodeenv==1.9.1
+nodeenv==1.10.0
# via pyright
nox==2025.11.12
packaging==25.0
# via dependency-groups
# via nox
# via pytest
-pathspec==0.12.1
+pathspec==1.0.3
# via mypy
platformdirs==4.4.0
# via virtualenv
@@ -115,13 +115,13 @@ python-dateutil==2.9.0.post0
# via time-machine
respx==0.22.0
rich==14.2.0
-ruff==0.14.7
+ruff==0.14.13
six==1.17.0
# via python-dateutil
sniffio==1.3.1
# via maisa
time-machine==2.19.0
-tomli==2.3.0
+tomli==2.4.0
# via dependency-groups
# via mypy
# via nox
@@ -141,7 +141,7 @@ typing-extensions==4.15.0
# via virtualenv
typing-inspection==0.4.2
# via pydantic
-virtualenv==20.35.4
+virtualenv==20.36.1
# via nox
yarl==1.22.0
# via aiohttp
diff --git a/requirements.lock b/requirements.lock
index 031d68e0..222fdaf2 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -12,21 +12,21 @@
-e file:.
aiohappyeyeballs==2.6.1
# via aiohttp
-aiohttp==3.13.2
+aiohttp==3.13.3
# via httpx-aiohttp
# via maisa
aiosignal==1.4.0
# via aiohttp
annotated-types==0.7.0
# via pydantic
-anyio==4.12.0
+anyio==4.12.1
# via httpx
# via maisa
async-timeout==5.0.1
# via aiohttp
attrs==25.4.0
# via aiohttp
-certifi==2025.11.12
+certifi==2026.1.4
# via httpcore
# via httpx
distro==1.9.0
@@ -43,7 +43,7 @@ httpcore==1.0.9
httpx==0.28.1
# via httpx-aiohttp
# via maisa
-httpx-aiohttp==0.1.9
+httpx-aiohttp==0.1.12
# via maisa
idna==3.11
# via anyio
From 6d06b9846990a180cc8dcd7c4bbeae2433d93da9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 12 Feb 2026 08:10:45 +0000
Subject: [PATCH 162/200] chore(internal): fix lint error on Python 3.14
---
src/maisa/_utils/_compat.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/maisa/_utils/_compat.py b/src/maisa/_utils/_compat.py
index dd703233..2c70b299 100644
--- a/src/maisa/_utils/_compat.py
+++ b/src/maisa/_utils/_compat.py
@@ -26,7 +26,7 @@ def is_union(tp: Optional[Type[Any]]) -> bool:
else:
import types
- return tp is Union or tp is types.UnionType
+ return tp is Union or tp is types.UnionType # type: ignore[comparison-overlap]
def is_typeddict(tp: Type[Any]) -> bool:
From 75ee9b26e5b8437cc020e245237215ed42367ce1 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 13 Feb 2026 05:17:51 +0000
Subject: [PATCH 163/200] chore: format all `api.md` files
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 5dc6987f..9291645e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -69,7 +69,7 @@ format = { chain = [
# run formatting again to fix any inconsistencies when imports are stripped
"format:ruff",
]}
-"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md"
+"format:docs" = "bash -c 'python scripts/utils/ruffen-docs.py README.md $(find . -type f -name api.md)'"
"format:ruff" = "ruff format"
"lint" = { chain = [
From ea4ade65d8e5934cc210b42bfd8a0c9a80ac79f6 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 20 Feb 2026 07:38:58 +0000
Subject: [PATCH 164/200] chore: update mock server docs
---
CONTRIBUTING.md | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a1e868dc..972498a9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -88,8 +88,7 @@ $ pip install ./path-to-wheel-file.whl
Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests.
```sh
-# you will need npm installed
-$ npx prism mock path/to/your/openapi.yml
+$ ./scripts/mock
```
```sh
From aa545fe0459755e62127496a35f5d139fda198aa Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 24 Feb 2026 07:48:50 +0000
Subject: [PATCH 165/200] chore(internal): add request options to SSE classes
---
src/maisa/_response.py | 3 +++
src/maisa/_streaming.py | 11 ++++++++---
2 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/src/maisa/_response.py b/src/maisa/_response.py
index 89735fac..891c3fb6 100644
--- a/src/maisa/_response.py
+++ b/src/maisa/_response.py
@@ -152,6 +152,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
),
response=self.http_response,
client=cast(Any, self._client),
+ options=self._options,
),
)
@@ -162,6 +163,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
cast_to=extract_stream_chunk_type(self._stream_cls),
response=self.http_response,
client=cast(Any, self._client),
+ options=self._options,
),
)
@@ -175,6 +177,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
cast_to=cast_to,
response=self.http_response,
client=cast(Any, self._client),
+ options=self._options,
),
)
diff --git a/src/maisa/_streaming.py b/src/maisa/_streaming.py
index 36e65af2..5f5934f2 100644
--- a/src/maisa/_streaming.py
+++ b/src/maisa/_streaming.py
@@ -4,7 +4,7 @@
import json
import inspect
from types import TracebackType
-from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast
+from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast
from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable
import httpx
@@ -13,6 +13,7 @@
if TYPE_CHECKING:
from ._client import Maisa, AsyncMaisa
+ from ._models import FinalRequestOptions
_T = TypeVar("_T")
@@ -22,7 +23,7 @@ class Stream(Generic[_T]):
"""Provides the core interface to iterate over a synchronous stream response."""
response: httpx.Response
-
+ _options: Optional[FinalRequestOptions] = None
_decoder: SSEBytesDecoder
def __init__(
@@ -31,10 +32,12 @@ def __init__(
cast_to: type[_T],
response: httpx.Response,
client: Maisa,
+ options: Optional[FinalRequestOptions] = None,
) -> None:
self.response = response
self._cast_to = cast_to
self._client = client
+ self._options = options
self._decoder = client._make_sse_decoder()
self._iterator = self.__stream__()
@@ -85,7 +88,7 @@ class AsyncStream(Generic[_T]):
"""Provides the core interface to iterate over an asynchronous stream response."""
response: httpx.Response
-
+ _options: Optional[FinalRequestOptions] = None
_decoder: SSEDecoder | SSEBytesDecoder
def __init__(
@@ -94,10 +97,12 @@ def __init__(
cast_to: type[_T],
response: httpx.Response,
client: AsyncMaisa,
+ options: Optional[FinalRequestOptions] = None,
) -> None:
self.response = response
self._cast_to = cast_to
self._client = client
+ self._options = options
self._decoder = client._make_sse_decoder()
self._iterator = self.__stream__()
From 035d6a071f6f44e7d94233660d0370bc6e1491bf Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 24 Feb 2026 07:57:14 +0000
Subject: [PATCH 166/200] chore(internal): make
`test_proxy_environment_variables` more resilient
---
tests/test_client.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/tests/test_client.py b/tests/test_client.py
index 26ffa6bc..1503ddf8 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -937,6 +937,8 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Test that the proxy environment variables are set correctly
monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
+ # Delete in case our environment has this set
+ monkeypatch.delenv("HTTP_PROXY", raising=False)
client = DefaultHttpxClient()
@@ -1831,6 +1833,8 @@ async def test_get_platform(self) -> None:
async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Test that the proxy environment variables are set correctly
monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
+ # Delete in case our environment has this set
+ monkeypatch.delenv("HTTP_PROXY", raising=False)
client = DefaultAsyncHttpxClient()
From 3f72803465edcab2e8a602deeb46b7a797a0a0cc Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 25 Feb 2026 07:46:54 +0000
Subject: [PATCH 167/200] chore(internal): make
`test_proxy_environment_variables` more resilient to env
---
tests/test_client.py | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/tests/test_client.py b/tests/test_client.py
index 1503ddf8..999c7a55 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -937,8 +937,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Test that the proxy environment variables are set correctly
monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
- # Delete in case our environment has this set
+ # Delete in case our environment has any proxy env vars set
monkeypatch.delenv("HTTP_PROXY", raising=False)
+ monkeypatch.delenv("ALL_PROXY", raising=False)
+ monkeypatch.delenv("NO_PROXY", raising=False)
+ monkeypatch.delenv("http_proxy", raising=False)
+ monkeypatch.delenv("https_proxy", raising=False)
+ monkeypatch.delenv("all_proxy", raising=False)
+ monkeypatch.delenv("no_proxy", raising=False)
client = DefaultHttpxClient()
@@ -1833,8 +1839,14 @@ async def test_get_platform(self) -> None:
async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Test that the proxy environment variables are set correctly
monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
- # Delete in case our environment has this set
+ # Delete in case our environment has any proxy env vars set
monkeypatch.delenv("HTTP_PROXY", raising=False)
+ monkeypatch.delenv("ALL_PROXY", raising=False)
+ monkeypatch.delenv("NO_PROXY", raising=False)
+ monkeypatch.delenv("http_proxy", raising=False)
+ monkeypatch.delenv("https_proxy", raising=False)
+ monkeypatch.delenv("all_proxy", raising=False)
+ monkeypatch.delenv("no_proxy", raising=False)
client = DefaultAsyncHttpxClient()
From 316c1eeb729b228b4f32dbc92846cfcc6e02194f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 6 Mar 2026 13:27:35 +0000
Subject: [PATCH 168/200] chore(test): do not count install time for mock
server timeout
---
scripts/mock | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/scripts/mock b/scripts/mock
index 0b28f6ea..bcf3b392 100755
--- a/scripts/mock
+++ b/scripts/mock
@@ -21,11 +21,22 @@ echo "==> Starting mock server with URL ${URL}"
# Run prism mock on the given spec
if [ "$1" == "--daemon" ]; then
+ # Pre-install the package so the download doesn't eat into the startup timeout
+ npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version
+
npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log &
- # Wait for server to come online
+ # Wait for server to come online (max 30s)
echo -n "Waiting for server"
+ attempts=0
while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do
+ attempts=$((attempts + 1))
+ if [ "$attempts" -ge 300 ]; then
+ echo
+ echo "Timed out waiting for Prism server to start"
+ cat .prism.log
+ exit 1
+ fi
echo -n "."
sleep 0.1
done
From 637875251e913836866694cc72a0bc67291eb0af Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 7 Mar 2026 19:10:49 +0000
Subject: [PATCH 169/200] chore(ci): skip uploading artifacts on
stainless-internal branches
---
.github/workflows/ci.yml | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fc4fda9b..451f028e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -61,14 +61,18 @@ jobs:
run: rye build
- name: Get GitHub OIDC Token
- if: github.repository == 'stainless-sdks/maisa-python'
+ if: |-
+ github.repository == 'stainless-sdks/maisa-python' &&
+ !startsWith(github.ref, 'refs/heads/stl/')
id: github-oidc
uses: actions/github-script@v8
with:
script: core.setOutput('github_token', await core.getIDToken());
- name: Upload tarball
- if: github.repository == 'stainless-sdks/maisa-python'
+ if: |-
+ github.repository == 'stainless-sdks/maisa-python' &&
+ !startsWith(github.ref, 'refs/heads/stl/')
env:
URL: https://pkg.stainless.com/s
AUTH: ${{ steps.github-oidc.outputs.github_token }}
From c38bd27f7f3d9a5a0cca82ec82d6e762697f86db Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 7 Mar 2026 19:12:25 +0000
Subject: [PATCH 170/200] chore: update placeholder string
---
README.md | 2 +-
.../api_resources/capabilities/test_media.py | 64 +++++++++----------
.../file_interpreter/test_from_audio.py | 12 ++--
.../file_interpreter/test_from_docx.py | 12 ++--
.../file_interpreter/test_from_html.py | 12 ++--
.../file_interpreter/test_from_image.py | 12 ++--
.../file_interpreter/test_from_pdf.py | 16 ++---
tests/api_resources/test_kpu.py | 4 +-
8 files changed, 67 insertions(+), 67 deletions(-)
diff --git a/README.md b/README.md
index 3a483b62..1fe3d1dd 100644
--- a/README.md
+++ b/README.md
@@ -124,7 +124,7 @@ client = Maisa()
client.capabilities.media.compare(
file1=Path("/path/to/file"),
- file2=b"raw file contents",
+ file2=b"Example data",
)
```
diff --git a/tests/api_resources/capabilities/test_media.py b/tests/api_resources/capabilities/test_media.py
index e160e63f..8ed62c73 100644
--- a/tests/api_resources/capabilities/test_media.py
+++ b/tests/api_resources/capabilities/test_media.py
@@ -20,16 +20,16 @@ class TestMedia:
@parametrize
def test_method_compare(self, client: Maisa) -> None:
media = client.capabilities.media.compare(
- file1=b"raw file contents",
- file2=b"raw file contents",
+ file1=b"Example data",
+ file2=b"Example data",
)
assert_matches_type(TextComparator, media, path=["response"])
@parametrize
def test_method_compare_with_all_params(self, client: Maisa) -> None:
media = client.capabilities.media.compare(
- file1=b"raw file contents",
- file2=b"raw file contents",
+ file1=b"Example data",
+ file2=b"Example data",
lang="en",
prompt="Compare the value for end customer.",
variable1_description="The name of the person.",
@@ -50,8 +50,8 @@ def test_method_compare_with_all_params(self, client: Maisa) -> None:
@parametrize
def test_raw_response_compare(self, client: Maisa) -> None:
response = client.capabilities.media.with_raw_response.compare(
- file1=b"raw file contents",
- file2=b"raw file contents",
+ file1=b"Example data",
+ file2=b"Example data",
)
assert response.is_closed is True
@@ -62,8 +62,8 @@ def test_raw_response_compare(self, client: Maisa) -> None:
@parametrize
def test_streaming_response_compare(self, client: Maisa) -> None:
with client.capabilities.media.with_streaming_response.compare(
- file1=b"raw file contents",
- file2=b"raw file contents",
+ file1=b"Example data",
+ file2=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -76,14 +76,14 @@ def test_streaming_response_compare(self, client: Maisa) -> None:
@parametrize
def test_method_extract(self, client: Maisa) -> None:
media = client.capabilities.media.extract(
- file=b"raw file contents",
+ file=b"Example data",
)
assert_matches_type(TextExtractor, media, path=["response"])
@parametrize
def test_method_extract_with_all_params(self, client: Maisa) -> None:
media = client.capabilities.media.extract(
- file=b"raw file contents",
+ file=b"Example data",
lang="en",
variable1_description="The name of the person.",
variable1_name="Name",
@@ -103,7 +103,7 @@ def test_method_extract_with_all_params(self, client: Maisa) -> None:
@parametrize
def test_raw_response_extract(self, client: Maisa) -> None:
response = client.capabilities.media.with_raw_response.extract(
- file=b"raw file contents",
+ file=b"Example data",
)
assert response.is_closed is True
@@ -114,7 +114,7 @@ def test_raw_response_extract(self, client: Maisa) -> None:
@parametrize
def test_streaming_response_extract(self, client: Maisa) -> None:
with client.capabilities.media.with_streaming_response.extract(
- file=b"raw file contents",
+ file=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -127,14 +127,14 @@ def test_streaming_response_extract(self, client: Maisa) -> None:
@parametrize
def test_method_summarize(self, client: Maisa) -> None:
media = client.capabilities.media.summarize(
- file=b"raw file contents",
+ file=b"Example data",
)
assert_matches_type(TextSummary, media, path=["response"])
@parametrize
def test_method_summarize_with_all_params(self, client: Maisa) -> None:
media = client.capabilities.media.summarize(
- file=b"raw file contents",
+ file=b"Example data",
format="paragraph",
lang="en",
length="short",
@@ -145,7 +145,7 @@ def test_method_summarize_with_all_params(self, client: Maisa) -> None:
@parametrize
def test_raw_response_summarize(self, client: Maisa) -> None:
response = client.capabilities.media.with_raw_response.summarize(
- file=b"raw file contents",
+ file=b"Example data",
)
assert response.is_closed is True
@@ -156,7 +156,7 @@ def test_raw_response_summarize(self, client: Maisa) -> None:
@parametrize
def test_streaming_response_summarize(self, client: Maisa) -> None:
with client.capabilities.media.with_streaming_response.summarize(
- file=b"raw file contents",
+ file=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -175,16 +175,16 @@ class TestAsyncMedia:
@parametrize
async def test_method_compare(self, async_client: AsyncMaisa) -> None:
media = await async_client.capabilities.media.compare(
- file1=b"raw file contents",
- file2=b"raw file contents",
+ file1=b"Example data",
+ file2=b"Example data",
)
assert_matches_type(TextComparator, media, path=["response"])
@parametrize
async def test_method_compare_with_all_params(self, async_client: AsyncMaisa) -> None:
media = await async_client.capabilities.media.compare(
- file1=b"raw file contents",
- file2=b"raw file contents",
+ file1=b"Example data",
+ file2=b"Example data",
lang="en",
prompt="Compare the value for end customer.",
variable1_description="The name of the person.",
@@ -205,8 +205,8 @@ async def test_method_compare_with_all_params(self, async_client: AsyncMaisa) ->
@parametrize
async def test_raw_response_compare(self, async_client: AsyncMaisa) -> None:
response = await async_client.capabilities.media.with_raw_response.compare(
- file1=b"raw file contents",
- file2=b"raw file contents",
+ file1=b"Example data",
+ file2=b"Example data",
)
assert response.is_closed is True
@@ -217,8 +217,8 @@ async def test_raw_response_compare(self, async_client: AsyncMaisa) -> None:
@parametrize
async def test_streaming_response_compare(self, async_client: AsyncMaisa) -> None:
async with async_client.capabilities.media.with_streaming_response.compare(
- file1=b"raw file contents",
- file2=b"raw file contents",
+ file1=b"Example data",
+ file2=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -231,14 +231,14 @@ async def test_streaming_response_compare(self, async_client: AsyncMaisa) -> Non
@parametrize
async def test_method_extract(self, async_client: AsyncMaisa) -> None:
media = await async_client.capabilities.media.extract(
- file=b"raw file contents",
+ file=b"Example data",
)
assert_matches_type(TextExtractor, media, path=["response"])
@parametrize
async def test_method_extract_with_all_params(self, async_client: AsyncMaisa) -> None:
media = await async_client.capabilities.media.extract(
- file=b"raw file contents",
+ file=b"Example data",
lang="en",
variable1_description="The name of the person.",
variable1_name="Name",
@@ -258,7 +258,7 @@ async def test_method_extract_with_all_params(self, async_client: AsyncMaisa) ->
@parametrize
async def test_raw_response_extract(self, async_client: AsyncMaisa) -> None:
response = await async_client.capabilities.media.with_raw_response.extract(
- file=b"raw file contents",
+ file=b"Example data",
)
assert response.is_closed is True
@@ -269,7 +269,7 @@ async def test_raw_response_extract(self, async_client: AsyncMaisa) -> None:
@parametrize
async def test_streaming_response_extract(self, async_client: AsyncMaisa) -> None:
async with async_client.capabilities.media.with_streaming_response.extract(
- file=b"raw file contents",
+ file=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -282,14 +282,14 @@ async def test_streaming_response_extract(self, async_client: AsyncMaisa) -> Non
@parametrize
async def test_method_summarize(self, async_client: AsyncMaisa) -> None:
media = await async_client.capabilities.media.summarize(
- file=b"raw file contents",
+ file=b"Example data",
)
assert_matches_type(TextSummary, media, path=["response"])
@parametrize
async def test_method_summarize_with_all_params(self, async_client: AsyncMaisa) -> None:
media = await async_client.capabilities.media.summarize(
- file=b"raw file contents",
+ file=b"Example data",
format="paragraph",
lang="en",
length="short",
@@ -300,7 +300,7 @@ async def test_method_summarize_with_all_params(self, async_client: AsyncMaisa)
@parametrize
async def test_raw_response_summarize(self, async_client: AsyncMaisa) -> None:
response = await async_client.capabilities.media.with_raw_response.summarize(
- file=b"raw file contents",
+ file=b"Example data",
)
assert response.is_closed is True
@@ -311,7 +311,7 @@ async def test_raw_response_summarize(self, async_client: AsyncMaisa) -> None:
@parametrize
async def test_streaming_response_summarize(self, async_client: AsyncMaisa) -> None:
async with async_client.capabilities.media.with_streaming_response.summarize(
- file=b"raw file contents",
+ file=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
diff --git a/tests/api_resources/file_interpreter/test_from_audio.py b/tests/api_resources/file_interpreter/test_from_audio.py
index 9f010b48..6d4d43ad 100644
--- a/tests/api_resources/file_interpreter/test_from_audio.py
+++ b/tests/api_resources/file_interpreter/test_from_audio.py
@@ -19,14 +19,14 @@ class TestFromAudio:
@parametrize
def test_method_create(self, client: Maisa) -> None:
from_audio = client.file_interpreter.from_audio.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert_matches_type(object, from_audio, path=["response"])
@parametrize
def test_raw_response_create(self, client: Maisa) -> None:
response = client.file_interpreter.from_audio.with_raw_response.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert response.is_closed is True
@@ -37,7 +37,7 @@ def test_raw_response_create(self, client: Maisa) -> None:
@parametrize
def test_streaming_response_create(self, client: Maisa) -> None:
with client.file_interpreter.from_audio.with_streaming_response.create(
- file=b"raw file contents",
+ file=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -56,14 +56,14 @@ class TestAsyncFromAudio:
@parametrize
async def test_method_create(self, async_client: AsyncMaisa) -> None:
from_audio = await async_client.file_interpreter.from_audio.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert_matches_type(object, from_audio, path=["response"])
@parametrize
async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
response = await async_client.file_interpreter.from_audio.with_raw_response.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert response.is_closed is True
@@ -74,7 +74,7 @@ async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
@parametrize
async def test_streaming_response_create(self, async_client: AsyncMaisa) -> None:
async with async_client.file_interpreter.from_audio.with_streaming_response.create(
- file=b"raw file contents",
+ file=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
diff --git a/tests/api_resources/file_interpreter/test_from_docx.py b/tests/api_resources/file_interpreter/test_from_docx.py
index dd7dd534..898a16d4 100644
--- a/tests/api_resources/file_interpreter/test_from_docx.py
+++ b/tests/api_resources/file_interpreter/test_from_docx.py
@@ -19,14 +19,14 @@ class TestFromDocx:
@parametrize
def test_method_create(self, client: Maisa) -> None:
from_docx = client.file_interpreter.from_docx.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert_matches_type(object, from_docx, path=["response"])
@parametrize
def test_raw_response_create(self, client: Maisa) -> None:
response = client.file_interpreter.from_docx.with_raw_response.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert response.is_closed is True
@@ -37,7 +37,7 @@ def test_raw_response_create(self, client: Maisa) -> None:
@parametrize
def test_streaming_response_create(self, client: Maisa) -> None:
with client.file_interpreter.from_docx.with_streaming_response.create(
- file=b"raw file contents",
+ file=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -56,14 +56,14 @@ class TestAsyncFromDocx:
@parametrize
async def test_method_create(self, async_client: AsyncMaisa) -> None:
from_docx = await async_client.file_interpreter.from_docx.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert_matches_type(object, from_docx, path=["response"])
@parametrize
async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
response = await async_client.file_interpreter.from_docx.with_raw_response.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert response.is_closed is True
@@ -74,7 +74,7 @@ async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
@parametrize
async def test_streaming_response_create(self, async_client: AsyncMaisa) -> None:
async with async_client.file_interpreter.from_docx.with_streaming_response.create(
- file=b"raw file contents",
+ file=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
diff --git a/tests/api_resources/file_interpreter/test_from_html.py b/tests/api_resources/file_interpreter/test_from_html.py
index 5fed8217..e57b9463 100644
--- a/tests/api_resources/file_interpreter/test_from_html.py
+++ b/tests/api_resources/file_interpreter/test_from_html.py
@@ -19,14 +19,14 @@ class TestFromHTML:
@parametrize
def test_method_create(self, client: Maisa) -> None:
from_html = client.file_interpreter.from_html.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert_matches_type(object, from_html, path=["response"])
@parametrize
def test_raw_response_create(self, client: Maisa) -> None:
response = client.file_interpreter.from_html.with_raw_response.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert response.is_closed is True
@@ -37,7 +37,7 @@ def test_raw_response_create(self, client: Maisa) -> None:
@parametrize
def test_streaming_response_create(self, client: Maisa) -> None:
with client.file_interpreter.from_html.with_streaming_response.create(
- file=b"raw file contents",
+ file=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -56,14 +56,14 @@ class TestAsyncFromHTML:
@parametrize
async def test_method_create(self, async_client: AsyncMaisa) -> None:
from_html = await async_client.file_interpreter.from_html.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert_matches_type(object, from_html, path=["response"])
@parametrize
async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
response = await async_client.file_interpreter.from_html.with_raw_response.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert response.is_closed is True
@@ -74,7 +74,7 @@ async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
@parametrize
async def test_streaming_response_create(self, async_client: AsyncMaisa) -> None:
async with async_client.file_interpreter.from_html.with_streaming_response.create(
- file=b"raw file contents",
+ file=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
diff --git a/tests/api_resources/file_interpreter/test_from_image.py b/tests/api_resources/file_interpreter/test_from_image.py
index 4cb88359..c6c83868 100644
--- a/tests/api_resources/file_interpreter/test_from_image.py
+++ b/tests/api_resources/file_interpreter/test_from_image.py
@@ -19,14 +19,14 @@ class TestFromImage:
@parametrize
def test_method_create(self, client: Maisa) -> None:
from_image = client.file_interpreter.from_image.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert_matches_type(object, from_image, path=["response"])
@parametrize
def test_raw_response_create(self, client: Maisa) -> None:
response = client.file_interpreter.from_image.with_raw_response.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert response.is_closed is True
@@ -37,7 +37,7 @@ def test_raw_response_create(self, client: Maisa) -> None:
@parametrize
def test_streaming_response_create(self, client: Maisa) -> None:
with client.file_interpreter.from_image.with_streaming_response.create(
- file=b"raw file contents",
+ file=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -56,14 +56,14 @@ class TestAsyncFromImage:
@parametrize
async def test_method_create(self, async_client: AsyncMaisa) -> None:
from_image = await async_client.file_interpreter.from_image.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert_matches_type(object, from_image, path=["response"])
@parametrize
async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
response = await async_client.file_interpreter.from_image.with_raw_response.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert response.is_closed is True
@@ -74,7 +74,7 @@ async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
@parametrize
async def test_streaming_response_create(self, async_client: AsyncMaisa) -> None:
async with async_client.file_interpreter.from_image.with_streaming_response.create(
- file=b"raw file contents",
+ file=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
diff --git a/tests/api_resources/file_interpreter/test_from_pdf.py b/tests/api_resources/file_interpreter/test_from_pdf.py
index bd216b5d..a7076eee 100644
--- a/tests/api_resources/file_interpreter/test_from_pdf.py
+++ b/tests/api_resources/file_interpreter/test_from_pdf.py
@@ -19,14 +19,14 @@ class TestFromPdf:
@parametrize
def test_method_create(self, client: Maisa) -> None:
from_pdf = client.file_interpreter.from_pdf.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert_matches_type(object, from_pdf, path=["response"])
@parametrize
def test_method_create_with_all_params(self, client: Maisa) -> None:
from_pdf = client.file_interpreter.from_pdf.create(
- file=b"raw file contents",
+ file=b"Example data",
max_pages=0,
)
assert_matches_type(object, from_pdf, path=["response"])
@@ -34,7 +34,7 @@ def test_method_create_with_all_params(self, client: Maisa) -> None:
@parametrize
def test_raw_response_create(self, client: Maisa) -> None:
response = client.file_interpreter.from_pdf.with_raw_response.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert response.is_closed is True
@@ -45,7 +45,7 @@ def test_raw_response_create(self, client: Maisa) -> None:
@parametrize
def test_streaming_response_create(self, client: Maisa) -> None:
with client.file_interpreter.from_pdf.with_streaming_response.create(
- file=b"raw file contents",
+ file=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -64,14 +64,14 @@ class TestAsyncFromPdf:
@parametrize
async def test_method_create(self, async_client: AsyncMaisa) -> None:
from_pdf = await async_client.file_interpreter.from_pdf.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert_matches_type(object, from_pdf, path=["response"])
@parametrize
async def test_method_create_with_all_params(self, async_client: AsyncMaisa) -> None:
from_pdf = await async_client.file_interpreter.from_pdf.create(
- file=b"raw file contents",
+ file=b"Example data",
max_pages=0,
)
assert_matches_type(object, from_pdf, path=["response"])
@@ -79,7 +79,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncMaisa) ->
@parametrize
async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
response = await async_client.file_interpreter.from_pdf.with_raw_response.create(
- file=b"raw file contents",
+ file=b"Example data",
)
assert response.is_closed is True
@@ -90,7 +90,7 @@ async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
@parametrize
async def test_streaming_response_create(self, async_client: AsyncMaisa) -> None:
async with async_client.file_interpreter.from_pdf.with_streaming_response.create(
- file=b"raw file contents",
+ file=b"Example data",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
diff --git a/tests/api_resources/test_kpu.py b/tests/api_resources/test_kpu.py
index f38a52e5..b8f417c5 100644
--- a/tests/api_resources/test_kpu.py
+++ b/tests/api_resources/test_kpu.py
@@ -29,7 +29,7 @@ def test_method_run_with_all_params(self, client: Maisa) -> None:
query="query",
explain_steps=True,
retries=1,
- file=[b"raw file contents"],
+ file=[b"Example data"],
reasoner_model="gpt-4-turbo",
reasoner_prompt="reasoner_prompt",
)
@@ -78,7 +78,7 @@ async def test_method_run_with_all_params(self, async_client: AsyncMaisa) -> Non
query="query",
explain_steps=True,
retries=1,
- file=[b"raw file contents"],
+ file=[b"Example data"],
reasoner_model="gpt-4-turbo",
reasoner_prompt="reasoner_prompt",
)
From ffcb1b9bf363d82db2589fb46a2b62037a93d301 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 17 Mar 2026 11:58:39 +0000
Subject: [PATCH 171/200] fix(pydantic): do not pass `by_alias` unless set
---
src/maisa/_compat.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/maisa/_compat.py b/src/maisa/_compat.py
index 786ff42a..e6690a4f 100644
--- a/src/maisa/_compat.py
+++ b/src/maisa/_compat.py
@@ -2,7 +2,7 @@
from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload
from datetime import date, datetime
-from typing_extensions import Self, Literal
+from typing_extensions import Self, Literal, TypedDict
import pydantic
from pydantic.fields import FieldInfo
@@ -131,6 +131,10 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str:
return model.model_dump_json(indent=indent)
+class _ModelDumpKwargs(TypedDict, total=False):
+ by_alias: bool
+
+
def model_dump(
model: pydantic.BaseModel,
*,
@@ -142,6 +146,9 @@ def model_dump(
by_alias: bool | None = None,
) -> dict[str, Any]:
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
+ kwargs: _ModelDumpKwargs = {}
+ if by_alias is not None:
+ kwargs["by_alias"] = by_alias
return model.model_dump(
mode=mode,
exclude=exclude,
@@ -149,7 +156,7 @@ def model_dump(
exclude_defaults=exclude_defaults,
# warnings are not supported in Pydantic v1
warnings=True if PYDANTIC_V1 else warnings,
- by_alias=by_alias,
+ **kwargs,
)
return cast(
"dict[str, Any]",
From 962db793a2d9731f1476d714af2e87bcd49c507d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 17 Mar 2026 12:03:24 +0000
Subject: [PATCH 172/200] fix(deps): bump minimum typing-extensions version
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 9291645e..c09ed1c1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ authors = [
dependencies = [
"httpx>=0.23.0, <1",
"pydantic>=1.9.0, <3",
- "typing-extensions>=4.10, <5",
+ "typing-extensions>=4.14, <5",
"anyio>=3.5.0, <5",
"distro>=1.7.0, <2",
"sniffio",
From e1096d9e4bb5266c09324492f0beb08c01ea1cbd Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 17 Mar 2026 12:07:17 +0000
Subject: [PATCH 173/200] chore(internal): tweak CI branches
---
.github/workflows/ci.yml | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 451f028e..e3037cac 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,12 +1,14 @@
name: CI
on:
push:
- branches-ignore:
- - 'generated'
- - 'codegen/**'
- - 'integrated/**'
- - 'stl-preview-head/**'
- - 'stl-preview-base/**'
+ branches:
+ - '**'
+ - '!integrated/**'
+ - '!stl-preview-head/**'
+ - '!stl-preview-base/**'
+ - '!generated'
+ - '!codegen/**'
+ - 'codegen/stl/**'
pull_request:
branches-ignore:
- 'stl-preview-head/**'
From bc2d54370413ec0ed4cb8d295ecea529ec601e7f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 20 Mar 2026 03:37:51 +0000
Subject: [PATCH 174/200] fix: sanitize endpoint path params
---
src/maisa/_utils/__init__.py | 1 +
src/maisa/_utils/_path.py | 127 ++++++++++++++++++++++++++++++++++
tests/test_utils/test_path.py | 89 ++++++++++++++++++++++++
3 files changed, 217 insertions(+)
create mode 100644 src/maisa/_utils/_path.py
create mode 100644 tests/test_utils/test_path.py
diff --git a/src/maisa/_utils/__init__.py b/src/maisa/_utils/__init__.py
index dc64e29a..10cb66d2 100644
--- a/src/maisa/_utils/__init__.py
+++ b/src/maisa/_utils/__init__.py
@@ -1,3 +1,4 @@
+from ._path import path_template as path_template
from ._sync import asyncify as asyncify
from ._proxy import LazyProxy as LazyProxy
from ._utils import (
diff --git a/src/maisa/_utils/_path.py b/src/maisa/_utils/_path.py
new file mode 100644
index 00000000..4d6e1e4c
--- /dev/null
+++ b/src/maisa/_utils/_path.py
@@ -0,0 +1,127 @@
+from __future__ import annotations
+
+import re
+from typing import (
+ Any,
+ Mapping,
+ Callable,
+)
+from urllib.parse import quote
+
+# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E).
+_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$")
+
+_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}")
+
+
+def _quote_path_segment_part(value: str) -> str:
+ """Percent-encode `value` for use in a URI path segment.
+
+ Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe.
+ https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
+ """
+ # quote() already treats unreserved characters (letters, digits, and -._~)
+ # as safe, so we only need to add sub-delims, ':', and '@'.
+ # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted.
+ return quote(value, safe="!$&'()*+,;=:@")
+
+
+def _quote_query_part(value: str) -> str:
+ """Percent-encode `value` for use in a URI query string.
+
+ Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe.
+ https://datatracker.ietf.org/doc/html/rfc3986#section-3.4
+ """
+ return quote(value, safe="!$'()*+,;:@/?")
+
+
+def _quote_fragment_part(value: str) -> str:
+ """Percent-encode `value` for use in a URI fragment.
+
+ Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe.
+ https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
+ """
+ return quote(value, safe="!$&'()*+,;=:@/?")
+
+
+def _interpolate(
+ template: str,
+ values: Mapping[str, Any],
+ quoter: Callable[[str], str],
+) -> str:
+ """Replace {name} placeholders in `template`, quoting each value with `quoter`.
+
+ Placeholder names are looked up in `values`.
+
+ Raises:
+ KeyError: If a placeholder is not found in `values`.
+ """
+ # re.split with a capturing group returns alternating
+ # [text, name, text, name, ..., text] elements.
+ parts = _PLACEHOLDER_RE.split(template)
+
+ for i in range(1, len(parts), 2):
+ name = parts[i]
+ if name not in values:
+ raise KeyError(f"a value for placeholder {{{name}}} was not provided")
+ val = values[name]
+ if val is None:
+ parts[i] = "null"
+ elif isinstance(val, bool):
+ parts[i] = "true" if val else "false"
+ else:
+ parts[i] = quoter(str(values[name]))
+
+ return "".join(parts)
+
+
+def path_template(template: str, /, **kwargs: Any) -> str:
+ """Interpolate {name} placeholders in `template` from keyword arguments.
+
+ Args:
+ template: The template string containing {name} placeholders.
+ **kwargs: Keyword arguments to interpolate into the template.
+
+ Returns:
+ The template with placeholders interpolated and percent-encoded.
+
+ Safe characters for percent-encoding are dependent on the URI component.
+ Placeholders in path and fragment portions are percent-encoded where the `segment`
+ and `fragment` sets from RFC 3986 respectively are considered safe.
+ Placeholders in the query portion are percent-encoded where the `query` set from
+ RFC 3986 §3.3 is considered safe except for = and & characters.
+
+ Raises:
+ KeyError: If a placeholder is not found in `kwargs`.
+ ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments).
+ """
+ # Split the template into path, query, and fragment portions.
+ fragment_template: str | None = None
+ query_template: str | None = None
+
+ rest = template
+ if "#" in rest:
+ rest, fragment_template = rest.split("#", 1)
+ if "?" in rest:
+ rest, query_template = rest.split("?", 1)
+ path_template = rest
+
+ # Interpolate each portion with the appropriate quoting rules.
+ path_result = _interpolate(path_template, kwargs, _quote_path_segment_part)
+
+ # Reject dot-segments (. and ..) in the final assembled path. The check
+ # runs after interpolation so that adjacent placeholders or a mix of static
+ # text and placeholders that together form a dot-segment are caught.
+ # Also reject percent-encoded dot-segments to protect against incorrectly
+ # implemented normalization in servers/proxies.
+ for segment in path_result.split("/"):
+ if _DOT_SEGMENT_RE.match(segment):
+ raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed")
+
+ result = path_result
+ if query_template is not None:
+ result += "?" + _interpolate(query_template, kwargs, _quote_query_part)
+ if fragment_template is not None:
+ result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part)
+
+ return result
diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py
new file mode 100644
index 00000000..05846179
--- /dev/null
+++ b/tests/test_utils/test_path.py
@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+from typing import Any
+
+import pytest
+
+from maisa._utils._path import path_template
+
+
+@pytest.mark.parametrize(
+ "template, kwargs, expected",
+ [
+ ("/v1/{id}", dict(id="abc"), "/v1/abc"),
+ ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"),
+ ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"),
+ ("/{w}/{w}", dict(w="echo"), "/echo/echo"),
+ ("/v1/static", {}, "/v1/static"),
+ ("", {}, ""),
+ ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"),
+ ("/v1/{v}", dict(v=None), "/v1/null"),
+ ("/v1/{v}", dict(v=True), "/v1/true"),
+ ("/v1/{v}", dict(v=False), "/v1/false"),
+ ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok
+ ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok
+ ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok
+ ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok
+ ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine
+ (
+ "/v1/{a}?query={b}",
+ dict(a="../../other/endpoint", b="a&bad=true"),
+ "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue",
+ ),
+ ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"),
+ ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"),
+ ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"),
+ ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input
+ # Query: slash and ? are safe, # is not
+ ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"),
+ ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"),
+ ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"),
+ ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"),
+ # Fragment: slash and ? are safe
+ ("/docs#{v}", dict(v="a/b"), "/docs#a/b"),
+ ("/docs#{v}", dict(v="a?b"), "/docs#a?b"),
+ # Path: slash, ? and # are all encoded
+ ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"),
+ ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"),
+ ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"),
+ # same var encoded differently by component
+ (
+ "/v1/{v}?q={v}#{v}",
+ dict(v="a/b?c#d"),
+ "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d",
+ ),
+ ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection
+ ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection
+ ],
+)
+def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None:
+ assert path_template(template, **kwargs) == expected
+
+
+def test_missing_kwarg_raises_key_error() -> None:
+ with pytest.raises(KeyError, match="org_id"):
+ path_template("/v1/{org_id}")
+
+
+@pytest.mark.parametrize(
+ "template, kwargs",
+ [
+ ("{a}/path", dict(a=".")),
+ ("{a}/path", dict(a="..")),
+ ("/v1/{a}", dict(a=".")),
+ ("/v1/{a}", dict(a="..")),
+ ("/v1/{a}/path", dict(a=".")),
+ ("/v1/{a}/path", dict(a="..")),
+ ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".."
+ ("/v1/{a}.", dict(a=".")), # var + static → ".."
+ ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "."
+ ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text
+ ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static
+ ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static
+ ("/v1/{v}?q=1", dict(v="..")),
+ ("/v1/{v}#frag", dict(v="..")),
+ ],
+)
+def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None:
+ with pytest.raises(ValueError, match="dot-segment"):
+ path_template(template, **kwargs)
From 5a58be52061d49b7e66a03472fac1e7c0dff5692 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 20 Mar 2026 03:39:08 +0000
Subject: [PATCH 175/200] refactor(tests): switch from prism to steady
---
CONTRIBUTING.md | 2 +-
scripts/mock | 26 +++++++++++++-------------
scripts/test | 16 ++++++++--------
3 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 972498a9..df794a1b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -85,7 +85,7 @@ $ pip install ./path-to-wheel-file.whl
## Running tests
-Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests.
+Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests.
```sh
$ ./scripts/mock
diff --git a/scripts/mock b/scripts/mock
index bcf3b392..38201de8 100755
--- a/scripts/mock
+++ b/scripts/mock
@@ -19,34 +19,34 @@ fi
echo "==> Starting mock server with URL ${URL}"
-# Run prism mock on the given spec
+# Run steady mock on the given spec
if [ "$1" == "--daemon" ]; then
# Pre-install the package so the download doesn't eat into the startup timeout
- npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version
+ npm exec --package=@stdy/cli@0.19.3 -- steady --version
- npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log &
+ npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL" &> .stdy.log &
- # Wait for server to come online (max 30s)
+ # Wait for server to come online via health endpoint (max 30s)
echo -n "Waiting for server"
attempts=0
- while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do
+ while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do
+ if ! kill -0 $! 2>/dev/null; then
+ echo
+ cat .stdy.log
+ exit 1
+ fi
attempts=$((attempts + 1))
if [ "$attempts" -ge 300 ]; then
echo
- echo "Timed out waiting for Prism server to start"
- cat .prism.log
+ echo "Timed out waiting for Steady server to start"
+ cat .stdy.log
exit 1
fi
echo -n "."
sleep 0.1
done
- if grep -q "✖ fatal" ".prism.log"; then
- cat .prism.log
- exit 1
- fi
-
echo
else
- npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL"
+ npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL"
fi
diff --git a/scripts/test b/scripts/test
index dbeda2d2..2dfdc409 100755
--- a/scripts/test
+++ b/scripts/test
@@ -9,8 +9,8 @@ GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
-function prism_is_running() {
- curl --silent "http://localhost:4010" >/dev/null 2>&1
+function steady_is_running() {
+ curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1
}
kill_server_on_port() {
@@ -25,7 +25,7 @@ function is_overriding_api_base_url() {
[ -n "$TEST_API_BASE_URL" ]
}
-if ! is_overriding_api_base_url && ! prism_is_running ; then
+if ! is_overriding_api_base_url && ! steady_is_running ; then
# When we exit this script, make sure to kill the background mock server process
trap 'kill_server_on_port 4010' EXIT
@@ -36,19 +36,19 @@ fi
if is_overriding_api_base_url ; then
echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}"
echo
-elif ! prism_is_running ; then
- echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server"
+elif ! steady_is_running ; then
+ echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady server"
echo -e "running against your OpenAPI spec."
echo
echo -e "To run the server, pass in the path or url of your OpenAPI"
- echo -e "spec to the prism command:"
+ echo -e "spec to the steady command:"
echo
- echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}"
+ echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.3 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets${NC}"
echo
exit 1
else
- echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}"
+ echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}"
echo
fi
From 0d00146527d8479b8de6935ce309bdad7127439e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 21 Mar 2026 04:43:52 +0000
Subject: [PATCH 176/200] chore(tests): bump steady to v0.19.4
---
scripts/mock | 6 +++---
scripts/test | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/scripts/mock b/scripts/mock
index 38201de8..e1c19e88 100755
--- a/scripts/mock
+++ b/scripts/mock
@@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}"
# Run steady mock on the given spec
if [ "$1" == "--daemon" ]; then
# Pre-install the package so the download doesn't eat into the startup timeout
- npm exec --package=@stdy/cli@0.19.3 -- steady --version
+ npm exec --package=@stdy/cli@0.19.4 -- steady --version
- npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL" &> .stdy.log &
+ npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log &
# Wait for server to come online via health endpoint (max 30s)
echo -n "Waiting for server"
@@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then
echo
else
- npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL"
+ npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL"
fi
diff --git a/scripts/test b/scripts/test
index 2dfdc409..36fab0ae 100755
--- a/scripts/test
+++ b/scripts/test
@@ -43,7 +43,7 @@ elif ! steady_is_running ; then
echo -e "To run the server, pass in the path or url of your OpenAPI"
echo -e "spec to the steady command:"
echo
- echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.3 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets${NC}"
+ echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.4 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}"
echo
exit 1
From 9c4b717e3413b402df4804e038d305597f0d717d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 21 Mar 2026 04:49:23 +0000
Subject: [PATCH 177/200] chore(tests): bump steady to v0.19.5
---
scripts/mock | 6 +++---
scripts/test | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/scripts/mock b/scripts/mock
index e1c19e88..ab814d38 100755
--- a/scripts/mock
+++ b/scripts/mock
@@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}"
# Run steady mock on the given spec
if [ "$1" == "--daemon" ]; then
# Pre-install the package so the download doesn't eat into the startup timeout
- npm exec --package=@stdy/cli@0.19.4 -- steady --version
+ npm exec --package=@stdy/cli@0.19.5 -- steady --version
- npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log &
+ npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log &
# Wait for server to come online via health endpoint (max 30s)
echo -n "Waiting for server"
@@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then
echo
else
- npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL"
+ npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL"
fi
diff --git a/scripts/test b/scripts/test
index 36fab0ae..d1c8e1a9 100755
--- a/scripts/test
+++ b/scripts/test
@@ -43,7 +43,7 @@ elif ! steady_is_running ; then
echo -e "To run the server, pass in the path or url of your OpenAPI"
echo -e "spec to the steady command:"
echo
- echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.4 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}"
+ echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}"
echo
exit 1
From f6bfaaa900a2d42d0f5354f95cb11f93bf4b1f26 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 24 Mar 2026 04:49:05 +0000
Subject: [PATCH 178/200] chore(internal): update gitignore
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index 95ceb189..3824f4c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.prism.log
+.stdy.log
_dev
__pycache__
From c8b2598b31b1e4afe9fbb7c77269e8148b5805ce Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 24 Mar 2026 04:54:59 +0000
Subject: [PATCH 179/200] chore(tests): bump steady to v0.19.6
---
scripts/mock | 6 +++---
scripts/test | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/scripts/mock b/scripts/mock
index ab814d38..b319bdfb 100755
--- a/scripts/mock
+++ b/scripts/mock
@@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}"
# Run steady mock on the given spec
if [ "$1" == "--daemon" ]; then
# Pre-install the package so the download doesn't eat into the startup timeout
- npm exec --package=@stdy/cli@0.19.5 -- steady --version
+ npm exec --package=@stdy/cli@0.19.6 -- steady --version
- npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log &
+ npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log &
# Wait for server to come online via health endpoint (max 30s)
echo -n "Waiting for server"
@@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then
echo
else
- npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL"
+ npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL"
fi
diff --git a/scripts/test b/scripts/test
index d1c8e1a9..ab01948b 100755
--- a/scripts/test
+++ b/scripts/test
@@ -43,7 +43,7 @@ elif ! steady_is_running ; then
echo -e "To run the server, pass in the path or url of your OpenAPI"
echo -e "spec to the steady command:"
echo
- echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}"
+ echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}"
echo
exit 1
From c0ad5b1a1796acc1b8bf38b3a9a5186a668c3ea7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 25 Mar 2026 03:18:10 +0000
Subject: [PATCH 180/200] chore(ci): skip lint on metadata-only changes
Note that we still want to run tests, as these depend on the metadata.
---
.github/workflows/ci.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e3037cac..68df4eb3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,7 +19,7 @@ jobs:
timeout-minutes: 10
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/maisa-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
- if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
+ if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- uses: actions/checkout@v6
@@ -38,7 +38,7 @@ jobs:
run: ./scripts/lint
build:
- if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
+ if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
timeout-minutes: 10
name: build
permissions:
From 8add188e472fa959b15a17b2d5fa29e43645c1f2 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 25 Mar 2026 03:18:59 +0000
Subject: [PATCH 181/200] chore(tests): bump steady to v0.19.7
---
scripts/mock | 6 +++---
scripts/test | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/scripts/mock b/scripts/mock
index b319bdfb..09eb49f6 100755
--- a/scripts/mock
+++ b/scripts/mock
@@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}"
# Run steady mock on the given spec
if [ "$1" == "--daemon" ]; then
# Pre-install the package so the download doesn't eat into the startup timeout
- npm exec --package=@stdy/cli@0.19.6 -- steady --version
+ npm exec --package=@stdy/cli@0.19.7 -- steady --version
- npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log &
+ npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log &
# Wait for server to come online via health endpoint (max 30s)
echo -n "Waiting for server"
@@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then
echo
else
- npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL"
+ npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL"
fi
diff --git a/scripts/test b/scripts/test
index ab01948b..e46b9b58 100755
--- a/scripts/test
+++ b/scripts/test
@@ -43,7 +43,7 @@ elif ! steady_is_running ; then
echo -e "To run the server, pass in the path or url of your OpenAPI"
echo -e "spec to the steady command:"
echo
- echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}"
+ echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}"
echo
exit 1
From f8c80c9299fc534e593890f1a94f966f54fd9021 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 27 Mar 2026 06:11:38 +0000
Subject: [PATCH 182/200] feat(internal): implement indices array format for
query and form serialization
---
scripts/mock | 4 ++--
scripts/test | 2 +-
src/maisa/_qs.py | 5 ++++-
3 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/scripts/mock b/scripts/mock
index 09eb49f6..290e21b9 100755
--- a/scripts/mock
+++ b/scripts/mock
@@ -24,7 +24,7 @@ if [ "$1" == "--daemon" ]; then
# Pre-install the package so the download doesn't eat into the startup timeout
npm exec --package=@stdy/cli@0.19.7 -- steady --version
- npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log &
+ npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &
# Wait for server to come online via health endpoint (max 30s)
echo -n "Waiting for server"
@@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then
echo
else
- npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL"
+ npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
fi
diff --git a/scripts/test b/scripts/test
index e46b9b58..661f9bf4 100755
--- a/scripts/test
+++ b/scripts/test
@@ -43,7 +43,7 @@ elif ! steady_is_running ; then
echo -e "To run the server, pass in the path or url of your OpenAPI"
echo -e "spec to the steady command:"
echo
- echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}"
+ echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}"
echo
exit 1
diff --git a/src/maisa/_qs.py b/src/maisa/_qs.py
index ada6fd3f..de8c99bc 100644
--- a/src/maisa/_qs.py
+++ b/src/maisa/_qs.py
@@ -101,7 +101,10 @@ def _stringify_item(
items.extend(self._stringify_item(key, item, opts))
return items
elif array_format == "indices":
- raise NotImplementedError("The array indices format is not supported yet")
+ items = []
+ for i, item in enumerate(value):
+ items.extend(self._stringify_item(f"{key}[{i}]", item, opts))
+ return items
elif array_format == "brackets":
items = []
key = key + "[]"
From a192e9f545c0f1ab18423679c15f8dc5af994807 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 1 Apr 2026 08:13:35 +0000
Subject: [PATCH 183/200] chore(tests): bump steady to v0.20.1
---
scripts/mock | 6 +++---
scripts/test | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/scripts/mock b/scripts/mock
index 290e21b9..15c29941 100755
--- a/scripts/mock
+++ b/scripts/mock
@@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}"
# Run steady mock on the given spec
if [ "$1" == "--daemon" ]; then
# Pre-install the package so the download doesn't eat into the startup timeout
- npm exec --package=@stdy/cli@0.19.7 -- steady --version
+ npm exec --package=@stdy/cli@0.20.1 -- steady --version
- npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &
+ npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &
# Wait for server to come online via health endpoint (max 30s)
echo -n "Waiting for server"
@@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then
echo
else
- npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
+ npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
fi
diff --git a/scripts/test b/scripts/test
index 661f9bf4..c8e2e9d5 100755
--- a/scripts/test
+++ b/scripts/test
@@ -43,7 +43,7 @@ elif ! steady_is_running ; then
echo -e "To run the server, pass in the path or url of your OpenAPI"
echo -e "spec to the steady command:"
echo
- echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}"
+ echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}"
echo
exit 1
From 36436dbb74c63d0ed3778470a6621472d5dbdaac Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 1 Apr 2026 08:16:54 +0000
Subject: [PATCH 184/200] chore(tests): bump steady to v0.20.2
---
scripts/mock | 6 +++---
scripts/test | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/scripts/mock b/scripts/mock
index 15c29941..5cd7c157 100755
--- a/scripts/mock
+++ b/scripts/mock
@@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}"
# Run steady mock on the given spec
if [ "$1" == "--daemon" ]; then
# Pre-install the package so the download doesn't eat into the startup timeout
- npm exec --package=@stdy/cli@0.20.1 -- steady --version
+ npm exec --package=@stdy/cli@0.20.2 -- steady --version
- npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &
+ npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &
# Wait for server to come online via health endpoint (max 30s)
echo -n "Waiting for server"
@@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then
echo
else
- npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
+ npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
fi
diff --git a/scripts/test b/scripts/test
index c8e2e9d5..b8143aa3 100755
--- a/scripts/test
+++ b/scripts/test
@@ -43,7 +43,7 @@ elif ! steady_is_running ; then
echo -e "To run the server, pass in the path or url of your OpenAPI"
echo -e "spec to the steady command:"
echo
- echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}"
+ echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}"
echo
exit 1
From c5e67a181f290db65decd213b3e4dcd859c1a5fa Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 8 Apr 2026 04:53:48 +0000
Subject: [PATCH 185/200] fix(client): preserve hardcoded query params when
merging with user params
---
src/maisa/_base_client.py | 4 ++++
tests/test_client.py | 48 +++++++++++++++++++++++++++++++++++++++
2 files changed, 52 insertions(+)
diff --git a/src/maisa/_base_client.py b/src/maisa/_base_client.py
index aa2f358d..dd77d3e2 100644
--- a/src/maisa/_base_client.py
+++ b/src/maisa/_base_client.py
@@ -540,6 +540,10 @@ def _build_request(
files = cast(HttpxRequestFiles, ForceMultipartDict())
prepared_url = self._prepare_url(options.url)
+ # preserve hard-coded query params from the url
+ if params and prepared_url.query:
+ params = {**dict(prepared_url.params.items()), **params}
+ prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0])
if "_" in prepared_url.host:
# work around https://github.com/encode/httpx/discussions/2880
kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")}
diff --git a/tests/test_client.py b/tests/test_client.py
index 999c7a55..ea9d8fab 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -417,6 +417,30 @@ def test_default_query_option(self) -> None:
client.close()
+ def test_hardcoded_query_params_in_url(self, client: Maisa) -> None:
+ request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true"))
+ url = httpx.URL(request.url)
+ assert dict(url.params) == {"beta": "true"}
+
+ request = client._build_request(
+ FinalRequestOptions(
+ method="get",
+ url="/foo?beta=true",
+ params={"limit": "10", "page": "abc"},
+ )
+ )
+ url = httpx.URL(request.url)
+ assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"}
+
+ request = client._build_request(
+ FinalRequestOptions(
+ method="get",
+ url="/files/a%2Fb?beta=true",
+ params={"limit": "10"},
+ )
+ )
+ assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10"
+
def test_request_extra_json(self, client: Maisa) -> None:
request = client._build_request(
FinalRequestOptions(
@@ -1300,6 +1324,30 @@ async def test_default_query_option(self) -> None:
await client.close()
+ async def test_hardcoded_query_params_in_url(self, async_client: AsyncMaisa) -> None:
+ request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true"))
+ url = httpx.URL(request.url)
+ assert dict(url.params) == {"beta": "true"}
+
+ request = async_client._build_request(
+ FinalRequestOptions(
+ method="get",
+ url="/foo?beta=true",
+ params={"limit": "10", "page": "abc"},
+ )
+ )
+ url = httpx.URL(request.url)
+ assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"}
+
+ request = async_client._build_request(
+ FinalRequestOptions(
+ method="get",
+ url="/files/a%2Fb?beta=true",
+ params={"limit": "10"},
+ )
+ )
+ assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10"
+
def test_request_extra_json(self, client: Maisa) -> None:
request = client._build_request(
FinalRequestOptions(
From c98a1dabdf4406d438b132b5807dca9bc8e726a5 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 11 Apr 2026 08:22:08 +0000
Subject: [PATCH 186/200] fix: ensure file data are only sent as 1 parameter
---
src/maisa/_utils/_utils.py | 5 +++--
tests/test_extract_files.py | 9 +++++++++
2 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/src/maisa/_utils/_utils.py b/src/maisa/_utils/_utils.py
index eec7f4a1..63b8cd60 100644
--- a/src/maisa/_utils/_utils.py
+++ b/src/maisa/_utils/_utils.py
@@ -86,8 +86,9 @@ def _extract_items(
index += 1
if is_dict(obj):
try:
- # We are at the last entry in the path so we must remove the field
- if (len(path)) == index:
+ # Remove the field if there are no more dict keys in the path,
+ # only "" traversal markers or end.
+ if all(p == "" for p in path[index:]):
item = obj.pop(key)
else:
item = obj[key]
diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py
index 0765c49c..ba8f0859 100644
--- a/tests/test_extract_files.py
+++ b/tests/test_extract_files.py
@@ -35,6 +35,15 @@ def test_multiple_files() -> None:
assert query == {"documents": [{}, {}]}
+def test_top_level_file_array() -> None:
+ query = {"files": [b"file one", b"file two"], "title": "hello"}
+ assert extract_files(query, paths=[["files", ""]]) == [
+ ("files[]", b"file one"),
+ ("files[]", b"file two"),
+ ]
+ assert query == {"title": "hello"}
+
+
@pytest.mark.parametrize(
"query,paths,expected",
[
From 819e55d3f636d92d288b5b309a08f7272089ee24 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 11 Apr 2026 08:26:20 +0000
Subject: [PATCH 187/200] docs: update examples
---
README.md | 16 ++++-----
tests/api_resources/models/test_embeddings.py | 12 +++----
tests/api_resources/test_capabilities.py | 36 +++++++++----------
tests/test_client.py | 24 +++++++------
4 files changed, 46 insertions(+), 42 deletions(-)
diff --git a/README.md b/README.md
index 1fe3d1dd..17e3f6d1 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@ client = Maisa(
)
text_summary = client.capabilities.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
)
print(text_summary.summary)
```
@@ -59,7 +59,7 @@ client = AsyncMaisa(
async def main() -> None:
text_summary = await client.capabilities.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
)
print(text_summary.summary)
@@ -95,7 +95,7 @@ async def main() -> None:
http_client=DefaultAioHttpClient(),
) as client:
text_summary = await client.capabilities.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
)
print(text_summary.summary)
@@ -147,7 +147,7 @@ client = Maisa()
try:
client.capabilities.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
)
except maisa.APIConnectionError as e:
print("The server could not be reached")
@@ -192,7 +192,7 @@ client = Maisa(
# Or, configure per-request:
client.with_options(max_retries=5).capabilities.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
)
```
@@ -217,7 +217,7 @@ client = Maisa(
# Override per-request:
client.with_options(timeout=5.0).capabilities.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
)
```
@@ -260,7 +260,7 @@ from maisa import Maisa
client = Maisa()
response = client.capabilities.with_raw_response.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
)
print(response.headers.get('X-My-Header'))
@@ -280,7 +280,7 @@ To stream the response body, use `.with_streaming_response` instead, which requi
```python
with client.capabilities.with_streaming_response.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
) as response:
print(response.headers.get("X-My-Header"))
diff --git a/tests/api_resources/models/test_embeddings.py b/tests/api_resources/models/test_embeddings.py
index 110daa4b..b36da62a 100644
--- a/tests/api_resources/models/test_embeddings.py
+++ b/tests/api_resources/models/test_embeddings.py
@@ -20,14 +20,14 @@ class TestEmbeddings:
@parametrize
def test_method_create(self, client: Maisa) -> None:
embedding = client.models.embeddings.create(
- texts=["string"],
+ texts=["Who invented the light bulb?", "Hey, how are you?"],
)
assert_matches_type(Embeddings, embedding, path=["response"])
@parametrize
def test_raw_response_create(self, client: Maisa) -> None:
response = client.models.embeddings.with_raw_response.create(
- texts=["string"],
+ texts=["Who invented the light bulb?", "Hey, how are you?"],
)
assert response.is_closed is True
@@ -38,7 +38,7 @@ def test_raw_response_create(self, client: Maisa) -> None:
@parametrize
def test_streaming_response_create(self, client: Maisa) -> None:
with client.models.embeddings.with_streaming_response.create(
- texts=["string"],
+ texts=["Who invented the light bulb?", "Hey, how are you?"],
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -57,14 +57,14 @@ class TestAsyncEmbeddings:
@parametrize
async def test_method_create(self, async_client: AsyncMaisa) -> None:
embedding = await async_client.models.embeddings.create(
- texts=["string"],
+ texts=["Who invented the light bulb?", "Hey, how are you?"],
)
assert_matches_type(Embeddings, embedding, path=["response"])
@parametrize
async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
response = await async_client.models.embeddings.with_raw_response.create(
- texts=["string"],
+ texts=["Who invented the light bulb?", "Hey, how are you?"],
)
assert response.is_closed is True
@@ -75,7 +75,7 @@ async def test_raw_response_create(self, async_client: AsyncMaisa) -> None:
@parametrize
async def test_streaming_response_create(self, async_client: AsyncMaisa) -> None:
async with async_client.models.embeddings.with_streaming_response.create(
- texts=["string"],
+ texts=["Who invented the light bulb?", "Hey, how are you?"],
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
diff --git a/tests/api_resources/test_capabilities.py b/tests/api_resources/test_capabilities.py
index d84c5e14..2b81ac1f 100644
--- a/tests/api_resources/test_capabilities.py
+++ b/tests/api_resources/test_capabilities.py
@@ -88,7 +88,7 @@ def test_streaming_response_compare(self, client: Maisa) -> None:
@parametrize
def test_method_extract(self, client: Maisa) -> None:
capability = client.capabilities.extract(
- text="Example long text...",
+ text="My name is John Doe",
variables={
"name": {
"description": "The name of the person.",
@@ -101,7 +101,7 @@ def test_method_extract(self, client: Maisa) -> None:
@parametrize
def test_method_extract_with_all_params(self, client: Maisa) -> None:
capability = client.capabilities.extract(
- text="Example long text...",
+ text="My name is John Doe",
variables={
"name": {
"description": "The name of the person.",
@@ -115,7 +115,7 @@ def test_method_extract_with_all_params(self, client: Maisa) -> None:
@parametrize
def test_raw_response_extract(self, client: Maisa) -> None:
response = client.capabilities.with_raw_response.extract(
- text="Example long text...",
+ text="My name is John Doe",
variables={
"name": {
"description": "The name of the person.",
@@ -132,7 +132,7 @@ def test_raw_response_extract(self, client: Maisa) -> None:
@parametrize
def test_streaming_response_extract(self, client: Maisa) -> None:
with client.capabilities.with_streaming_response.extract(
- text="Example long text...",
+ text="My name is John Doe",
variables={
"name": {
"description": "The name of the person.",
@@ -151,17 +151,17 @@ def test_streaming_response_extract(self, client: Maisa) -> None:
@parametrize
def test_method_summarize(self, client: Maisa) -> None:
capability = client.capabilities.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
)
assert_matches_type(TextSummary, capability, path=["response"])
@parametrize
def test_method_summarize_with_all_params(self, client: Maisa) -> None:
capability = client.capabilities.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
format="paragraph",
lang="en",
- length="medium",
+ length="long",
summary_hint="Example summary of the text...",
)
assert_matches_type(TextSummary, capability, path=["response"])
@@ -169,7 +169,7 @@ def test_method_summarize_with_all_params(self, client: Maisa) -> None:
@parametrize
def test_raw_response_summarize(self, client: Maisa) -> None:
response = client.capabilities.with_raw_response.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
)
assert response.is_closed is True
@@ -180,7 +180,7 @@ def test_raw_response_summarize(self, client: Maisa) -> None:
@parametrize
def test_streaming_response_summarize(self, client: Maisa) -> None:
with client.capabilities.with_streaming_response.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -267,7 +267,7 @@ async def test_streaming_response_compare(self, async_client: AsyncMaisa) -> Non
@parametrize
async def test_method_extract(self, async_client: AsyncMaisa) -> None:
capability = await async_client.capabilities.extract(
- text="Example long text...",
+ text="My name is John Doe",
variables={
"name": {
"description": "The name of the person.",
@@ -280,7 +280,7 @@ async def test_method_extract(self, async_client: AsyncMaisa) -> None:
@parametrize
async def test_method_extract_with_all_params(self, async_client: AsyncMaisa) -> None:
capability = await async_client.capabilities.extract(
- text="Example long text...",
+ text="My name is John Doe",
variables={
"name": {
"description": "The name of the person.",
@@ -294,7 +294,7 @@ async def test_method_extract_with_all_params(self, async_client: AsyncMaisa) ->
@parametrize
async def test_raw_response_extract(self, async_client: AsyncMaisa) -> None:
response = await async_client.capabilities.with_raw_response.extract(
- text="Example long text...",
+ text="My name is John Doe",
variables={
"name": {
"description": "The name of the person.",
@@ -311,7 +311,7 @@ async def test_raw_response_extract(self, async_client: AsyncMaisa) -> None:
@parametrize
async def test_streaming_response_extract(self, async_client: AsyncMaisa) -> None:
async with async_client.capabilities.with_streaming_response.extract(
- text="Example long text...",
+ text="My name is John Doe",
variables={
"name": {
"description": "The name of the person.",
@@ -330,17 +330,17 @@ async def test_streaming_response_extract(self, async_client: AsyncMaisa) -> Non
@parametrize
async def test_method_summarize(self, async_client: AsyncMaisa) -> None:
capability = await async_client.capabilities.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
)
assert_matches_type(TextSummary, capability, path=["response"])
@parametrize
async def test_method_summarize_with_all_params(self, async_client: AsyncMaisa) -> None:
capability = await async_client.capabilities.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
format="paragraph",
lang="en",
- length="medium",
+ length="long",
summary_hint="Example summary of the text...",
)
assert_matches_type(TextSummary, capability, path=["response"])
@@ -348,7 +348,7 @@ async def test_method_summarize_with_all_params(self, async_client: AsyncMaisa)
@parametrize
async def test_raw_response_summarize(self, async_client: AsyncMaisa) -> None:
response = await async_client.capabilities.with_raw_response.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
)
assert response.is_closed is True
@@ -359,7 +359,7 @@ async def test_raw_response_summarize(self, async_client: AsyncMaisa) -> None:
@parametrize
async def test_streaming_response_summarize(self, async_client: AsyncMaisa) -> None:
async with async_client.capabilities.with_streaming_response.summarize(
- text="Example long text...",
+ text="Lorem Ipsum dolor sit amet",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
diff --git a/tests/test_client.py b/tests/test_client.py
index ea9d8fab..88ae8c67 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -866,7 +866,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien
respx_mock.post("/v1/capabilities/summarize").mock(side_effect=httpx.TimeoutException("Test timeout error"))
with pytest.raises(APITimeoutError):
- client.capabilities.with_streaming_response.summarize(text="Example long text...").__enter__()
+ client.capabilities.with_streaming_response.summarize(text="Lorem Ipsum dolor sit amet").__enter__()
assert _get_open_connections(client) == 0
@@ -876,7 +876,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client
respx_mock.post("/v1/capabilities/summarize").mock(return_value=httpx.Response(500))
with pytest.raises(APIStatusError):
- client.capabilities.with_streaming_response.summarize(text="Example long text...").__enter__()
+ client.capabilities.with_streaming_response.summarize(text="Lorem Ipsum dolor sit amet").__enter__()
assert _get_open_connections(client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@@ -905,7 +905,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
respx_mock.post("/v1/capabilities/summarize").mock(side_effect=retry_handler)
- response = client.capabilities.with_raw_response.summarize(text="Example long text...")
+ response = client.capabilities.with_raw_response.summarize(text="Lorem Ipsum dolor sit amet")
assert response.retries_taken == failures_before_success
assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
@@ -928,7 +928,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
respx_mock.post("/v1/capabilities/summarize").mock(side_effect=retry_handler)
response = client.capabilities.with_raw_response.summarize(
- text="Example long text...", extra_headers={"x-stainless-retry-count": Omit()}
+ text="Lorem Ipsum dolor sit amet", extra_headers={"x-stainless-retry-count": Omit()}
)
assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0
@@ -953,7 +953,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
respx_mock.post("/v1/capabilities/summarize").mock(side_effect=retry_handler)
response = client.capabilities.with_raw_response.summarize(
- text="Example long text...", extra_headers={"x-stainless-retry-count": "42"}
+ text="Lorem Ipsum dolor sit amet", extra_headers={"x-stainless-retry-count": "42"}
)
assert response.http_request.headers.get("x-stainless-retry-count") == "42"
@@ -1786,7 +1786,9 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter,
respx_mock.post("/v1/capabilities/summarize").mock(side_effect=httpx.TimeoutException("Test timeout error"))
with pytest.raises(APITimeoutError):
- await async_client.capabilities.with_streaming_response.summarize(text="Example long text...").__aenter__()
+ await async_client.capabilities.with_streaming_response.summarize(
+ text="Lorem Ipsum dolor sit amet"
+ ).__aenter__()
assert _get_open_connections(async_client) == 0
@@ -1796,7 +1798,9 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter,
respx_mock.post("/v1/capabilities/summarize").mock(return_value=httpx.Response(500))
with pytest.raises(APIStatusError):
- await async_client.capabilities.with_streaming_response.summarize(text="Example long text...").__aenter__()
+ await async_client.capabilities.with_streaming_response.summarize(
+ text="Lorem Ipsum dolor sit amet"
+ ).__aenter__()
assert _get_open_connections(async_client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@@ -1825,7 +1829,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
respx_mock.post("/v1/capabilities/summarize").mock(side_effect=retry_handler)
- response = await client.capabilities.with_raw_response.summarize(text="Example long text...")
+ response = await client.capabilities.with_raw_response.summarize(text="Lorem Ipsum dolor sit amet")
assert response.retries_taken == failures_before_success
assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
@@ -1850,7 +1854,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
respx_mock.post("/v1/capabilities/summarize").mock(side_effect=retry_handler)
response = await client.capabilities.with_raw_response.summarize(
- text="Example long text...", extra_headers={"x-stainless-retry-count": Omit()}
+ text="Lorem Ipsum dolor sit amet", extra_headers={"x-stainless-retry-count": Omit()}
)
assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0
@@ -1875,7 +1879,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
respx_mock.post("/v1/capabilities/summarize").mock(side_effect=retry_handler)
response = await client.capabilities.with_raw_response.summarize(
- text="Example long text...", extra_headers={"x-stainless-retry-count": "42"}
+ text="Lorem Ipsum dolor sit amet", extra_headers={"x-stainless-retry-count": "42"}
)
assert response.http_request.headers.get("x-stainless-retry-count") == "42"
From 9a6858defce8c5bffe40725c26b0ac47edd725f4 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 18 Apr 2026 07:41:41 +0000
Subject: [PATCH 188/200] perf(client): optimize file structure copying in
multipart requests
---
src/maisa/_files.py | 56 ++++++++++-
src/maisa/_utils/__init__.py | 1 -
src/maisa/_utils/_utils.py | 15 ---
src/maisa/resources/capabilities/media.py | 33 ++++---
.../resources/file_interpreter/from_audio.py | 7 +-
.../resources/file_interpreter/from_docx.py | 7 +-
.../resources/file_interpreter/from_html.py | 7 +-
.../resources/file_interpreter/from_image.py | 7 +-
.../resources/file_interpreter/from_pdf.py | 7 +-
src/maisa/resources/kpu.py | 13 ++-
tests/test_deepcopy.py | 58 -----------
tests/test_files.py | 99 ++++++++++++++++++-
12 files changed, 199 insertions(+), 111 deletions(-)
delete mode 100644 tests/test_deepcopy.py
diff --git a/src/maisa/_files.py b/src/maisa/_files.py
index 5bff5118..3d17ee3a 100644
--- a/src/maisa/_files.py
+++ b/src/maisa/_files.py
@@ -3,8 +3,8 @@
import io
import os
import pathlib
-from typing import overload
-from typing_extensions import TypeGuard
+from typing import Sequence, cast, overload
+from typing_extensions import TypeVar, TypeGuard
import anyio
@@ -17,7 +17,9 @@
HttpxFileContent,
HttpxRequestFiles,
)
-from ._utils import is_tuple_t, is_mapping_t, is_sequence_t
+from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t
+
+_T = TypeVar("_T")
def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]:
@@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent:
return await anyio.Path(file).read_bytes()
return file
+
+
+def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T:
+ """Copy only the containers along the given paths.
+
+ Used to guard against mutation by extract_files without copying the entire structure.
+ Only dicts and lists that lie on a path are copied; everything else
+ is returned by reference.
+
+ For example, given paths=[["foo", "files", "file"]] and the structure:
+ {
+ "foo": {
+ "bar": {"baz": {}},
+ "files": {"file": }
+ }
+ }
+ The root dict, "foo", and "files" are copied (they lie on the path).
+ "bar" and "baz" are returned by reference (off the path).
+ """
+ return _deepcopy_with_paths(item, paths, 0)
+
+
+def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T:
+ if not paths:
+ return item
+ if is_mapping(item):
+ key_to_paths: dict[str, list[Sequence[str]]] = {}
+ for path in paths:
+ if index < len(path):
+ key_to_paths.setdefault(path[index], []).append(path)
+
+ # if no path continues through this mapping, it won't be mutated and copying it is redundant
+ if not key_to_paths:
+ return item
+
+ result = dict(item)
+ for key, subpaths in key_to_paths.items():
+ if key in result:
+ result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1)
+ return cast(_T, result)
+ if is_list(item):
+ array_paths = [path for path in paths if index < len(path) and path[index] == ""]
+
+ # if no path expects a list here, nothing will be mutated inside it - return by reference
+ if not array_paths:
+ return cast(_T, item)
+ return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item])
+ return item
diff --git a/src/maisa/_utils/__init__.py b/src/maisa/_utils/__init__.py
index 10cb66d2..1c090e51 100644
--- a/src/maisa/_utils/__init__.py
+++ b/src/maisa/_utils/__init__.py
@@ -24,7 +24,6 @@
coerce_integer as coerce_integer,
file_from_path as file_from_path,
strip_not_given as strip_not_given,
- deepcopy_minimal as deepcopy_minimal,
get_async_library as get_async_library,
maybe_coerce_float as maybe_coerce_float,
get_required_header as get_required_header,
diff --git a/src/maisa/_utils/_utils.py b/src/maisa/_utils/_utils.py
index 63b8cd60..771859f5 100644
--- a/src/maisa/_utils/_utils.py
+++ b/src/maisa/_utils/_utils.py
@@ -177,21 +177,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]:
return isinstance(obj, Iterable)
-def deepcopy_minimal(item: _T) -> _T:
- """Minimal reimplementation of copy.deepcopy() that will only copy certain object types:
-
- - mappings, e.g. `dict`
- - list
-
- This is done for performance reasons.
- """
- if is_mapping(item):
- return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()})
- if is_list(item):
- return cast(_T, [deepcopy_minimal(entry) for entry in item])
- return item
-
-
# copied from https://github.com/Rapptz/RoboDanny
def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str:
size = len(seq)
diff --git a/src/maisa/resources/capabilities/media.py b/src/maisa/resources/capabilities/media.py
index d53ac9a2..225252ce 100644
--- a/src/maisa/resources/capabilities/media.py
+++ b/src/maisa/resources/capabilities/media.py
@@ -7,8 +7,9 @@
import httpx
+from ..._files import deepcopy_with_paths
from ..._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given
-from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
+from ..._utils import extract_files, maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -116,7 +117,7 @@ def compare(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal(
+ body = deepcopy_with_paths(
{
"file1": file1,
"file2": file2,
@@ -134,7 +135,8 @@ def compare(
"variable4_description": variable4_description,
"variable4_name": variable4_name,
"variable4_type": variable4_type,
- }
+ },
+ [["file1"], ["file2"]],
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file1"], ["file2"]])
# It should be noted that the actual Content-Type header that will be
@@ -217,7 +219,7 @@ def extract(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal(
+ body = deepcopy_with_paths(
{
"file": file,
"lang": lang,
@@ -233,7 +235,8 @@ def extract(
"variable4_description": variable4_description,
"variable4_name": variable4_name,
"variable4_type": variable4_type,
- }
+ },
+ [["file"]],
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
@@ -289,14 +292,15 @@ def summarize(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal(
+ body = deepcopy_with_paths(
{
"file": file,
"format": format,
"lang": lang,
"length": length,
"summary_hint": summary_hint,
- }
+ },
+ [["file"]],
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
@@ -404,7 +408,7 @@ async def compare(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal(
+ body = deepcopy_with_paths(
{
"file1": file1,
"file2": file2,
@@ -422,7 +426,8 @@ async def compare(
"variable4_description": variable4_description,
"variable4_name": variable4_name,
"variable4_type": variable4_type,
- }
+ },
+ [["file1"], ["file2"]],
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file1"], ["file2"]])
# It should be noted that the actual Content-Type header that will be
@@ -505,7 +510,7 @@ async def extract(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal(
+ body = deepcopy_with_paths(
{
"file": file,
"lang": lang,
@@ -521,7 +526,8 @@ async def extract(
"variable4_description": variable4_description,
"variable4_name": variable4_name,
"variable4_type": variable4_type,
- }
+ },
+ [["file"]],
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
@@ -577,14 +583,15 @@ async def summarize(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal(
+ body = deepcopy_with_paths(
{
"file": file,
"format": format,
"lang": lang,
"length": length,
"summary_hint": summary_hint,
- }
+ },
+ [["file"]],
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
diff --git a/src/maisa/resources/file_interpreter/from_audio.py b/src/maisa/resources/file_interpreter/from_audio.py
index 149ed68b..bdd43cd6 100644
--- a/src/maisa/resources/file_interpreter/from_audio.py
+++ b/src/maisa/resources/file_interpreter/from_audio.py
@@ -6,8 +6,9 @@
import httpx
+from ..._files import deepcopy_with_paths
from ..._types import Body, Query, Headers, NotGiven, FileTypes, not_given
-from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
+from ..._utils import extract_files, maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -65,7 +66,7 @@ def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal({"file": file})
+ body = deepcopy_with_paths({"file": file}, [["file"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
@@ -125,7 +126,7 @@ async def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal({"file": file})
+ body = deepcopy_with_paths({"file": file}, [["file"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
diff --git a/src/maisa/resources/file_interpreter/from_docx.py b/src/maisa/resources/file_interpreter/from_docx.py
index c799b57b..87866e23 100644
--- a/src/maisa/resources/file_interpreter/from_docx.py
+++ b/src/maisa/resources/file_interpreter/from_docx.py
@@ -6,8 +6,9 @@
import httpx
+from ..._files import deepcopy_with_paths
from ..._types import Body, Query, Headers, NotGiven, FileTypes, not_given
-from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
+from ..._utils import extract_files, maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -65,7 +66,7 @@ def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal({"file": file})
+ body = deepcopy_with_paths({"file": file}, [["file"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
@@ -125,7 +126,7 @@ async def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal({"file": file})
+ body = deepcopy_with_paths({"file": file}, [["file"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
diff --git a/src/maisa/resources/file_interpreter/from_html.py b/src/maisa/resources/file_interpreter/from_html.py
index 38e4fe7b..5e004ba5 100644
--- a/src/maisa/resources/file_interpreter/from_html.py
+++ b/src/maisa/resources/file_interpreter/from_html.py
@@ -6,8 +6,9 @@
import httpx
+from ..._files import deepcopy_with_paths
from ..._types import Body, Query, Headers, NotGiven, FileTypes, not_given
-from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
+from ..._utils import extract_files, maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -65,7 +66,7 @@ def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal({"file": file})
+ body = deepcopy_with_paths({"file": file}, [["file"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
@@ -125,7 +126,7 @@ async def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal({"file": file})
+ body = deepcopy_with_paths({"file": file}, [["file"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
diff --git a/src/maisa/resources/file_interpreter/from_image.py b/src/maisa/resources/file_interpreter/from_image.py
index 5686c97e..f01fcbc9 100644
--- a/src/maisa/resources/file_interpreter/from_image.py
+++ b/src/maisa/resources/file_interpreter/from_image.py
@@ -6,8 +6,9 @@
import httpx
+from ..._files import deepcopy_with_paths
from ..._types import Body, Query, Headers, NotGiven, FileTypes, not_given
-from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
+from ..._utils import extract_files, maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -65,7 +66,7 @@ def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal({"file": file})
+ body = deepcopy_with_paths({"file": file}, [["file"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
@@ -125,7 +126,7 @@ async def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal({"file": file})
+ body = deepcopy_with_paths({"file": file}, [["file"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
diff --git a/src/maisa/resources/file_interpreter/from_pdf.py b/src/maisa/resources/file_interpreter/from_pdf.py
index 20191f10..81138fbe 100644
--- a/src/maisa/resources/file_interpreter/from_pdf.py
+++ b/src/maisa/resources/file_interpreter/from_pdf.py
@@ -6,8 +6,9 @@
import httpx
+from ..._files import deepcopy_with_paths
from ..._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given
-from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
+from ..._utils import extract_files, maybe_transform, async_maybe_transform
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -66,7 +67,7 @@ def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal({"file": file})
+ body = deepcopy_with_paths({"file": file}, [["file"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
@@ -131,7 +132,7 @@ async def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal({"file": file})
+ body = deepcopy_with_paths({"file": file}, [["file"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
diff --git a/src/maisa/resources/kpu.py b/src/maisa/resources/kpu.py
index e0c0f5de..22b8b8f5 100644
--- a/src/maisa/resources/kpu.py
+++ b/src/maisa/resources/kpu.py
@@ -8,8 +8,9 @@
import httpx
from ..types import kpu_run_params
+from .._files import deepcopy_with_paths
from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, SequenceNotStr, omit, not_given
-from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
+from .._utils import extract_files, maybe_transform, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
from .._response import (
@@ -96,13 +97,14 @@ def run(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal(
+ body = deepcopy_with_paths(
{
"query": query,
"file": file,
"reasoner_model": reasoner_model,
"reasoner_prompt": reasoner_prompt,
- }
+ },
+ [["file", ""]],
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file", ""]])
# It should be noted that the actual Content-Type header that will be
@@ -203,13 +205,14 @@ async def run(
timeout: Override the client-level default timeout for this request, in seconds
"""
- body = deepcopy_minimal(
+ body = deepcopy_with_paths(
{
"query": query,
"file": file,
"reasoner_model": reasoner_model,
"reasoner_prompt": reasoner_prompt,
- }
+ },
+ [["file", ""]],
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file", ""]])
# It should be noted that the actual Content-Type header that will be
diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py
deleted file mode 100644
index 3697c8c9..00000000
--- a/tests/test_deepcopy.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from maisa._utils import deepcopy_minimal
-
-
-def assert_different_identities(obj1: object, obj2: object) -> None:
- assert obj1 == obj2
- assert id(obj1) != id(obj2)
-
-
-def test_simple_dict() -> None:
- obj1 = {"foo": "bar"}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
-
-
-def test_nested_dict() -> None:
- obj1 = {"foo": {"bar": True}}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert_different_identities(obj1["foo"], obj2["foo"])
-
-
-def test_complex_nested_dict() -> None:
- obj1 = {"foo": {"bar": [{"hello": "world"}]}}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert_different_identities(obj1["foo"], obj2["foo"])
- assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"])
- assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0])
-
-
-def test_simple_list() -> None:
- obj1 = ["a", "b", "c"]
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
-
-
-def test_nested_list() -> None:
- obj1 = ["a", [1, 2, 3]]
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert_different_identities(obj1[1], obj2[1])
-
-
-class MyObject: ...
-
-
-def test_ignores_other_types() -> None:
- # custom classes
- my_obj = MyObject()
- obj1 = {"foo": my_obj}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert obj1["foo"] is my_obj
-
- # tuples
- obj3 = ("a", "b")
- obj4 = deepcopy_minimal(obj3)
- assert obj3 is obj4
diff --git a/tests/test_files.py b/tests/test_files.py
index 340832e6..d448ce0d 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -4,7 +4,8 @@
import pytest
from dirty_equals import IsDict, IsList, IsBytes, IsTuple
-from maisa._files import to_httpx_files, async_to_httpx_files
+from maisa._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files
+from maisa._utils import extract_files
readme_path = Path(__file__).parent.parent.joinpath("README.md")
@@ -49,3 +50,99 @@ def test_string_not_allowed() -> None:
"file": "foo", # type: ignore
}
)
+
+
+def assert_different_identities(obj1: object, obj2: object) -> None:
+ assert obj1 == obj2
+ assert obj1 is not obj2
+
+
+class TestDeepcopyWithPaths:
+ def test_copies_top_level_dict(self) -> None:
+ original = {"file": b"data", "other": "value"}
+ result = deepcopy_with_paths(original, [["file"]])
+ assert_different_identities(result, original)
+
+ def test_file_value_is_same_reference(self) -> None:
+ file_bytes = b"contents"
+ original = {"file": file_bytes}
+ result = deepcopy_with_paths(original, [["file"]])
+ assert_different_identities(result, original)
+ assert result["file"] is file_bytes
+
+ def test_list_popped_wholesale(self) -> None:
+ files = [b"f1", b"f2"]
+ original = {"files": files, "title": "t"}
+ result = deepcopy_with_paths(original, [["files", ""]])
+ assert_different_identities(result, original)
+ result_files = result["files"]
+ assert isinstance(result_files, list)
+ assert_different_identities(result_files, files)
+
+ def test_nested_array_path_copies_list_and_elements(self) -> None:
+ elem1 = {"file": b"f1", "extra": 1}
+ elem2 = {"file": b"f2", "extra": 2}
+ original = {"items": [elem1, elem2]}
+ result = deepcopy_with_paths(original, [["items", "", "file"]])
+ assert_different_identities(result, original)
+ result_items = result["items"]
+ assert isinstance(result_items, list)
+ assert_different_identities(result_items, original["items"])
+ assert_different_identities(result_items[0], elem1)
+ assert_different_identities(result_items[1], elem2)
+
+ def test_empty_paths_returns_same_object(self) -> None:
+ original = {"foo": "bar"}
+ result = deepcopy_with_paths(original, [])
+ assert result is original
+
+ def test_multiple_paths(self) -> None:
+ f1 = b"file1"
+ f2 = b"file2"
+ original = {"a": f1, "b": f2, "c": "unchanged"}
+ result = deepcopy_with_paths(original, [["a"], ["b"]])
+ assert_different_identities(result, original)
+ assert result["a"] is f1
+ assert result["b"] is f2
+ assert result["c"] is original["c"]
+
+ def test_extract_files_does_not_mutate_original_top_level(self) -> None:
+ file_bytes = b"contents"
+ original = {"file": file_bytes, "other": "value"}
+
+ copied = deepcopy_with_paths(original, [["file"]])
+ extracted = extract_files(copied, paths=[["file"]])
+
+ assert extracted == [("file", file_bytes)]
+ assert original == {"file": file_bytes, "other": "value"}
+ assert copied == {"other": "value"}
+
+ def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None:
+ file1 = b"f1"
+ file2 = b"f2"
+ original = {
+ "items": [
+ {"file": file1, "extra": 1},
+ {"file": file2, "extra": 2},
+ ],
+ "title": "example",
+ }
+
+ copied = deepcopy_with_paths(original, [["items", "", "file"]])
+ extracted = extract_files(copied, paths=[["items", "", "file"]])
+
+ assert extracted == [("items[][file]", file1), ("items[][file]", file2)]
+ assert original == {
+ "items": [
+ {"file": file1, "extra": 1},
+ {"file": file2, "extra": 2},
+ ],
+ "title": "example",
+ }
+ assert copied == {
+ "items": [
+ {"extra": 1},
+ {"extra": 2},
+ ],
+ "title": "example",
+ }
From 50901a8ac0737687c112895ef10218d6f5d5ee81 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 18 Apr 2026 07:43:56 +0000
Subject: [PATCH 189/200] chore(tests): bump steady to v0.22.1
---
scripts/mock | 6 +++---
scripts/test | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/scripts/mock b/scripts/mock
index 5cd7c157..feebe5ed 100755
--- a/scripts/mock
+++ b/scripts/mock
@@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}"
# Run steady mock on the given spec
if [ "$1" == "--daemon" ]; then
# Pre-install the package so the download doesn't eat into the startup timeout
- npm exec --package=@stdy/cli@0.20.2 -- steady --version
+ npm exec --package=@stdy/cli@0.22.1 -- steady --version
- npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &
+ npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &
# Wait for server to come online via health endpoint (max 30s)
echo -n "Waiting for server"
@@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then
echo
else
- npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
+ npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
fi
diff --git a/scripts/test b/scripts/test
index b8143aa3..19acc916 100755
--- a/scripts/test
+++ b/scripts/test
@@ -43,7 +43,7 @@ elif ! steady_is_running ; then
echo -e "To run the server, pass in the path or url of your OpenAPI"
echo -e "spec to the steady command:"
echo
- echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}"
+ echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}"
echo
exit 1
From 9cd364b6ff6fab42f437930408238c1fe1707bd7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 23 Apr 2026 03:56:32 +0000
Subject: [PATCH 190/200] chore(internal): more robust bootstrap script
---
scripts/bootstrap | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/bootstrap b/scripts/bootstrap
index b430fee3..fe8451e4 100755
--- a/scripts/bootstrap
+++ b/scripts/bootstrap
@@ -4,7 +4,7 @@ set -e
cd "$(dirname "$0")/.."
-if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then
+if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then
brew bundle check >/dev/null 2>&1 || {
echo -n "==> Install Homebrew dependencies? (y/N): "
read -r response
From 7e2bc16aadbc08804f9ef07e4ac901cd57673d9d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 28 Apr 2026 03:59:53 +0000
Subject: [PATCH 191/200] fix: use correct field name format for multipart file
arrays
---
src/maisa/_qs.py | 8 ++-----
src/maisa/_types.py | 3 +++
src/maisa/_utils/_utils.py | 42 ++++++++++++++++++++++++++++++-------
tests/test_extract_files.py | 28 ++++++++++++++++++++-----
tests/test_files.py | 2 +-
5 files changed, 63 insertions(+), 20 deletions(-)
diff --git a/src/maisa/_qs.py b/src/maisa/_qs.py
index de8c99bc..4127c19c 100644
--- a/src/maisa/_qs.py
+++ b/src/maisa/_qs.py
@@ -2,17 +2,13 @@
from typing import Any, List, Tuple, Union, Mapping, TypeVar
from urllib.parse import parse_qs, urlencode
-from typing_extensions import Literal, get_args
+from typing_extensions import get_args
-from ._types import NotGiven, not_given
+from ._types import NotGiven, ArrayFormat, NestedFormat, not_given
from ._utils import flatten
_T = TypeVar("_T")
-
-ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
-NestedFormat = Literal["dots", "brackets"]
-
PrimitiveData = Union[str, int, float, bool, None]
# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"]
# https://github.com/microsoft/pyright/issues/3555
diff --git a/src/maisa/_types.py b/src/maisa/_types.py
index e54fd14a..b43da775 100644
--- a/src/maisa/_types.py
+++ b/src/maisa/_types.py
@@ -47,6 +47,9 @@
ModelT = TypeVar("ModelT", bound=pydantic.BaseModel)
_T = TypeVar("_T")
+ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
+NestedFormat = Literal["dots", "brackets"]
+
# Approximates httpx internal ProxiesTypes and RequestFiles types
# while adding support for `PathLike` instances
diff --git a/src/maisa/_utils/_utils.py b/src/maisa/_utils/_utils.py
index 771859f5..199cd231 100644
--- a/src/maisa/_utils/_utils.py
+++ b/src/maisa/_utils/_utils.py
@@ -17,11 +17,11 @@
)
from pathlib import Path
from datetime import date, datetime
-from typing_extensions import TypeGuard
+from typing_extensions import TypeGuard, get_args
import sniffio
-from .._types import Omit, NotGiven, FileTypes, HeadersLike
+from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike
_T = TypeVar("_T")
_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...])
@@ -40,25 +40,45 @@ def extract_files(
query: Mapping[str, object],
*,
paths: Sequence[Sequence[str]],
+ array_format: ArrayFormat = "brackets",
) -> list[tuple[str, FileTypes]]:
"""Recursively extract files from the given dictionary based on specified paths.
A path may look like this ['foo', 'files', '', 'data'].
+ ``array_format`` controls how ```` segments contribute to the emitted
+ field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and
+ ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``).
+
Note: this mutates the given dictionary.
"""
files: list[tuple[str, FileTypes]] = []
for path in paths:
- files.extend(_extract_items(query, path, index=0, flattened_key=None))
+ files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format))
return files
+def _array_suffix(array_format: ArrayFormat, array_index: int) -> str:
+ if array_format == "brackets":
+ return "[]"
+ if array_format == "indices":
+ return f"[{array_index}]"
+ if array_format == "repeat" or array_format == "comma":
+ # Both repeat the bare field name for each file part; there is no
+ # meaningful way to comma-join binary parts.
+ return ""
+ raise NotImplementedError(
+ f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}"
+ )
+
+
def _extract_items(
obj: object,
path: Sequence[str],
*,
index: int,
flattened_key: str | None,
+ array_format: ArrayFormat,
) -> list[tuple[str, FileTypes]]:
try:
key = path[index]
@@ -75,9 +95,11 @@ def _extract_items(
if is_list(obj):
files: list[tuple[str, FileTypes]] = []
- for entry in obj:
- assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "")
- files.append((flattened_key + "[]", cast(FileTypes, entry)))
+ for array_index, entry in enumerate(obj):
+ suffix = _array_suffix(array_format, array_index)
+ emitted_key = (flattened_key + suffix) if flattened_key else suffix
+ assert_is_file_content(entry, key=emitted_key)
+ files.append((emitted_key, cast(FileTypes, entry)))
return files
assert_is_file_content(obj, key=flattened_key)
@@ -106,6 +128,7 @@ def _extract_items(
path,
index=index,
flattened_key=flattened_key,
+ array_format=array_format,
)
elif is_list(obj):
if key != "":
@@ -117,9 +140,12 @@ def _extract_items(
item,
path,
index=index,
- flattened_key=flattened_key + "[]" if flattened_key is not None else "[]",
+ flattened_key=(
+ (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index)
+ ),
+ array_format=array_format,
)
- for item in obj
+ for array_index, item in enumerate(obj)
]
)
diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py
index ba8f0859..98da03c4 100644
--- a/tests/test_extract_files.py
+++ b/tests/test_extract_files.py
@@ -4,7 +4,7 @@
import pytest
-from maisa._types import FileTypes
+from maisa._types import FileTypes, ArrayFormat
from maisa._utils import extract_files
@@ -37,10 +37,7 @@ def test_multiple_files() -> None:
def test_top_level_file_array() -> None:
query = {"files": [b"file one", b"file two"], "title": "hello"}
- assert extract_files(query, paths=[["files", ""]]) == [
- ("files[]", b"file one"),
- ("files[]", b"file two"),
- ]
+ assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")]
assert query == {"title": "hello"}
@@ -71,3 +68,24 @@ def test_ignores_incorrect_paths(
expected: list[tuple[str, FileTypes]],
) -> None:
assert extract_files(query, paths=paths) == expected
+
+
+@pytest.mark.parametrize(
+ "array_format,expected_top_level,expected_nested",
+ [
+ ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]),
+ ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]),
+ ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]),
+ ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]),
+ ],
+)
+def test_array_format_controls_file_field_names(
+ array_format: ArrayFormat,
+ expected_top_level: list[tuple[str, FileTypes]],
+ expected_nested: list[tuple[str, FileTypes]],
+) -> None:
+ top_level = {"files": [b"a", b"b"]}
+ assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level
+
+ nested = {"items": [{"file": b"a"}, {"file": b"b"}]}
+ assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested
diff --git a/tests/test_files.py b/tests/test_files.py
index d448ce0d..2d68c618 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -131,7 +131,7 @@ def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None:
copied = deepcopy_with_paths(original, [["items", "", "file"]])
extracted = extract_files(copied, paths=[["items", "", "file"]])
- assert extracted == [("items[][file]", file1), ("items[][file]", file2)]
+ assert [entry for _, entry in extracted] == [file1, file2]
assert original == {
"items": [
{"file": file1, "extra": 1},
From c87da55481f8d827b67ee9831a946dfce275ffbd Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 28 Apr 2026 04:01:55 +0000
Subject: [PATCH 192/200] feat: support setting headers via env
---
src/maisa/_client.py | 24 +++++++++++++++++++++++-
1 file changed, 23 insertions(+), 1 deletion(-)
diff --git a/src/maisa/_client.py b/src/maisa/_client.py
index 63907cb2..7405bb49 100644
--- a/src/maisa/_client.py
+++ b/src/maisa/_client.py
@@ -19,7 +19,11 @@
RequestOptions,
not_given,
)
-from ._utils import is_given, get_async_library
+from ._utils import (
+ is_given,
+ is_mapping_t,
+ get_async_library,
+)
from ._compat import cached_property
from ._version import __version__
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
@@ -84,6 +88,15 @@ def __init__(
if base_url is None:
base_url = f"https://api.maisa.ai"
+ custom_headers_env = os.environ.get("MAISA_CUSTOM_HEADERS")
+ if custom_headers_env is not None:
+ parsed: dict[str, str] = {}
+ for line in custom_headers_env.split("\n"):
+ colon = line.find(":")
+ if colon >= 0:
+ parsed[line[:colon].strip()] = line[colon + 1 :].strip()
+ default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})}
+
super().__init__(
version=__version__,
base_url=base_url,
@@ -276,6 +289,15 @@ def __init__(
if base_url is None:
base_url = f"https://api.maisa.ai"
+ custom_headers_env = os.environ.get("MAISA_CUSTOM_HEADERS")
+ if custom_headers_env is not None:
+ parsed: dict[str, str] = {}
+ for line in custom_headers_env.split("\n"):
+ colon = line.find(":")
+ if colon >= 0:
+ parsed[line[:colon].strip()] = line[colon + 1 :].strip()
+ default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})}
+
super().__init__(
version=__version__,
base_url=base_url,
From 96de242682f156be89cf0ddd301a296dfdfc1cd1 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 30 Apr 2026 06:09:16 +0000
Subject: [PATCH 193/200] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 817e9225..2fed3f1b 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 13
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/maisa%2Fmaisa-c82e0a80b379d33d11af783865ec15844bcd750043bf0f1543ebdf72bda79f3a.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/maisa/maisa-c82e0a80b379d33d11af783865ec15844bcd750043bf0f1543ebdf72bda79f3a.yml
openapi_spec_hash: 67e2baafe7455c2a734ac0b0ea699719
config_hash: d6a8d922bf8b98716e3f55b9c829cd45
From 2303aa002345157e77c75b7ed802238bfaf7e2fb Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 1 May 2026 02:03:51 +0000
Subject: [PATCH 194/200] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 2fed3f1b..cfa854aa 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 13
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/maisa/maisa-c82e0a80b379d33d11af783865ec15844bcd750043bf0f1543ebdf72bda79f3a.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/maisa/maisa-d1dd8e6736faa244dcf5cd516d0e2463cb22f1aba163ab329430582d7bd0d8c7.yml
openapi_spec_hash: 67e2baafe7455c2a734ac0b0ea699719
config_hash: d6a8d922bf8b98716e3f55b9c829cd45
From dcc2d4664581a32d2c53c3860f6de76b6e8ffb6f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 1 May 2026 02:06:05 +0000
Subject: [PATCH 195/200] chore(internal): reformat pyproject.toml
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index c09ed1c1..24bf8b4a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -168,7 +168,7 @@ show_error_codes = true
#
# We also exclude our `tests` as mypy doesn't always infer
# types correctly and Pyright will still catch any type errors.
-exclude = ['src/maisa/_files.py', '_dev/.*.py', 'tests/.*']
+exclude = ["src/maisa/_files.py", "_dev/.*.py", "tests/.*"]
strict_equality = true
implicit_reexport = true
From 803d63add1fd7b844004892b7970c5a9308164e0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 9 May 2026 02:02:06 +0000
Subject: [PATCH 196/200] fix(client): add missing f-string prefix in file type
error message
---
src/maisa/_files.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/maisa/_files.py b/src/maisa/_files.py
index 3d17ee3a..5a84d9a6 100644
--- a/src/maisa/_files.py
+++ b/src/maisa/_files.py
@@ -99,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles
elif is_sequence_t(files):
files = [(key, await _async_transform_file(file)) for key, file in files]
else:
- raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence")
+ raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence")
return files
From 60ebd0d3f03aaabc2e21e74a9d51b2e790e5c98d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 12 May 2026 02:03:10 +0000
Subject: [PATCH 197/200] feat(internal/types): support eagerly validating
pydantic iterators
---
src/maisa/_models.py | 80 ++++++++++++++++++++++++++++++++++++++++++++
tests/test_models.py | 60 +++++++++++++++++++++++++++++++--
2 files changed, 137 insertions(+), 3 deletions(-)
diff --git a/src/maisa/_models.py b/src/maisa/_models.py
index 29070e05..8c5ab260 100644
--- a/src/maisa/_models.py
+++ b/src/maisa/_models.py
@@ -25,7 +25,9 @@
ClassVar,
Protocol,
Required,
+ Annotated,
ParamSpec,
+ TypeAlias,
TypedDict,
TypeGuard,
final,
@@ -79,7 +81,15 @@
from ._constants import RAW_RESPONSE_HEADER
if TYPE_CHECKING:
+ from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler
+ from pydantic_core import CoreSchema, core_schema
from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema
+else:
+ try:
+ from pydantic_core import CoreSchema, core_schema
+ except ImportError:
+ CoreSchema = None
+ core_schema = None
__all__ = ["BaseModel", "GenericModel"]
@@ -396,6 +406,76 @@ def model_dump_json(
)
+class _EagerIterable(list[_T], Generic[_T]):
+ """
+ Accepts any Iterable[T] input (including generators), consumes it
+ eagerly, and validates all items upfront.
+
+ Validation preserves the original container type where possible
+ (e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON)
+ always emits a list — round-tripping through model_dump() will not
+ restore the original container type.
+ """
+
+ @classmethod
+ def __get_pydantic_core_schema__(
+ cls,
+ source_type: Any,
+ handler: GetCoreSchemaHandler,
+ ) -> CoreSchema:
+ (item_type,) = get_args(source_type) or (Any,)
+ item_schema: CoreSchema = handler.generate_schema(item_type)
+ list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema)
+
+ return core_schema.no_info_wrap_validator_function(
+ cls._validate,
+ list_of_items_schema,
+ serialization=core_schema.plain_serializer_function_ser_schema(
+ cls._serialize,
+ info_arg=False,
+ ),
+ )
+
+ @staticmethod
+ def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any:
+ original_type: type[Any] = type(v)
+
+ # Normalize to list so list_schema can validate each item
+ if isinstance(v, list):
+ items: list[_T] = v
+ else:
+ try:
+ items = list(v)
+ except TypeError as e:
+ raise TypeError("Value is not iterable") from e
+
+ # Validate items against the inner schema
+ validated: list[_T] = handler(items)
+
+ # Reconstruct original container type
+ if original_type is list:
+ return validated
+ # str(list) produces the list's repr, not a string built from items,
+ # so skip reconstruction for str and its subclasses.
+ if issubclass(original_type, str):
+ return validated
+ try:
+ return original_type(validated)
+ except (TypeError, ValueError):
+ # If the type cannot be reconstructed, just return the validated list
+ return validated
+
+ @staticmethod
+ def _serialize(v: Iterable[_T]) -> list[_T]:
+ """Always serialize as a list so Pydantic's JSON encoder is happy."""
+ if isinstance(v, list):
+ return v
+ return list(v)
+
+
+EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable]
+
+
def _construct_field(value: object, field: FieldInfo, key: str) -> object:
if value is None:
return field_get_default(field)
diff --git a/tests/test_models.py b/tests/test_models.py
index 855edf2c..11a5c92d 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,7 +1,8 @@
import json
-from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast
+from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast
from datetime import datetime, timezone
-from typing_extensions import Literal, Annotated, TypeAliasType
+from collections import deque
+from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType
import pytest
import pydantic
@@ -9,7 +10,7 @@
from maisa._utils import PropertyInfo
from maisa._compat import PYDANTIC_V1, parse_obj, model_dump, model_json
-from maisa._models import DISCRIMINATOR_CACHE, BaseModel, construct_type
+from maisa._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type
class BasicModel(BaseModel):
@@ -961,3 +962,56 @@ def __getattr__(self, attr: str) -> Item: ...
assert model.a.prop == 1
assert isinstance(model.a, Item)
assert model.other == "foo"
+
+
+# NOTE: Workaround for Pydantic Iterable behavior.
+# Iterable fields are replaced with a ValidatorIterator and may be consumed
+# during serialization, which can cause subsequent dumps to return empty data.
+# See: https://github.com/pydantic/pydantic/issues/9541
+@pytest.mark.parametrize(
+ "data, expected_validated",
+ [
+ ([1, 2, 3], [1, 2, 3]),
+ ((1, 2, 3), (1, 2, 3)),
+ (set([1, 2, 3]), set([1, 2, 3])),
+ (iter([1, 2, 3]), [1, 2, 3]),
+ ([], []),
+ ((x for x in [1, 2, 3]), [1, 2, 3]),
+ (map(lambda x: x, [1, 2, 3]), [1, 2, 3]),
+ (frozenset([1, 2, 3]), frozenset([1, 2, 3])),
+ (deque([1, 2, 3]), deque([1, 2, 3])),
+ ],
+ ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"],
+)
+@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2")
+def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None:
+ class TypeWithIterable(TypedDict):
+ items: EagerIterable[int]
+
+ class Model(BaseModel):
+ data: TypeWithIterable
+
+ m = Model.model_validate({"data": {"items": data}})
+ assert m.data["items"] == expected_validated
+
+ # Verify repeated dumps don't lose data (the original bug)
+ assert m.model_dump()["data"]["items"] == list(expected_validated)
+ assert m.model_dump()["data"]["items"] == list(expected_validated)
+
+
+@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2")
+def test_iterable_construction_str_falls_back_to_list() -> None:
+ # str is iterable (over chars), but str(list_of_chars) produces the list's repr
+ # rather than reconstructing a string from items. We special-case str to fall
+ # back to list instead of attempting reconstruction.
+ class TypeWithIterable(TypedDict):
+ items: EagerIterable[str]
+
+ class Model(BaseModel):
+ data: TypeWithIterable
+
+ m = Model.model_validate({"data": {"items": "hello"}})
+
+ # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"])
+ assert m.data["items"] == ["h", "e", "l", "l", "o"]
+ assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"]
From b0d9444cae275688fd74b2445d5e4eeb5432d00d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 13 May 2026 02:00:47 +0000
Subject: [PATCH 198/200] ci: pin GitHub Actions to commit SHAs
Pin all GitHub Actions referenced in generated workflows (both
first-party `actions/*` and third-party) to immutable commit SHAs.
Updating pinned actions is now a deliberate codegen-side bump rather
than implicit on every workflow run.
---
.github/workflows/ci.yml | 8 ++++----
.github/workflows/publish-pypi.yml | 2 +-
.github/workflows/release-doctor.yml | 2 +-
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 68df4eb3..27ba89fb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,7 +21,7 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/maisa-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rye
run: |
@@ -46,7 +46,7 @@ jobs:
id-token: write
runs-on: ${{ github.repository == 'stainless-sdks/maisa-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rye
run: |
@@ -67,7 +67,7 @@ jobs:
github.repository == 'stainless-sdks/maisa-python' &&
!startsWith(github.ref, 'refs/heads/stl/')
id: github-oidc
- uses: actions/github-script@v8
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: core.setOutput('github_token', await core.getIDToken());
@@ -87,7 +87,7 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/maisa-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rye
run: |
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index b5a491fb..be9cd0aa 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rye
run: |
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index a0b29d24..3225ef94 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'maisaai/python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check release environment
run: |
From 34286185744ee73293b5b161b6e82aa4ebcf1dcf Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 18 Jul 2026 02:00:23 +0000
Subject: [PATCH 199/200] feat(stlc): configurable CI runner and
private-production-repo support in workflow templates
---
.github/workflows/ci.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 27ba89fb..c7cedea1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,7 +18,7 @@ jobs:
lint:
timeout-minutes: 10
name: lint
- runs-on: ${{ github.repository == 'stainless-sdks/maisa-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -44,7 +44,7 @@ jobs:
permissions:
contents: read
id-token: write
- runs-on: ${{ github.repository == 'stainless-sdks/maisa-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -84,7 +84,7 @@ jobs:
test:
timeout-minutes: 10
name: test
- runs-on: ${{ github.repository == 'stainless-sdks/maisa-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
From 81af0f1c33b654ccc7c5f33c2fa9e3822d95b635 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 18 Jul 2026 02:01:49 +0000
Subject: [PATCH 200/200] release: 0.1.0-alpha.5
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 221 ++++++++++++++++++++++++++++++++++
pyproject.toml | 2 +-
src/maisa/_version.py | 2 +-
4 files changed, 224 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index b56c3d0b..e8285b71 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.1.0-alpha.4"
+ ".": "0.1.0-alpha.5"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2970d300..0e2dc639 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,226 @@
# Changelog
+## 0.1.0-alpha.5 (2026-07-18)
+
+Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/maisaai/python-sdk/compare/v0.1.0-alpha.4...v0.1.0-alpha.5)
+
+### Features
+
+* **api:** OpenAPI spec update via Stainless API ([#18](https://github.com/maisaai/python-sdk/issues/18)) ([51c4025](https://github.com/maisaai/python-sdk/commit/51c40252cc130836f517ec20601618b67f5b5c43))
+* **api:** OpenAPI spec update via Stainless API ([#21](https://github.com/maisaai/python-sdk/issues/21)) ([4b94f71](https://github.com/maisaai/python-sdk/commit/4b94f710dfcf6e0a3155ee1328d96dafe7b26efa))
+* clean up environment call outs ([ad83736](https://github.com/maisaai/python-sdk/commit/ad83736f3015d86a20ebc71886a0e817d243f6f8))
+* **client:** add custom JSON encoder for extended type support ([55a31ca](https://github.com/maisaai/python-sdk/commit/55a31ca42d8156d80dafa60b950f0eb786189c8e))
+* **client:** add follow_redirects request option ([cbd27b4](https://github.com/maisaai/python-sdk/commit/cbd27b4498b8cefde68fd1a1dfd1078ed7d29767))
+* **client:** add support for aiohttp ([393e393](https://github.com/maisaai/python-sdk/commit/393e393190cdf75a676f6d786837fe25a3e1978e))
+* **client:** add support for binary request streaming ([04eb2a7](https://github.com/maisaai/python-sdk/commit/04eb2a7ffc3b23c07c88f0d0a4e97a07a0b0f803))
+* **client:** allow passing `NotGiven` for body ([#66](https://github.com/maisaai/python-sdk/issues/66)) ([c53de39](https://github.com/maisaai/python-sdk/commit/c53de39f59d976f8c2e3540c6254a0ec19de2e05))
+* **client:** send `X-Stainless-Read-Timeout` header ([#60](https://github.com/maisaai/python-sdk/issues/60)) ([69e514f](https://github.com/maisaai/python-sdk/commit/69e514f736ef246a99c7b842521cf59a751a7814))
+* **client:** support file upload requests ([09e05f0](https://github.com/maisaai/python-sdk/commit/09e05f0a711d7852e36aa117a55f8fb8ca33a252))
+* improve future compat with pydantic v3 ([93d8fb9](https://github.com/maisaai/python-sdk/commit/93d8fb9eb67510674a5c60a96cd9e9eca1ef0330))
+* **internal/types:** support eagerly validating pydantic iterators ([60ebd0d](https://github.com/maisaai/python-sdk/commit/60ebd0d3f03aaabc2e21e74a9d51b2e790e5c98d))
+* **internal:** implement indices array format for query and form serialization ([f8c80c9](https://github.com/maisaai/python-sdk/commit/f8c80c9299fc534e593890f1a94f966f54fd9021))
+* **stlc:** configurable CI runner and private-production-repo support in workflow templates ([3428618](https://github.com/maisaai/python-sdk/commit/34286185744ee73293b5b161b6e82aa4ebcf1dcf))
+* support setting headers via env ([c87da55](https://github.com/maisaai/python-sdk/commit/c87da55481f8d827b67ee9831a946dfce275ffbd))
+* **types:** replace List[str] with SequenceNotStr in params ([ae478df](https://github.com/maisaai/python-sdk/commit/ae478dfce2707ec1b9133c1d5c5d2cc418b2e1df))
+
+
+### Bug Fixes
+
+* asyncify on non-asyncio runtimes ([#64](https://github.com/maisaai/python-sdk/issues/64)) ([f7ad873](https://github.com/maisaai/python-sdk/commit/f7ad8737d8a8996062f6dc62e9ad8ecf34456dd5))
+* avoid newer type syntax ([1bed539](https://github.com/maisaai/python-sdk/commit/1bed5391d47d402c8a2e77685db12221538b2071))
+* **ci:** correct conditional ([df34ac8](https://github.com/maisaai/python-sdk/commit/df34ac81de4f6cdbf96190ebe4b8e03149551d2f))
+* **ci:** ensure pip is always available ([#77](https://github.com/maisaai/python-sdk/issues/77)) ([5eb597e](https://github.com/maisaai/python-sdk/commit/5eb597ec123f7ca80ccefbefaeda0a9397297c9e))
+* **ci:** release-doctor — report correct token name ([a22ef2e](https://github.com/maisaai/python-sdk/commit/a22ef2e16a3862b31350e11f330a05dcee2fe723))
+* **ci:** remove publishing patch ([#78](https://github.com/maisaai/python-sdk/issues/78)) ([b142c88](https://github.com/maisaai/python-sdk/commit/b142c8802df54c2601023ad97841be29c9605ae4))
+* **client:** add missing f-string prefix in file type error message ([803d63a](https://github.com/maisaai/python-sdk/commit/803d63add1fd7b844004892b7970c5a9308164e0))
+* **client:** close streams without requiring full consumption ([63670ce](https://github.com/maisaai/python-sdk/commit/63670ce664ebc59df42945101b871968b9ae0a9c))
+* **client:** compat with new httpx 0.28.0 release ([#31](https://github.com/maisaai/python-sdk/issues/31)) ([3494f25](https://github.com/maisaai/python-sdk/commit/3494f25077638c28d293cae09a251c6d6a75fd0e))
+* **client:** correctly parse binary response | stream ([0762f8d](https://github.com/maisaai/python-sdk/commit/0762f8da514337c02d925be41cfbcd799fc3439d))
+* **client:** don't send Content-Type header on GET requests ([791e4fc](https://github.com/maisaai/python-sdk/commit/791e4fcef7112f4089aa115deca5c16a938255b7))
+* **client:** mark some request bodies as optional ([c53de39](https://github.com/maisaai/python-sdk/commit/c53de39f59d976f8c2e3540c6254a0ec19de2e05))
+* **client:** only call .close() when needed ([#48](https://github.com/maisaai/python-sdk/issues/48)) ([2a8b3ea](https://github.com/maisaai/python-sdk/commit/2a8b3eae602602a012db86751b7569129fb1a02c))
+* **client:** preserve hardcoded query params when merging with user params ([c5e67a1](https://github.com/maisaai/python-sdk/commit/c5e67a181f290db65decd213b3e4dcd859c1a5fa))
+* compat with Python 3.14 ([988f89c](https://github.com/maisaai/python-sdk/commit/988f89c86baafbfeb4d718b01e3af07cc239a458))
+* **compat:** update signatures of `model_dump` and `model_dump_json` for Pydantic v1 ([7d0d7e4](https://github.com/maisaai/python-sdk/commit/7d0d7e45141ce9c33de4f842ca40fd8634af0cc5))
+* correctly handle deserialising `cls` fields ([#51](https://github.com/maisaai/python-sdk/issues/51)) ([8bab70c](https://github.com/maisaai/python-sdk/commit/8bab70c60cd7bf9e441173a0273ce17e0f41a302))
+* **deps:** bump minimum typing-extensions version ([962db79](https://github.com/maisaai/python-sdk/commit/962db793a2d9731f1476d714af2e87bcd49c507d))
+* **docs/api:** remove references to nonexistent types ([28735f6](https://github.com/maisaai/python-sdk/commit/28735f60ec7129a1026a1b361a66fb034ccf6584))
+* ensure file data are only sent as 1 parameter ([c98a1da](https://github.com/maisaai/python-sdk/commit/c98a1dabdf4406d438b132b5807dca9bc8e726a5))
+* ensure streams are always closed ([53847ee](https://github.com/maisaai/python-sdk/commit/53847ee697e0392299c5bd8c5b76cfb729b36d9c))
+* **package:** support direct resource imports ([9443d25](https://github.com/maisaai/python-sdk/commit/9443d25d49504fcdc76e6c7fe435339113500146))
+* **parsing:** correctly handle nested discriminated unions ([4a0795e](https://github.com/maisaai/python-sdk/commit/4a0795e78702d1851afacb07c8e853a71821f6e2))
+* **parsing:** ignore empty metadata ([8babd3c](https://github.com/maisaai/python-sdk/commit/8babd3cc1a7a134467ec0dc009b7a1ba6cb99ad1))
+* **parsing:** parse extra field types ([da719a3](https://github.com/maisaai/python-sdk/commit/da719a30f9c3425e2ab68efc80be15ceba4dfad6))
+* **perf:** optimize some hot paths ([c0a0d6e](https://github.com/maisaai/python-sdk/commit/c0a0d6e0b5a5a3327f2e50c5e5d87f1db62abd97))
+* **perf:** skip traversing types for NotGiven values ([96b5e8a](https://github.com/maisaai/python-sdk/commit/96b5e8a122394ac86460aec62d0319bb81b8b0a5))
+* **pydantic v1:** more robust ModelField.annotation check ([dcf4095](https://github.com/maisaai/python-sdk/commit/dcf4095858cbd3c8794eb38ba5092e4040e7efe3))
+* **pydantic:** do not pass `by_alias` unless set ([ffcb1b9](https://github.com/maisaai/python-sdk/commit/ffcb1b9bf363d82db2589fb46a2b62037a93d301))
+* sanitize endpoint path params ([bc2d543](https://github.com/maisaai/python-sdk/commit/bc2d54370413ec0ed4cb8d295ecea529ec601e7f))
+* **tests:** fix: tests which call HTTP endpoints directly with the example parameters ([1edfc13](https://github.com/maisaai/python-sdk/commit/1edfc1391bb51fad2f3f0468eae25b24e7124549))
+* **tests:** make test_get_platform less flaky ([#54](https://github.com/maisaai/python-sdk/issues/54)) ([0f53e2e](https://github.com/maisaai/python-sdk/commit/0f53e2eaf845b8631222747adbed6aef26e71aef))
+* **types:** allow pyright to infer TypedDict types within SequenceNotStr ([e03ff7f](https://github.com/maisaai/python-sdk/commit/e03ff7f9b4857533b050ec72e13e9a397eb2f2fa))
+* **types:** handle more discriminated union shapes ([#76](https://github.com/maisaai/python-sdk/issues/76)) ([4c58555](https://github.com/maisaai/python-sdk/commit/4c5855527fa35f1de85c19f3eb44080c96671fe8))
+* use async_to_httpx_files in patch method ([6a0bda4](https://github.com/maisaai/python-sdk/commit/6a0bda4b3294efa0605f660582b837656049eb1b))
+* use correct field name format for multipart file arrays ([7e2bc16](https://github.com/maisaai/python-sdk/commit/7e2bc16aadbc08804f9ef07e4ac901cd57673d9d))
+
+
+### Performance Improvements
+
+* **client:** optimize file structure copying in multipart requests ([9a6858d](https://github.com/maisaai/python-sdk/commit/9a6858defce8c5bffe40725c26b0ac47edd725f4))
+
+
+### Chores
+
+* add missing docstrings ([109587c](https://github.com/maisaai/python-sdk/commit/109587c0f7075c7af5c4679318a44d5696a1627f))
+* add missing isclass check ([#46](https://github.com/maisaai/python-sdk/issues/46)) ([0e94ac2](https://github.com/maisaai/python-sdk/commit/0e94ac22ca8e33653888f357526433a23e82dad0))
+* add Python 3.14 classifier and testing ([46aed18](https://github.com/maisaai/python-sdk/commit/46aed18f409d684470aa0ebc0a13b5b6ff9bd0dc))
+* broadly detect json family of content-type headers ([484ef55](https://github.com/maisaai/python-sdk/commit/484ef558aedf0bfe5b0a34e302b22254f0170963))
+* bump `httpx-aiohttp` version to 0.1.9 ([c632881](https://github.com/maisaai/python-sdk/commit/c632881aaf640e18337951f9a8ca0a7f219f45dc))
+* **ci:** add timeout thresholds for CI jobs ([c64f88b](https://github.com/maisaai/python-sdk/commit/c64f88bcb87e36d04d3965dd3a1cd9394782b0ae))
+* **ci:** change upload type ([d6ecb98](https://github.com/maisaai/python-sdk/commit/d6ecb98f9571ba8513deb7cb640f2ca60f473b32))
+* **ci:** enable for pull requests ([4d45823](https://github.com/maisaai/python-sdk/commit/4d4582354137ab157b489745da734da45250aa09))
+* **ci:** fix installation instructions ([3ec60bf](https://github.com/maisaai/python-sdk/commit/3ec60bfd974811e6ed892566497405a2388a6ede))
+* **ci:** only run for pushes and fork pull requests ([cb8d889](https://github.com/maisaai/python-sdk/commit/cb8d889f01ce3e4b516b45218bcfeb556970992a))
+* **ci:** only use depot for staging repos ([a34edb6](https://github.com/maisaai/python-sdk/commit/a34edb6a27cc6c6365749cc45ebb3a7ae13d480e))
+* **ci:** skip lint on metadata-only changes ([c0ad5b1](https://github.com/maisaai/python-sdk/commit/c0ad5b1a1796acc1b8bf38b3a9a5186a668c3ea7))
+* **ci:** skip uploading artifacts on stainless-internal branches ([6378752](https://github.com/maisaai/python-sdk/commit/637875251e913836866694cc72a0bc67291eb0af))
+* **ci:** upgrade `actions/github-script` ([7ab563f](https://github.com/maisaai/python-sdk/commit/7ab563f9ddaad08e5fc28fc0a91eca7f2a9b048b))
+* **ci:** upload sdks to package manager ([92b578e](https://github.com/maisaai/python-sdk/commit/92b578ebad4bd611eb0b7e28890cdb2f40f3622f))
+* **client:** minor internal fixes ([46c6012](https://github.com/maisaai/python-sdk/commit/46c60128342a86388db165058fd8801da92e8d33))
+* **deps:** mypy 1.18.1 has a regression, pin to 1.17 ([d98efa4](https://github.com/maisaai/python-sdk/commit/d98efa461f0f5f492281fc3605c8fc187b61e804))
+* do not install brew dependencies in ./scripts/bootstrap by default ([e355179](https://github.com/maisaai/python-sdk/commit/e355179123c014ed8cef32ac224a89a5e7532016))
+* **docs:** grammar improvements ([365638e](https://github.com/maisaai/python-sdk/commit/365638e4c43e29b6d8384a782eb2f54abeff608e))
+* **docs:** remove reference to rye shell ([df13a62](https://github.com/maisaai/python-sdk/commit/df13a62c2c7ed732f3c4fed57ace19569caac052))
+* **docs:** update client docstring ([#70](https://github.com/maisaai/python-sdk/issues/70)) ([7044c85](https://github.com/maisaai/python-sdk/commit/7044c85821ce9bb9837e6d7069af88249c4a983a))
+* **docs:** use environment variables for authentication in code snippets ([01f64ef](https://github.com/maisaai/python-sdk/commit/01f64ef00b017e4f9da7e863b9beb64acc90eb3d))
+* fix typos ([#79](https://github.com/maisaai/python-sdk/issues/79)) ([2b40b83](https://github.com/maisaai/python-sdk/commit/2b40b832f20097203b6fcb3cbb78a73ed0f34640))
+* format all `api.md` files ([75ee9b2](https://github.com/maisaai/python-sdk/commit/75ee9b26e5b8437cc020e245237215ed42367ce1))
+* **internal/tests:** avoid race condition with implicit client cleanup ([d913cd6](https://github.com/maisaai/python-sdk/commit/d913cd6fd031903cd15098974af2b16c86bd52ac))
+* **internal:** add `--fix` argument to lint script ([e44e409](https://github.com/maisaai/python-sdk/commit/e44e4092f427678acfb3b2d7a417a23124942a85))
+* **internal:** add missing files argument to base client ([934bd9d](https://github.com/maisaai/python-sdk/commit/934bd9d81930acd5fc3a65ad9508c94c6e12f868))
+* **internal:** add request options to SSE classes ([aa545fe](https://github.com/maisaai/python-sdk/commit/aa545fe0459755e62127496a35f5d139fda198aa))
+* **internal:** add Sequence related utils ([f0d2871](https://github.com/maisaai/python-sdk/commit/f0d287110342ec657ec4023387f51d203b2d5a48))
+* **internal:** add support for TypeAliasType ([#37](https://github.com/maisaai/python-sdk/issues/37)) ([c4f353f](https://github.com/maisaai/python-sdk/commit/c4f353f07fdb0a7f1a8b737f85973509c6aa6bb7))
+* **internal:** avoid errors for isinstance checks on proxies ([a7b6a05](https://github.com/maisaai/python-sdk/commit/a7b6a0505996024a903de3d310e16233ba99ddff))
+* **internal:** avoid pytest-asyncio deprecation warning ([#55](https://github.com/maisaai/python-sdk/issues/55)) ([bc8b43e](https://github.com/maisaai/python-sdk/commit/bc8b43ef36216fbfac1526075402c1cd20d4af57))
+* **internal:** base client updates ([e30b668](https://github.com/maisaai/python-sdk/commit/e30b668cf0787e16dd26a0a77e13e04cc0a09200))
+* **internal:** bummp ruff dependency ([#59](https://github.com/maisaai/python-sdk/issues/59)) ([7882f2c](https://github.com/maisaai/python-sdk/commit/7882f2c5a49d968bafb60729ac31c831e2b8161f))
+* **internal:** bump dependencies ([9e165df](https://github.com/maisaai/python-sdk/commit/9e165dff04e1ecfb06f3413ce67ca5f748be831c))
+* **internal:** bump httpx dependency ([#47](https://github.com/maisaai/python-sdk/issues/47)) ([fb2bc94](https://github.com/maisaai/python-sdk/commit/fb2bc9496c5b7c66454d1b2e77c8bf8665a28ab5))
+* **internal:** bump pinned h11 dep ([57ff829](https://github.com/maisaai/python-sdk/commit/57ff8290fbf16fb995158780c5a01f9d30b27ce5))
+* **internal:** bump pydantic dependency ([#34](https://github.com/maisaai/python-sdk/issues/34)) ([1b764fd](https://github.com/maisaai/python-sdk/commit/1b764fdebbf53d067b1c208e9c9c124cab1ab76d))
+* **internal:** bump pyright ([#32](https://github.com/maisaai/python-sdk/issues/32)) ([0ecc7ec](https://github.com/maisaai/python-sdk/commit/0ecc7ec95c31105611aa6abd11f230654e961009))
+* **internal:** bump pyright ([#36](https://github.com/maisaai/python-sdk/issues/36)) ([a12e8fb](https://github.com/maisaai/python-sdk/commit/a12e8fbc0c7aa7e8e40280305ef08b382e091d34))
+* **internal:** bump pyright version ([14e4c20](https://github.com/maisaai/python-sdk/commit/14e4c2012cfca8f2b159917d3e63a8c79b850341))
+* **internal:** bump rye to 0.44.0 ([#75](https://github.com/maisaai/python-sdk/issues/75)) ([0d2de68](https://github.com/maisaai/python-sdk/commit/0d2de6828ac638e20428cd71a03dba1aff27167a))
+* **internal:** change ci workflow machines ([8615982](https://github.com/maisaai/python-sdk/commit/8615982869fb3c28b01783d53e90fa3d174359cb))
+* **internal:** change default timeout to an int ([#58](https://github.com/maisaai/python-sdk/issues/58)) ([9b8fbb7](https://github.com/maisaai/python-sdk/commit/9b8fbb799000c30e15cdb7777e71aecf1584826c))
+* **internal:** codegen related update ([c0a96e0](https://github.com/maisaai/python-sdk/commit/c0a96e09be09ba4aa690bec9de3359538b972a72))
+* **internal:** codegen related update ([cd89671](https://github.com/maisaai/python-sdk/commit/cd89671fe53d2e31916a53f5bbd3c7bc9c7ab56d))
+* **internal:** codegen related update ([d0979d5](https://github.com/maisaai/python-sdk/commit/d0979d5cfa0a424672e519264469b35be2bcdd0e))
+* **internal:** codegen related update ([b86bfb7](https://github.com/maisaai/python-sdk/commit/b86bfb75296288127820f5f46c98a9525ba0183c))
+* **internal:** codegen related update ([07cb56f](https://github.com/maisaai/python-sdk/commit/07cb56f3ec286adc1744b5080056c1156a8f850d))
+* **internal:** codegen related update ([2bdbbe4](https://github.com/maisaai/python-sdk/commit/2bdbbe484071b54ac7ce2bf15404aa1e33c4e766))
+* **internal:** codegen related update ([#38](https://github.com/maisaai/python-sdk/issues/38)) ([07f9a2a](https://github.com/maisaai/python-sdk/commit/07f9a2a232504e9a4c5165e0fa8a1fc0a9cda823))
+* **internal:** codegen related update ([#39](https://github.com/maisaai/python-sdk/issues/39)) ([0526011](https://github.com/maisaai/python-sdk/commit/05260113c3701215702d9b1024ccd5c0598afcad))
+* **internal:** codegen related update ([#40](https://github.com/maisaai/python-sdk/issues/40)) ([6010846](https://github.com/maisaai/python-sdk/commit/601084637e5d988387413e25c0412f3b4de82c90))
+* **internal:** codegen related update ([#41](https://github.com/maisaai/python-sdk/issues/41)) ([017e4c3](https://github.com/maisaai/python-sdk/commit/017e4c3a0a59b4e50beef1b6486642f9e9ccd7cb))
+* **internal:** codegen related update ([#42](https://github.com/maisaai/python-sdk/issues/42)) ([738fb58](https://github.com/maisaai/python-sdk/commit/738fb58f48f2a7cdbdc1c5a04b8ac2e50eb4c3ab))
+* **internal:** codegen related update ([#45](https://github.com/maisaai/python-sdk/issues/45)) ([a28dbb9](https://github.com/maisaai/python-sdk/commit/a28dbb952c8668b1fbd895979705646789522712))
+* **internal:** codegen related update ([#50](https://github.com/maisaai/python-sdk/issues/50)) ([0f877e7](https://github.com/maisaai/python-sdk/commit/0f877e7b5d82c07647afaddb9f236181e286ad07))
+* **internal:** codegen related update ([#52](https://github.com/maisaai/python-sdk/issues/52)) ([e097f1c](https://github.com/maisaai/python-sdk/commit/e097f1c81b5504db6d7761a845542a5dd300221f))
+* **internal:** codegen related update ([#65](https://github.com/maisaai/python-sdk/issues/65)) ([851aa1d](https://github.com/maisaai/python-sdk/commit/851aa1d4e01286a35eb3b64e6d299dda49ccad28))
+* **internal:** codegen related update ([#74](https://github.com/maisaai/python-sdk/issues/74)) ([7f7fc88](https://github.com/maisaai/python-sdk/commit/7f7fc88a76874624cfc14408fb3080b0b2798ba8))
+* **internal:** detect missing future annotations with ruff ([4dcaf54](https://github.com/maisaai/python-sdk/commit/4dcaf540ad3b1d2bce32b24885b77daaf67a0ee2))
+* **internal:** exclude mypy from running on tests ([#30](https://github.com/maisaai/python-sdk/issues/30)) ([3bcf1c9](https://github.com/maisaai/python-sdk/commit/3bcf1c92cd0d4614f3e433a875ff5c57deef884a))
+* **internal:** expand CI branch coverage ([b849844](https://github.com/maisaai/python-sdk/commit/b849844d128bb9cc3f04d16f126c075018013b92))
+* **internal:** fix compat model_dump method when warnings are passed ([#27](https://github.com/maisaai/python-sdk/issues/27)) ([d354f4a](https://github.com/maisaai/python-sdk/commit/d354f4a0dea8c17c8cb9d3651f726f92f8a7b81d))
+* **internal:** fix devcontainers setup ([#67](https://github.com/maisaai/python-sdk/issues/67)) ([9925791](https://github.com/maisaai/python-sdk/commit/992579197032920caa208eb6930507235d2b08a7))
+* **internal:** fix lint error on Python 3.14 ([6d06b98](https://github.com/maisaai/python-sdk/commit/6d06b9846990a180cc8dcd7c4bbeae2433d93da9))
+* **internal:** fix list file params ([dcc896c](https://github.com/maisaai/python-sdk/commit/dcc896c62ce95f2a76b4f153c2c86045f31d1192))
+* **internal:** fix ruff target version ([e16c3d8](https://github.com/maisaai/python-sdk/commit/e16c3d81dd8d05db7e17bbd0f2318da894caf835))
+* **internal:** fix some typos ([#44](https://github.com/maisaai/python-sdk/issues/44)) ([01228e6](https://github.com/maisaai/python-sdk/commit/01228e6aade70fd952ee192abdb2f90a4381fc92))
+* **internal:** fix type traversing dictionary params ([#61](https://github.com/maisaai/python-sdk/issues/61)) ([d69c8ce](https://github.com/maisaai/python-sdk/commit/d69c8cecc1eb95971ad11814b5c39506b5a35ae3))
+* **internal:** grammar fix (it's -> its) ([359a8c2](https://github.com/maisaai/python-sdk/commit/359a8c23e2cbde6f5f2a255a09125f2043ba278c))
+* **internal:** import reformatting ([69505f7](https://github.com/maisaai/python-sdk/commit/69505f7ff92e385feba9854eae4e597ac748c7c8))
+* **internal:** make `test_proxy_environment_variables` more resilient ([035d6a0](https://github.com/maisaai/python-sdk/commit/035d6a071f6f44e7d94233660d0370bc6e1491bf))
+* **internal:** make `test_proxy_environment_variables` more resilient to env ([3f72803](https://github.com/maisaai/python-sdk/commit/3f72803465edcab2e8a602deeb46b7a797a0a0cc))
+* **internal:** minor formatting changes ([8933135](https://github.com/maisaai/python-sdk/commit/8933135761ff9085dcc53ba504c0c7469328769f))
+* **internal:** minor formatting changes ([#57](https://github.com/maisaai/python-sdk/issues/57)) ([218176b](https://github.com/maisaai/python-sdk/commit/218176bbb0aad125049959bf249298d9275276ef))
+* **internal:** minor style changes ([#56](https://github.com/maisaai/python-sdk/issues/56)) ([0614b4a](https://github.com/maisaai/python-sdk/commit/0614b4a6d98d5f36c2abf99692cc249eec7b1462))
+* **internal:** minor type handling changes ([#62](https://github.com/maisaai/python-sdk/issues/62)) ([3cde3a5](https://github.com/maisaai/python-sdk/commit/3cde3a52a5cc06e686bf44bebce5642d5ebbd6f0))
+* **internal:** more robust bootstrap script ([9cd364b](https://github.com/maisaai/python-sdk/commit/9cd364b6ff6fab42f437930408238c1fe1707bd7))
+* **internal:** properly set __pydantic_private__ ([#68](https://github.com/maisaai/python-sdk/issues/68)) ([7af75eb](https://github.com/maisaai/python-sdk/commit/7af75eb3b3ca2b06521473713f0fb3d1a05a6517))
+* **internal:** reduce CI branch coverage ([971a4ef](https://github.com/maisaai/python-sdk/commit/971a4ef91cb47972bbc2fbcbea369c84bb4890d8))
+* **internal:** refactor retries to not use recursion ([d8eb684](https://github.com/maisaai/python-sdk/commit/d8eb684bc1f626f87bc0228ef09af0d4c71c2769))
+* **internal:** reformat pyproject.toml ([dcc2d46](https://github.com/maisaai/python-sdk/commit/dcc2d4664581a32d2c53c3860f6de76b6e8ffb6f))
+* **internal:** remove extra empty newlines ([#73](https://github.com/maisaai/python-sdk/issues/73)) ([9e95737](https://github.com/maisaai/python-sdk/commit/9e957376224f86d8a863fe83077513faa4d94239))
+* **internal:** remove trailing character ([#80](https://github.com/maisaai/python-sdk/issues/80)) ([e336af2](https://github.com/maisaai/python-sdk/commit/e336af250e5b570100da9573ee341bb81859c850))
+* **internal:** remove unused http client options forwarding ([#71](https://github.com/maisaai/python-sdk/issues/71)) ([855d582](https://github.com/maisaai/python-sdk/commit/855d58284c791307976763f104eb8ecf8bb94fb3))
+* **internal:** slight transform perf improvement ([#81](https://github.com/maisaai/python-sdk/issues/81)) ([725bac2](https://github.com/maisaai/python-sdk/commit/725bac24208c0107455b11db84a860039905ae3e))
+* **internal:** tweak CI branches ([e1096d9](https://github.com/maisaai/python-sdk/commit/e1096d9e4bb5266c09324492f0beb08c01ea1cbd))
+* **internal:** update `actions/checkout` version ([e5f90c9](https://github.com/maisaai/python-sdk/commit/e5f90c964b7060e6a063937a4c1ae48b61721243))
+* **internal:** update client tests ([#63](https://github.com/maisaai/python-sdk/issues/63)) ([4596d45](https://github.com/maisaai/python-sdk/commit/4596d45e982f7d3fc3de025a1ba5574c590d875a))
+* **internal:** update comment in script ([8735ebc](https://github.com/maisaai/python-sdk/commit/8735ebcba8abd47d3bf01f45908015a737e8ee6b))
+* **internal:** update conftest.py ([0b96f69](https://github.com/maisaai/python-sdk/commit/0b96f69329ed5c975ef8013acac2e699949f2c2c))
+* **internal:** update gitignore ([f6bfaaa](https://github.com/maisaai/python-sdk/commit/f6bfaaa900a2d42d0f5354f95cb11f93bf4b1f26))
+* **internal:** update models test ([2625b52](https://github.com/maisaai/python-sdk/commit/2625b528dd6c331336ff2edb6e37a2eb55c16d34))
+* **internal:** update pydantic dependency ([1d81895](https://github.com/maisaai/python-sdk/commit/1d8189553d8b258cf613335bab65e2bf26bad23f))
+* **internal:** update pyright exclude list ([d5f6145](https://github.com/maisaai/python-sdk/commit/d5f61458a707cd450d1433febf3a9550dc103643))
+* **internal:** update pyright settings ([86bac8f](https://github.com/maisaai/python-sdk/commit/86bac8f7145e4097a4ca5295db678d37dc99dddc))
+* make the `Omit` type public ([#33](https://github.com/maisaai/python-sdk/issues/33)) ([72de306](https://github.com/maisaai/python-sdk/commit/72de3060418356f0b460f9b138a158e56eceb2a9))
+* **package:** drop Python 3.8 support ([a0fdbfd](https://github.com/maisaai/python-sdk/commit/a0fdbfdf095c289a505e0a9f0f5005556b9dea6f))
+* **package:** mark python 3.13 as supported ([70d59fd](https://github.com/maisaai/python-sdk/commit/70d59fd2d01ab63d402305473eacd669c37b3406))
+* **project:** add settings file for vscode ([f9ac05a](https://github.com/maisaai/python-sdk/commit/f9ac05aaaa7a6f0e50249200aab9a017332d2b57))
+* **readme:** fix version rendering on pypi ([e05102a](https://github.com/maisaai/python-sdk/commit/e05102abfe4c503445afeecc74efa09e58cef23c))
+* **readme:** update badges ([c47640b](https://github.com/maisaai/python-sdk/commit/c47640be2fdb0f0d1c6b33eaf1eb11a771f892b7))
+* rebuild project due to codegen change ([#22](https://github.com/maisaai/python-sdk/issues/22)) ([29c8c9e](https://github.com/maisaai/python-sdk/commit/29c8c9e95ae569de0a785722d0773198b96912bc))
+* rebuild project due to codegen change ([#23](https://github.com/maisaai/python-sdk/issues/23)) ([7c494c5](https://github.com/maisaai/python-sdk/commit/7c494c538e949dbccf6bcfa09a05c0119af5c4e8))
+* rebuild project due to codegen change ([#24](https://github.com/maisaai/python-sdk/issues/24)) ([552ed44](https://github.com/maisaai/python-sdk/commit/552ed440c47d95c939942439344e3da2907b272e))
+* rebuild project due to codegen change ([#25](https://github.com/maisaai/python-sdk/issues/25)) ([4c06923](https://github.com/maisaai/python-sdk/commit/4c06923e146784772fc5198485e0b46c336beb25))
+* rebuild project due to codegen change ([#26](https://github.com/maisaai/python-sdk/issues/26)) ([2414a9e](https://github.com/maisaai/python-sdk/commit/2414a9e7d43f960247dc09534b429c7525838854))
+* rebuild project due to oas spec rename ([#20](https://github.com/maisaai/python-sdk/issues/20)) ([9d1408b](https://github.com/maisaai/python-sdk/commit/9d1408bab9d6a93e0058ebe9a62b4f84d036f133))
+* remove now unused `cached-property` dep ([#29](https://github.com/maisaai/python-sdk/issues/29)) ([5c8985c](https://github.com/maisaai/python-sdk/commit/5c8985c2e5fa6621d4801f4c3ccf44df58af0369))
+* rename some identifiers ([451bed3](https://github.com/maisaai/python-sdk/commit/451bed3131cbfed8c3ce56ce8ca040c05b5b0e1e))
+* slight wording improvement in README ([#83](https://github.com/maisaai/python-sdk/issues/83)) ([b01b22e](https://github.com/maisaai/python-sdk/commit/b01b22eac5d5ea0177624858b062b9f30f75c3ed))
+* speedup initial import ([454784d](https://github.com/maisaai/python-sdk/commit/454784da0fadabddc20c97d189bdf9e0a7d411fb))
+* **test:** do not count install time for mock server timeout ([316c1ee](https://github.com/maisaai/python-sdk/commit/316c1eeb729b228b4f32dbc92846cfcc6e02194f))
+* **tests:** add tests for httpx client instantiation & proxies ([911a987](https://github.com/maisaai/python-sdk/commit/911a987972c2a0d25113272994702f0c8eb07abb))
+* **tests:** bump steady to v0.19.4 ([0d00146](https://github.com/maisaai/python-sdk/commit/0d00146527d8479b8de6935ce309bdad7127439e))
+* **tests:** bump steady to v0.19.5 ([9c4b717](https://github.com/maisaai/python-sdk/commit/9c4b717e3413b402df4804e038d305597f0d717d))
+* **tests:** bump steady to v0.19.6 ([c8b2598](https://github.com/maisaai/python-sdk/commit/c8b2598b31b1e4afe9fbb7c77269e8148b5805ce))
+* **tests:** bump steady to v0.19.7 ([8add188](https://github.com/maisaai/python-sdk/commit/8add188e472fa959b15a17b2d5fa29e43645c1f2))
+* **tests:** bump steady to v0.20.1 ([a192e9f](https://github.com/maisaai/python-sdk/commit/a192e9f545c0f1ab18423679c15f8dc5af994807))
+* **tests:** bump steady to v0.20.2 ([36436db](https://github.com/maisaai/python-sdk/commit/36436dbb74c63d0ed3778470a6621472d5dbdaac))
+* **tests:** bump steady to v0.22.1 ([50901a8](https://github.com/maisaai/python-sdk/commit/50901a8ac0737687c112895ef10218d6f5d5ee81))
+* **tests:** improve enum examples ([#82](https://github.com/maisaai/python-sdk/issues/82)) ([992264c](https://github.com/maisaai/python-sdk/commit/992264cfce392fa1504cc65871652aa0452fec82))
+* **tests:** run tests in parallel ([672fffa](https://github.com/maisaai/python-sdk/commit/672fffa3fe4800289419bf14d4532b3a4cdf07fc))
+* **tests:** simplify `get_platform` test ([d6ec18d](https://github.com/maisaai/python-sdk/commit/d6ec18d85859eb1069e30e57450e55e0ecfd89df))
+* **tests:** skip some failing tests on the latest python versions ([4b51622](https://github.com/maisaai/python-sdk/commit/4b5162257aea9f1875b7a01213446cb56eeb17ca))
+* **types:** change optional parameter type from NotGiven to Omit ([6abb395](https://github.com/maisaai/python-sdk/commit/6abb39527de16f19c4187c1ea6dceda1c891ec13))
+* update @stainless-api/prism-cli to v5.15.0 ([0fd434f](https://github.com/maisaai/python-sdk/commit/0fd434f70bd220e5413429c31e532d50b5de9578))
+* update github action ([37deecf](https://github.com/maisaai/python-sdk/commit/37deecf9e54363de62ea3a55c2b2313a6241c6b8))
+* update lockfile ([5e779fc](https://github.com/maisaai/python-sdk/commit/5e779fcdf6fdfbde16fd3d5a8b0beee30e893532))
+* update mock server docs ([ea4ade6](https://github.com/maisaai/python-sdk/commit/ea4ade65d8e5934cc210b42bfd8a0c9a80ac79f6))
+* update placeholder string ([c38bd27](https://github.com/maisaai/python-sdk/commit/c38bd27f7f3d9a5a0cca82ec82d6e762697f86db))
+
+
+### Documentation
+
+* add info log level to readme ([#28](https://github.com/maisaai/python-sdk/issues/28)) ([ac21690](https://github.com/maisaai/python-sdk/commit/ac2169019646303ec6c9eff1da3e07c2b71a6115))
+* **client:** fix httpx.Timeout documentation reference ([52ab338](https://github.com/maisaai/python-sdk/commit/52ab33896a8fd2166bf0510766038bd69a67b2a0))
+* fix typos ([#49](https://github.com/maisaai/python-sdk/issues/49)) ([baf1fbc](https://github.com/maisaai/python-sdk/commit/baf1fbc795d885fd3871cf3a65da1950b53e7391))
+* **raw responses:** fix duplicate `the` ([#53](https://github.com/maisaai/python-sdk/issues/53)) ([ff88f4f](https://github.com/maisaai/python-sdk/commit/ff88f4fb37f793982750daffd89742f06edd9d0d))
+* **readme:** example snippet for client context manager ([#43](https://github.com/maisaai/python-sdk/issues/43)) ([03b1d41](https://github.com/maisaai/python-sdk/commit/03b1d41f8bf0eb3b7e4374ed66655dc393530303))
+* **readme:** fix http client proxies example ([#35](https://github.com/maisaai/python-sdk/issues/35)) ([90fa18a](https://github.com/maisaai/python-sdk/commit/90fa18a81017f3ee361bcdb1a1f5bc1164f20052))
+* update examples ([819e55d](https://github.com/maisaai/python-sdk/commit/819e55d3f636d92d288b5b309a08f7272089ee24))
+* update URLs from stainlessapi.com to stainless.com ([#69](https://github.com/maisaai/python-sdk/issues/69)) ([dac10cc](https://github.com/maisaai/python-sdk/commit/dac10cc19c2b7a0d12e2261e55019cb2eafef2f3))
+
+
+### Refactors
+
+* **tests:** switch from prism to steady ([5a58be5](https://github.com/maisaai/python-sdk/commit/5a58be52061d49b7e66a03472fac1e7c0dff5692))
+
## 0.1.0-alpha.4 (2024-03-05)
Full Changelog: [v0.1.0-alpha.3...v0.1.0-alpha.4](https://github.com/maisaai/python-sdk/compare/v0.1.0-alpha.3...v0.1.0-alpha.4)
diff --git a/pyproject.toml b/pyproject.toml
index 24bf8b4a..5d06e50a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "maisa"
-version = "0.1.0-alpha.4"
+version = "0.1.0-alpha.5"
description = "The official Python library for the Maisa API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/maisa/_version.py b/src/maisa/_version.py
index 9fc7974d..aef381b4 100644
--- a/src/maisa/_version.py
+++ b/src/maisa/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "maisa"
-__version__ = "0.1.0-alpha.4" # x-release-please-version
+__version__ = "0.1.0-alpha.5" # x-release-please-version