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 @@ [![PyPI version](https://img.shields.io/pypi/v/maisa.svg)](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 -[![PyPI version](https://img.shields.io/pypi/v/maisa.svg)](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/) + +[![PyPI version](https://img.shields.io/pypi/v/maisa.svg?label=pypi%20(stable))](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 @@ [![PyPI version](https://img.shields.io/pypi/v/maisa.svg?label=pypi%20(stable))](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