diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 025d0a3..5743b12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,12 +7,17 @@ on: - 'integrated/**' - 'stl-preview-head/**' - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' jobs: lint: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/zeroentropy-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 @@ -30,10 +35,10 @@ jobs: - name: Run lints run: ./scripts/lint - upload: - if: github.repository == 'stainless-sdks/zeroentropy-python' + build: + if: github.repository == 'stainless-sdks/zeroentropy-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 @@ -41,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 @@ -58,6 +77,7 @@ jobs: timeout-minutes: 10 name: test runs-on: ${{ github.repository == 'stainless-sdks/zeroentropy-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e8285b7..4f9005e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.5" + ".": "0.1.0-alpha.6" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 5e6082f..569eead 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zeroentropy%2Fzeroentropy-f06c49dfd4b38a4f9d5bcad56348156bbf641aa8b7968acfbf655ad6ceff2126.yml -openapi_spec_hash: cac52dd65fbcb65ffa7a183e764b7f06 -config_hash: beba80a17ba64c5439712e85129ab5ad +configured_endpoints: 14 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zeroentropy%2Fzeroentropy-bd2f55f423e09b74f83cbad6034fb76f7052363308d02533a908b49543cff459.yml +openapi_spec_hash: 6d7566ebda7fecac4069744949d547e0 +config_hash: e07cdee04c971e1db74e91a5a4cd981c diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a4c83f..c57f354 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## 0.1.0-alpha.6 (2025-07-08) + +Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/zeroentropy-ai/zeroentropy-python/compare/v0.1.0-alpha.5...v0.1.0-alpha.6) + +### Features + +* **api:** manual updates ([b6616f5](https://github.com/zeroentropy-ai/zeroentropy-python/commit/b6616f523b9dd5a4a3f9142bf2a561b560767492)) +* **api:** manual updates ([8752560](https://github.com/zeroentropy-ai/zeroentropy-python/commit/875256098555c768cd90d459df84e7cdcb4244ce)) +* **api:** manual updates ([11c83ad](https://github.com/zeroentropy-ai/zeroentropy-python/commit/11c83ad2bc4942f703bd255301816eb3a99682b7)) +* **api:** manual updates ([801513a](https://github.com/zeroentropy-ai/zeroentropy-python/commit/801513aefc80ac3345bb8d9995d558e576d3c58e)) +* **client:** add support for aiohttp ([3aa7ee2](https://github.com/zeroentropy-ai/zeroentropy-python/commit/3aa7ee28445efc3285b1ce79d6b7959d18bf7425)) + + +### Bug Fixes + +* **ci:** correct conditional ([6ab3ba2](https://github.com/zeroentropy-ai/zeroentropy-python/commit/6ab3ba21090ea2c54dfd7b9aea8c1d86963cd046)) +* **ci:** release-doctor — report correct token name ([c9a55cb](https://github.com/zeroentropy-ai/zeroentropy-python/commit/c9a55cb8318c27c3acd8a84395cd295e348a05f9)) +* **client:** correctly parse binary response | stream ([f7b7ef9](https://github.com/zeroentropy-ai/zeroentropy-python/commit/f7b7ef9f45ff779734d1b6ee48349943bf14fce2)) +* **tests:** fix: tests which call HTTP endpoints directly with the example parameters ([027e535](https://github.com/zeroentropy-ai/zeroentropy-python/commit/027e535f70651ff5f6b0df8eb3a9786e31241896)) + + +### Chores + +* **ci:** change upload type ([cb76da7](https://github.com/zeroentropy-ai/zeroentropy-python/commit/cb76da7ab4ab322268bd7663f5371893ff851a05)) +* **ci:** enable for pull requests ([fb3b81d](https://github.com/zeroentropy-ai/zeroentropy-python/commit/fb3b81d7b36b9242b56ed8e5e928272857a82aac)) +* **ci:** only run for pushes and fork pull requests ([ff5c91d](https://github.com/zeroentropy-ai/zeroentropy-python/commit/ff5c91d9ebca88bad06e0b9b1c62cd013dcf1b9b)) +* **internal:** update conftest.py ([0ddbc5a](https://github.com/zeroentropy-ai/zeroentropy-python/commit/0ddbc5ac04d55067056a50b6ad38b9afc9be15c1)) +* **readme:** update badges ([e790eb4](https://github.com/zeroentropy-ai/zeroentropy-python/commit/e790eb48bfd89d6b384cd9382480574a83cee175)) +* **tests:** add tests for httpx client instantiation & proxies ([64a0a4c](https://github.com/zeroentropy-ai/zeroentropy-python/commit/64a0a4cb1b1c4f71b37174e748e91e0df7bcc2cc)) +* **tests:** run tests in parallel ([0261b3c](https://github.com/zeroentropy-ai/zeroentropy-python/commit/0261b3c5bb54b02fe84b42c2d0e41a5e43519adc)) +* **tests:** skip some failing tests on the latest python versions ([2cbe097](https://github.com/zeroentropy-ai/zeroentropy-python/commit/2cbe097a9ac15c6f87e7137788a44058bd966524)) + + +### Documentation + +* **client:** fix httpx.Timeout documentation reference ([3cd4296](https://github.com/zeroentropy-ai/zeroentropy-python/commit/3cd4296f8fe24f373dba2100dc1bc389f8e3b496)) + ## 0.1.0-alpha.5 (2025-06-04) Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/zeroentropy-ai/zeroentropy-python/compare/v0.1.0-alpha.4...v0.1.0-alpha.5) diff --git a/README.md b/README.md index a0c514e..239e0f2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ZeroEntropy Python SDK -[![PyPI version](https://img.shields.io/pypi/v/zeroentropy.svg)](https://pypi.org/project/zeroentropy/) +[![PyPI version]()](https://pypi.org/project/zeroentropy/) The ZeroEntropy Python SDK provides convenient access to the [ZeroEntropy REST API](https://docs.zeroentropy.dev/api-reference/) from any Python 3.8+ application. @@ -79,6 +79,45 @@ 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 zeroentropy[aiohttp] +``` + +Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: + +```python +import os +import asyncio +from zeroentropy import DefaultAioHttpClient +from zeroentropy import AsyncZeroEntropy + + +async def main() -> None: + async with AsyncZeroEntropy( + api_key=os.environ.get("ZEROENTROPY_API_KEY"), # This is the default and can be omitted + http_client=DefaultAioHttpClient(), + ) as client: + response = await client.documents.add( + collection_name="example_collection", + content={ + "type": "text", + "text": "Example Content", + }, + path="my_document.txt", + ) + print(response.message) + + +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: @@ -224,7 +263,7 @@ client.with_options(max_retries=5).status.get_status() ### 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 zeroentropy import ZeroEntropy diff --git a/api.md b/api.md index 8c93fa7..cc69a2d 100644 --- a/api.md +++ b/api.md @@ -1,15 +1,3 @@ -# Admin - -Types: - -```python -from zeroentropy.types import AdminCreateOrganizationResponse -``` - -Methods: - -- client.admin.create_organization(\*\*params) -> AdminCreateOrganizationResponse - # Status Types: @@ -82,14 +70,14 @@ Methods: - client.queries.top_pages(\*\*params) -> QueryTopPagesResponse - client.queries.top_snippets(\*\*params) -> QueryTopSnippetsResponse -# Parsers +# Models Types: ```python -from zeroentropy.types import ParserParseDocumentResponse +from zeroentropy.types import ModelRerankResponse ``` Methods: -- client.parsers.parse_document(\*\*params) -> ParserParseDocumentResponse +- client.models.rerank(\*\*params) -> ModelRerankResponse diff --git a/bin/check-release-environment b/bin/check-release-environment index f9aa191..b845b0f 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -3,7 +3,7 @@ errors=() if [ -z "${PYPI_TOKEN}" ]; then - errors+=("The ZEROENTROPY_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[@]} diff --git a/pyproject.toml b/pyproject.toml index 3dc6cd6..b989bd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zeroentropy" -version = "0.1.0-alpha.5" +version = "0.1.0-alpha.6" description = "The official Python library for the ZeroEntropy API" dynamic = ["readme"] license = "Apache-2.0" @@ -37,6 +37,8 @@ classifiers = [ Homepage = "https://github.com/zeroentropy-ai/zeroentropy-python" Repository = "https://github.com/zeroentropy-ai/zeroentropy-python" +[project.optional-dependencies] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"] [tool.rye] managed = true @@ -54,6 +56,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 +128,7 @@ replacement = '[\1](https://github.com/zeroentropy-ai/zeroentropy-python/tree/ma [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 9dd3c5a..c1bb9eb 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 zeroentropy +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 zeroentropy 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 @@ -30,18 +41,27 @@ 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 +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 respx # via zeroentropy +httpx-aiohttp==0.1.8 + # via zeroentropy idna==3.4 # via anyio # via httpx + # via yarl importlib-metadata==7.0.0 iniconfig==2.0.0 # via pytest @@ -49,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 @@ -63,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 zeroentropy pydantic-core==2.27.1 @@ -72,7 +98,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 @@ -93,6 +121,7 @@ tomli==2.0.2 # via pytest typing-extensions==4.12.2 # via anyio + # via multidict # via mypy # via pydantic # via pydantic-core @@ -100,5 +129,7 @@ typing-extensions==4.12.2 # via zeroentropy 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 2f5d5bf..9a36b80 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 zeroentropy +aiosignal==1.3.2 + # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 # via httpx # via zeroentropy +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 zeroentropy 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 zeroentropy +httpx-aiohttp==0.1.8 # via zeroentropy 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 zeroentropy pydantic-core==2.27.1 @@ -40,6 +64,9 @@ sniffio==1.3.0 # via zeroentropy typing-extensions==4.12.2 # via anyio + # via multidict # via pydantic # via pydantic-core # via zeroentropy +yarl==1.20.0 + # via aiohttp diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index 7a82b4d..5b1a27c 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/zeroentropy-python/$SHA'\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/zeroentropy-python/$SHA/$FILENAME'\033[0m" else echo -e "\033[31mFailed to upload artifact.\033[0m" exit 1 diff --git a/src/zeroentropy/__init__.py b/src/zeroentropy/__init__.py index 06ec13f..196eb66 100644 --- a/src/zeroentropy/__init__.py +++ b/src/zeroentropy/__init__.py @@ -36,7 +36,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__ = [ @@ -78,6 +78,7 @@ "DEFAULT_CONNECTION_LIMITS", "DefaultHttpxClient", "DefaultAsyncHttpxClient", + "DefaultAioHttpClient", ] if not _t.TYPE_CHECKING: diff --git a/src/zeroentropy/_base_client.py b/src/zeroentropy/_base_client.py index 4788795..42f3bdd 100644 --- a/src/zeroentropy/_base_client.py +++ b/src/zeroentropy/_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}") @@ -1282,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 @@ -1290,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): @@ -1574,7 +1603,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}") diff --git a/src/zeroentropy/_client.py b/src/zeroentropy/_client.py index a53554b..abf8acc 100644 --- a/src/zeroentropy/_client.py +++ b/src/zeroentropy/_client.py @@ -21,7 +21,7 @@ ) from ._utils import is_given, get_async_library from ._version import __version__ -from .resources import admin, status, parsers, queries, documents, collections +from .resources import models, status, queries, documents, collections from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, ZeroEntropyError from ._base_client import ( @@ -43,12 +43,11 @@ class ZeroEntropy(SyncAPIClient): - admin: admin.AdminResource status: status.StatusResource collections: collections.CollectionsResource documents: documents.DocumentsResource queries: queries.QueriesResource - parsers: parsers.ParsersResource + models: models.ModelsResource with_raw_response: ZeroEntropyWithRawResponse with_streaming_response: ZeroEntropyWithStreamedResponse @@ -106,12 +105,11 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.admin = admin.AdminResource(self) self.status = status.StatusResource(self) self.collections = collections.CollectionsResource(self) self.documents = documents.DocumentsResource(self) self.queries = queries.QueriesResource(self) - self.parsers = parsers.ParsersResource(self) + self.models = models.ModelsResource(self) self.with_raw_response = ZeroEntropyWithRawResponse(self) self.with_streaming_response = ZeroEntropyWithStreamedResponse(self) @@ -221,12 +219,11 @@ def _make_status_error( class AsyncZeroEntropy(AsyncAPIClient): - admin: admin.AsyncAdminResource status: status.AsyncStatusResource collections: collections.AsyncCollectionsResource documents: documents.AsyncDocumentsResource queries: queries.AsyncQueriesResource - parsers: parsers.AsyncParsersResource + models: models.AsyncModelsResource with_raw_response: AsyncZeroEntropyWithRawResponse with_streaming_response: AsyncZeroEntropyWithStreamedResponse @@ -284,12 +281,11 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.admin = admin.AsyncAdminResource(self) self.status = status.AsyncStatusResource(self) self.collections = collections.AsyncCollectionsResource(self) self.documents = documents.AsyncDocumentsResource(self) self.queries = queries.AsyncQueriesResource(self) - self.parsers = parsers.AsyncParsersResource(self) + self.models = models.AsyncModelsResource(self) self.with_raw_response = AsyncZeroEntropyWithRawResponse(self) self.with_streaming_response = AsyncZeroEntropyWithStreamedResponse(self) @@ -400,42 +396,38 @@ def _make_status_error( class ZeroEntropyWithRawResponse: def __init__(self, client: ZeroEntropy) -> None: - self.admin = admin.AdminResourceWithRawResponse(client.admin) self.status = status.StatusResourceWithRawResponse(client.status) self.collections = collections.CollectionsResourceWithRawResponse(client.collections) self.documents = documents.DocumentsResourceWithRawResponse(client.documents) self.queries = queries.QueriesResourceWithRawResponse(client.queries) - self.parsers = parsers.ParsersResourceWithRawResponse(client.parsers) + self.models = models.ModelsResourceWithRawResponse(client.models) class AsyncZeroEntropyWithRawResponse: def __init__(self, client: AsyncZeroEntropy) -> None: - self.admin = admin.AsyncAdminResourceWithRawResponse(client.admin) self.status = status.AsyncStatusResourceWithRawResponse(client.status) self.collections = collections.AsyncCollectionsResourceWithRawResponse(client.collections) self.documents = documents.AsyncDocumentsResourceWithRawResponse(client.documents) self.queries = queries.AsyncQueriesResourceWithRawResponse(client.queries) - self.parsers = parsers.AsyncParsersResourceWithRawResponse(client.parsers) + self.models = models.AsyncModelsResourceWithRawResponse(client.models) class ZeroEntropyWithStreamedResponse: def __init__(self, client: ZeroEntropy) -> None: - self.admin = admin.AdminResourceWithStreamingResponse(client.admin) self.status = status.StatusResourceWithStreamingResponse(client.status) self.collections = collections.CollectionsResourceWithStreamingResponse(client.collections) self.documents = documents.DocumentsResourceWithStreamingResponse(client.documents) self.queries = queries.QueriesResourceWithStreamingResponse(client.queries) - self.parsers = parsers.ParsersResourceWithStreamingResponse(client.parsers) + self.models = models.ModelsResourceWithStreamingResponse(client.models) class AsyncZeroEntropyWithStreamedResponse: def __init__(self, client: AsyncZeroEntropy) -> None: - self.admin = admin.AsyncAdminResourceWithStreamingResponse(client.admin) self.status = status.AsyncStatusResourceWithStreamingResponse(client.status) self.collections = collections.AsyncCollectionsResourceWithStreamingResponse(client.collections) self.documents = documents.AsyncDocumentsResourceWithStreamingResponse(client.documents) self.queries = queries.AsyncQueriesResourceWithStreamingResponse(client.queries) - self.parsers = parsers.AsyncParsersResourceWithStreamingResponse(client.parsers) + self.models = models.AsyncModelsResourceWithStreamingResponse(client.models) Client = ZeroEntropy diff --git a/src/zeroentropy/_version.py b/src/zeroentropy/_version.py index 9922b27..24ebcef 100644 --- a/src/zeroentropy/_version.py +++ b/src/zeroentropy/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "zeroentropy" -__version__ = "0.1.0-alpha.5" # x-release-please-version +__version__ = "0.1.0-alpha.6" # x-release-please-version diff --git a/src/zeroentropy/resources/__init__.py b/src/zeroentropy/resources/__init__.py index d4929d1..072fd26 100644 --- a/src/zeroentropy/resources/__init__.py +++ b/src/zeroentropy/resources/__init__.py @@ -1,12 +1,12 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from .admin import ( - AdminResource, - AsyncAdminResource, - AdminResourceWithRawResponse, - AsyncAdminResourceWithRawResponse, - AdminResourceWithStreamingResponse, - AsyncAdminResourceWithStreamingResponse, +from .models import ( + ModelsResource, + AsyncModelsResource, + ModelsResourceWithRawResponse, + AsyncModelsResourceWithRawResponse, + ModelsResourceWithStreamingResponse, + AsyncModelsResourceWithStreamingResponse, ) from .status import ( StatusResource, @@ -16,14 +16,6 @@ StatusResourceWithStreamingResponse, AsyncStatusResourceWithStreamingResponse, ) -from .parsers import ( - ParsersResource, - AsyncParsersResource, - ParsersResourceWithRawResponse, - AsyncParsersResourceWithRawResponse, - ParsersResourceWithStreamingResponse, - AsyncParsersResourceWithStreamingResponse, -) from .queries import ( QueriesResource, AsyncQueriesResource, @@ -50,12 +42,6 @@ ) __all__ = [ - "AdminResource", - "AsyncAdminResource", - "AdminResourceWithRawResponse", - "AsyncAdminResourceWithRawResponse", - "AdminResourceWithStreamingResponse", - "AsyncAdminResourceWithStreamingResponse", "StatusResource", "AsyncStatusResource", "StatusResourceWithRawResponse", @@ -80,10 +66,10 @@ "AsyncQueriesResourceWithRawResponse", "QueriesResourceWithStreamingResponse", "AsyncQueriesResourceWithStreamingResponse", - "ParsersResource", - "AsyncParsersResource", - "ParsersResourceWithRawResponse", - "AsyncParsersResourceWithRawResponse", - "ParsersResourceWithStreamingResponse", - "AsyncParsersResourceWithStreamingResponse", + "ModelsResource", + "AsyncModelsResource", + "ModelsResourceWithRawResponse", + "AsyncModelsResourceWithRawResponse", + "ModelsResourceWithStreamingResponse", + "AsyncModelsResourceWithStreamingResponse", ] diff --git a/src/zeroentropy/resources/admin.py b/src/zeroentropy/resources/admin.py deleted file mode 100644 index af2b633..0000000 --- a/src/zeroentropy/resources/admin.py +++ /dev/null @@ -1,181 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import httpx - -from ..types import admin_create_organization_params -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.admin_create_organization_response import AdminCreateOrganizationResponse - -__all__ = ["AdminResource", "AsyncAdminResource"] - - -class AdminResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> AdminResourceWithRawResponse: - """ - 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/zeroentropy-ai/zeroentropy-python#accessing-raw-response-data-eg-headers - """ - return AdminResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AdminResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/zeroentropy-ai/zeroentropy-python#with_streaming_response - """ - return AdminResourceWithStreamingResponse(self) - - def create_organization( - self, - *, - organization_name: 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, - ) -> AdminCreateOrganizationResponse: - """Creates or updates an organization with the provided organization name. - - An API - Key will be returned. - - Returns 201 if a new organization was created, 200 if an existing organization - was found. - - Args: - organization_name: The orgniazation name to create. Must be unique. - - 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( - "/admin/create-organization", - body=maybe_transform( - {"organization_name": organization_name}, admin_create_organization_params.AdminCreateOrganizationParams - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AdminCreateOrganizationResponse, - ) - - -class AsyncAdminResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncAdminResourceWithRawResponse: - """ - 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/zeroentropy-ai/zeroentropy-python#accessing-raw-response-data-eg-headers - """ - return AsyncAdminResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncAdminResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/zeroentropy-ai/zeroentropy-python#with_streaming_response - """ - return AsyncAdminResourceWithStreamingResponse(self) - - async def create_organization( - self, - *, - organization_name: 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, - ) -> AdminCreateOrganizationResponse: - """Creates or updates an organization with the provided organization name. - - An API - Key will be returned. - - Returns 201 if a new organization was created, 200 if an existing organization - was found. - - Args: - organization_name: The orgniazation name to create. Must be unique. - - 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( - "/admin/create-organization", - body=await async_maybe_transform( - {"organization_name": organization_name}, admin_create_organization_params.AdminCreateOrganizationParams - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AdminCreateOrganizationResponse, - ) - - -class AdminResourceWithRawResponse: - def __init__(self, admin: AdminResource) -> None: - self._admin = admin - - self.create_organization = to_raw_response_wrapper( - admin.create_organization, - ) - - -class AsyncAdminResourceWithRawResponse: - def __init__(self, admin: AsyncAdminResource) -> None: - self._admin = admin - - self.create_organization = async_to_raw_response_wrapper( - admin.create_organization, - ) - - -class AdminResourceWithStreamingResponse: - def __init__(self, admin: AdminResource) -> None: - self._admin = admin - - self.create_organization = to_streamed_response_wrapper( - admin.create_organization, - ) - - -class AsyncAdminResourceWithStreamingResponse: - def __init__(self, admin: AsyncAdminResource) -> None: - self._admin = admin - - self.create_organization = async_to_streamed_response_wrapper( - admin.create_organization, - ) diff --git a/src/zeroentropy/resources/documents.py b/src/zeroentropy/resources/documents.py index f23eba1..bbe7f76 100644 --- a/src/zeroentropy/resources/documents.py +++ b/src/zeroentropy/resources/documents.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Dict, List, Union, Optional +from typing_extensions import Literal import httpx @@ -61,6 +62,7 @@ def update( *, collection_name: str, path: str, + index_status: Optional[Literal["not_parsed", "not_indexed"]] | NotGiven = NOT_GIVEN, metadata: Optional[Dict[str, Union[str, List[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. @@ -73,14 +75,15 @@ def update( This endpoint is atomic. - The only attribute currently supported for update is `metadata`. This endpoint - can only be called with a non-null `metadata` if the document status is - `indexed`. + Currently both `metadata` and `index_status` are supported. - Sometimes, when updating a document, a new document ID will be assigned and the - previous will be deleted. For this reason, the previous and the new document ID - will both be returned in the response. If the document ID was not updated, then - these two IDs will be identical. + - When updating with a non-null `metadata`, the document must have + `index_status` of `indexed`. After this call, the document will have an + `index_status` of `not_indexed`, since the document will need to reindex with + the new metadata. + - When updating with a non-null `index_status`, setting it to + `not_parsed or `not_indexed`requires that the document must have`index_status`of`parsing_failed`or`indexing_failed`, + respectively. A `404 Not Found` status code will be returned, if the provided collection name or document path does not exist. @@ -91,6 +94,11 @@ def update( path: The filepath of the document that you are updating. A `404 Not Found` status code will be returned if no document with this path was found. + index_status: If the document is in the index_status of + `parsing_failed or `indexing_failed`, then this endpoint allows you to update the index status to `not_parsed`and`not_indexed`, + respectively. This allows the document to re-attempt to parse/index after + failure. + metadata: If this field is provided, the given metadata json will replace the document's existing metadata json. In other words, if you want to add a new field, you will need to provide the entire metadata object (Both the original fields, and the @@ -110,6 +118,7 @@ def update( { "collection_name": collection_name, "path": path, + "index_status": index_status, "metadata": metadata, }, document_update_params.DocumentUpdateParams, @@ -456,6 +465,7 @@ async def update( *, collection_name: str, path: str, + index_status: Optional[Literal["not_parsed", "not_indexed"]] | NotGiven = NOT_GIVEN, metadata: Optional[Dict[str, Union[str, List[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. @@ -468,14 +478,15 @@ async def update( This endpoint is atomic. - The only attribute currently supported for update is `metadata`. This endpoint - can only be called with a non-null `metadata` if the document status is - `indexed`. + Currently both `metadata` and `index_status` are supported. - Sometimes, when updating a document, a new document ID will be assigned and the - previous will be deleted. For this reason, the previous and the new document ID - will both be returned in the response. If the document ID was not updated, then - these two IDs will be identical. + - When updating with a non-null `metadata`, the document must have + `index_status` of `indexed`. After this call, the document will have an + `index_status` of `not_indexed`, since the document will need to reindex with + the new metadata. + - When updating with a non-null `index_status`, setting it to + `not_parsed or `not_indexed`requires that the document must have`index_status`of`parsing_failed`or`indexing_failed`, + respectively. A `404 Not Found` status code will be returned, if the provided collection name or document path does not exist. @@ -486,6 +497,11 @@ async def update( path: The filepath of the document that you are updating. A `404 Not Found` status code will be returned if no document with this path was found. + index_status: If the document is in the index_status of + `parsing_failed or `indexing_failed`, then this endpoint allows you to update the index status to `not_parsed`and`not_indexed`, + respectively. This allows the document to re-attempt to parse/index after + failure. + metadata: If this field is provided, the given metadata json will replace the document's existing metadata json. In other words, if you want to add a new field, you will need to provide the entire metadata object (Both the original fields, and the @@ -505,6 +521,7 @@ async def update( { "collection_name": collection_name, "path": path, + "index_status": index_status, "metadata": metadata, }, document_update_params.DocumentUpdateParams, diff --git a/src/zeroentropy/resources/models.py b/src/zeroentropy/resources/models.py new file mode 100644 index 0000000..09db25a --- /dev/null +++ b/src/zeroentropy/resources/models.py @@ -0,0 +1,219 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Optional + +import httpx + +from ..types import model_rerank_params +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.model_rerank_response import ModelRerankResponse + +__all__ = ["ModelsResource", "AsyncModelsResource"] + + +class ModelsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ModelsResourceWithRawResponse: + """ + 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/zeroentropy-ai/zeroentropy-python#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/zeroentropy-ai/zeroentropy-python#with_streaming_response + """ + return ModelsResourceWithStreamingResponse(self) + + def rerank( + self, + *, + documents: List[str], + query: str, + model: str | NotGiven = NOT_GIVEN, + top_n: Optional[int] | 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, + ) -> ModelRerankResponse: + """ + Reranks the provided documents, according to the provided query. + + The results will be sorted by descending order of relevance. For each document, + the index and the score will be returned. The index is relative to the documents + array that was passed in. The score is the query-document relevancy determined + by the reranker model. The value will be returned in descending order to + relevance. + + Args: + documents: The list of documents to rerank. Each document is a string. + + query: The query to rerank the documents by. Results will be in descending order of + relevance. + + model: The model ID to use for reranking. Options are: ["zerank-1-large"] + + top_n: If provided, then only the top `n` documents will be returned in the results + array. + + 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( + "/models/rerank", + body=maybe_transform( + { + "documents": documents, + "query": query, + "model": model, + "top_n": top_n, + }, + model_rerank_params.ModelRerankParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ModelRerankResponse, + ) + + +class AsyncModelsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncModelsResourceWithRawResponse: + """ + 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/zeroentropy-ai/zeroentropy-python#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/zeroentropy-ai/zeroentropy-python#with_streaming_response + """ + return AsyncModelsResourceWithStreamingResponse(self) + + async def rerank( + self, + *, + documents: List[str], + query: str, + model: str | NotGiven = NOT_GIVEN, + top_n: Optional[int] | 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, + ) -> ModelRerankResponse: + """ + Reranks the provided documents, according to the provided query. + + The results will be sorted by descending order of relevance. For each document, + the index and the score will be returned. The index is relative to the documents + array that was passed in. The score is the query-document relevancy determined + by the reranker model. The value will be returned in descending order to + relevance. + + Args: + documents: The list of documents to rerank. Each document is a string. + + query: The query to rerank the documents by. Results will be in descending order of + relevance. + + model: The model ID to use for reranking. Options are: ["zerank-1-large"] + + top_n: If provided, then only the top `n` documents will be returned in the results + array. + + 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( + "/models/rerank", + body=await async_maybe_transform( + { + "documents": documents, + "query": query, + "model": model, + "top_n": top_n, + }, + model_rerank_params.ModelRerankParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ModelRerankResponse, + ) + + +class ModelsResourceWithRawResponse: + def __init__(self, models: ModelsResource) -> None: + self._models = models + + self.rerank = to_raw_response_wrapper( + models.rerank, + ) + + +class AsyncModelsResourceWithRawResponse: + def __init__(self, models: AsyncModelsResource) -> None: + self._models = models + + self.rerank = async_to_raw_response_wrapper( + models.rerank, + ) + + +class ModelsResourceWithStreamingResponse: + def __init__(self, models: ModelsResource) -> None: + self._models = models + + self.rerank = to_streamed_response_wrapper( + models.rerank, + ) + + +class AsyncModelsResourceWithStreamingResponse: + def __init__(self, models: AsyncModelsResource) -> None: + self._models = models + + self.rerank = async_to_streamed_response_wrapper( + models.rerank, + ) diff --git a/src/zeroentropy/resources/parsers.py b/src/zeroentropy/resources/parsers.py deleted file mode 100644 index 0d0d002..0000000 --- a/src/zeroentropy/resources/parsers.py +++ /dev/null @@ -1,183 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import httpx - -from ..types import parser_parse_document_params -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.parser_parse_document_response import ParserParseDocumentResponse - -__all__ = ["ParsersResource", "AsyncParsersResource"] - - -class ParsersResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> ParsersResourceWithRawResponse: - """ - 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/zeroentropy-ai/zeroentropy-python#accessing-raw-response-data-eg-headers - """ - return ParsersResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> ParsersResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/zeroentropy-ai/zeroentropy-python#with_streaming_response - """ - return ParsersResourceWithStreamingResponse(self) - - def parse_document( - self, - *, - base64_data: 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, - ) -> ParserParseDocumentResponse: - """This provides access to the parsers that we use for indexing. - - This endpoint will - not access any collection or search index, and the result will not be saved. - This will use the same parsing method as the `/documents/add-document` endpoint. - - A common use-case for this endpoint, is to use our parser in combination with - your own pre-processing step, before then uploading it to the search index using - the `text-pages` filetype. - - Args: - base64_data: The document's raw data, as a base64-encoded string - - 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( - "/parsers/parse-document", - body=maybe_transform({"base64_data": base64_data}, parser_parse_document_params.ParserParseDocumentParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ParserParseDocumentResponse, - ) - - -class AsyncParsersResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncParsersResourceWithRawResponse: - """ - 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/zeroentropy-ai/zeroentropy-python#accessing-raw-response-data-eg-headers - """ - return AsyncParsersResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncParsersResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/zeroentropy-ai/zeroentropy-python#with_streaming_response - """ - return AsyncParsersResourceWithStreamingResponse(self) - - async def parse_document( - self, - *, - base64_data: 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, - ) -> ParserParseDocumentResponse: - """This provides access to the parsers that we use for indexing. - - This endpoint will - not access any collection or search index, and the result will not be saved. - This will use the same parsing method as the `/documents/add-document` endpoint. - - A common use-case for this endpoint, is to use our parser in combination with - your own pre-processing step, before then uploading it to the search index using - the `text-pages` filetype. - - Args: - base64_data: The document's raw data, as a base64-encoded string - - 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( - "/parsers/parse-document", - body=await async_maybe_transform( - {"base64_data": base64_data}, parser_parse_document_params.ParserParseDocumentParams - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ParserParseDocumentResponse, - ) - - -class ParsersResourceWithRawResponse: - def __init__(self, parsers: ParsersResource) -> None: - self._parsers = parsers - - self.parse_document = to_raw_response_wrapper( - parsers.parse_document, - ) - - -class AsyncParsersResourceWithRawResponse: - def __init__(self, parsers: AsyncParsersResource) -> None: - self._parsers = parsers - - self.parse_document = async_to_raw_response_wrapper( - parsers.parse_document, - ) - - -class ParsersResourceWithStreamingResponse: - def __init__(self, parsers: ParsersResource) -> None: - self._parsers = parsers - - self.parse_document = to_streamed_response_wrapper( - parsers.parse_document, - ) - - -class AsyncParsersResourceWithStreamingResponse: - def __init__(self, parsers: AsyncParsersResource) -> None: - self._parsers = parsers - - self.parse_document = async_to_streamed_response_wrapper( - parsers.parse_document, - ) diff --git a/src/zeroentropy/resources/queries.py b/src/zeroentropy/resources/queries.py index f0c0958..635e01a 100644 --- a/src/zeroentropy/resources/queries.py +++ b/src/zeroentropy/resources/queries.py @@ -55,6 +55,7 @@ def top_documents( filter: Optional[Dict[str, object]] | NotGiven = NOT_GIVEN, include_metadata: bool | NotGiven = NOT_GIVEN, latency_mode: Literal["low", "high"] | NotGiven = NOT_GIVEN, + reranker: 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, @@ -72,8 +73,7 @@ def top_documents( your filters, then fewer may be returned. This number must be between 1 and 2048, inclusive. - query: The natural language query to search with. This cannot exceed 4096 characters (A - single UTF-8 codepoint, is considered to be 1 character). + query: The natural language query to search with. This cannot exceed 4096 UTF-8 bytes. filter: The query filter to apply. Please read [Metadata Filtering](/metadata-filtering) for more information. If not provided, then all documents will be searched. @@ -87,6 +87,10 @@ def top_documents( default of "low" and only swap if you need an additional improvement in search result quality. + reranker: The reranker to use after initial retrieval. The default is `null`. You can find + available model ids along with more information at + [/models/rerank](/api-reference/models/rerank). + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -105,6 +109,7 @@ def top_documents( "filter": filter, "include_metadata": include_metadata, "latency_mode": latency_mode, + "reranker": reranker, }, query_top_documents_params.QueryTopDocumentsParams, ), @@ -137,11 +142,10 @@ def top_pages( collection_name: The name of the collection. k: The number of pages to return. If there are not enough pages matching your - filters, then fewer may be returned. This number must be between 1 and 2048, + filters, then fewer may be returned. This number must be between 1 and 1024, inclusive. - query: The natural language query to search with. This cannot exceed 4096 characters (A - single UTF-8 codepoint, is considered to be 1 character). + query: The natural language query to search with. This cannot exceed 4096 UTF-8 bytes. filter: The query filter to apply. Please read [Metadata Filtering](/metadata-filtering) for more information. If not provided, then all documents will be searched. @@ -189,8 +193,8 @@ def top_snippets( query: str, filter: Optional[Dict[str, object]] | NotGiven = NOT_GIVEN, include_document_metadata: bool | NotGiven = NOT_GIVEN, - latency_mode: Literal["low"] | NotGiven = NOT_GIVEN, precise_responses: bool | NotGiven = NOT_GIVEN, + reranker: 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, @@ -209,7 +213,7 @@ def top_snippets( collection_name: The name of the collection. k: The number of snippets to return. If there are not enough snippets matching your - filters, then fewer may be returned. This number must be between 1 and 2048, + filters, then fewer may be returned. This number must be between 1 and 128, inclusive. query: The natural language query to search with. This cannot exceed 4096 characters (A @@ -222,17 +226,15 @@ def top_snippets( metadata. This is false by default, as returning metadata can add overhead if the amount of data to return is large. - latency_mode: Note that for Top K Snippets, only latency_mode "low" is available. This option - selects between our latency modes. The higher latency mode takes longer, but can - allow for more accurate responses. If desired, test both to customize your - search experience for your particular use-case, or use the default of "low" and - only swap if you need an additional improvement in search result quality. - precise_responses: Enable precise responses. Precise responses will have higher latency, but provide much more precise snippets. When `precise_responses` is set to `true`, the responses will average 200 characters. If set to `false`, the responses will average 2000 characters. The default is `false`. + reranker: The reranker to use after initial retrieval. The default is `null`. You can find + available model ids, along with more information, at + [/models/rerank](/api-reference/models/rerank). + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -250,8 +252,8 @@ def top_snippets( "query": query, "filter": filter, "include_document_metadata": include_document_metadata, - "latency_mode": latency_mode, "precise_responses": precise_responses, + "reranker": reranker, }, query_top_snippets_params.QueryTopSnippetsParams, ), @@ -291,6 +293,7 @@ async def top_documents( filter: Optional[Dict[str, object]] | NotGiven = NOT_GIVEN, include_metadata: bool | NotGiven = NOT_GIVEN, latency_mode: Literal["low", "high"] | NotGiven = NOT_GIVEN, + reranker: 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, @@ -308,8 +311,7 @@ async def top_documents( your filters, then fewer may be returned. This number must be between 1 and 2048, inclusive. - query: The natural language query to search with. This cannot exceed 4096 characters (A - single UTF-8 codepoint, is considered to be 1 character). + query: The natural language query to search with. This cannot exceed 4096 UTF-8 bytes. filter: The query filter to apply. Please read [Metadata Filtering](/metadata-filtering) for more information. If not provided, then all documents will be searched. @@ -323,6 +325,10 @@ async def top_documents( default of "low" and only swap if you need an additional improvement in search result quality. + reranker: The reranker to use after initial retrieval. The default is `null`. You can find + available model ids along with more information at + [/models/rerank](/api-reference/models/rerank). + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -341,6 +347,7 @@ async def top_documents( "filter": filter, "include_metadata": include_metadata, "latency_mode": latency_mode, + "reranker": reranker, }, query_top_documents_params.QueryTopDocumentsParams, ), @@ -373,11 +380,10 @@ async def top_pages( collection_name: The name of the collection. k: The number of pages to return. If there are not enough pages matching your - filters, then fewer may be returned. This number must be between 1 and 2048, + filters, then fewer may be returned. This number must be between 1 and 1024, inclusive. - query: The natural language query to search with. This cannot exceed 4096 characters (A - single UTF-8 codepoint, is considered to be 1 character). + query: The natural language query to search with. This cannot exceed 4096 UTF-8 bytes. filter: The query filter to apply. Please read [Metadata Filtering](/metadata-filtering) for more information. If not provided, then all documents will be searched. @@ -425,8 +431,8 @@ async def top_snippets( query: str, filter: Optional[Dict[str, object]] | NotGiven = NOT_GIVEN, include_document_metadata: bool | NotGiven = NOT_GIVEN, - latency_mode: Literal["low"] | NotGiven = NOT_GIVEN, precise_responses: bool | NotGiven = NOT_GIVEN, + reranker: 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, @@ -445,7 +451,7 @@ async def top_snippets( collection_name: The name of the collection. k: The number of snippets to return. If there are not enough snippets matching your - filters, then fewer may be returned. This number must be between 1 and 2048, + filters, then fewer may be returned. This number must be between 1 and 128, inclusive. query: The natural language query to search with. This cannot exceed 4096 characters (A @@ -458,17 +464,15 @@ async def top_snippets( metadata. This is false by default, as returning metadata can add overhead if the amount of data to return is large. - latency_mode: Note that for Top K Snippets, only latency_mode "low" is available. This option - selects between our latency modes. The higher latency mode takes longer, but can - allow for more accurate responses. If desired, test both to customize your - search experience for your particular use-case, or use the default of "low" and - only swap if you need an additional improvement in search result quality. - precise_responses: Enable precise responses. Precise responses will have higher latency, but provide much more precise snippets. When `precise_responses` is set to `true`, the responses will average 200 characters. If set to `false`, the responses will average 2000 characters. The default is `false`. + reranker: The reranker to use after initial retrieval. The default is `null`. You can find + available model ids, along with more information, at + [/models/rerank](/api-reference/models/rerank). + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -486,8 +490,8 @@ async def top_snippets( "query": query, "filter": filter, "include_document_metadata": include_document_metadata, - "latency_mode": latency_mode, "precise_responses": precise_responses, + "reranker": reranker, }, query_top_snippets_params.QueryTopSnippetsParams, ), diff --git a/src/zeroentropy/types/__init__.py b/src/zeroentropy/types/__init__.py index 637283b..1117c5f 100644 --- a/src/zeroentropy/types/__init__.py +++ b/src/zeroentropy/types/__init__.py @@ -3,8 +3,10 @@ from __future__ import annotations from .document_add_params import DocumentAddParams as DocumentAddParams +from .model_rerank_params import ModelRerankParams as ModelRerankParams from .collection_add_params import CollectionAddParams as CollectionAddParams from .document_add_response import DocumentAddResponse as DocumentAddResponse +from .model_rerank_response import ModelRerankResponse as ModelRerankResponse from .document_delete_params import DocumentDeleteParams as DocumentDeleteParams from .document_update_params import DocumentUpdateParams as DocumentUpdateParams from .query_top_pages_params import QueryTopPagesParams as QueryTopPagesParams @@ -22,12 +24,8 @@ from .status_get_status_response import StatusGetStatusResponse as StatusGetStatusResponse from .query_top_snippets_response import QueryTopSnippetsResponse as QueryTopSnippetsResponse from .collection_get_list_response import CollectionGetListResponse as CollectionGetListResponse -from .parser_parse_document_params import ParserParseDocumentParams as ParserParseDocumentParams from .query_top_documents_response import QueryTopDocumentsResponse as QueryTopDocumentsResponse from .document_get_info_list_params import DocumentGetInfoListParams as DocumentGetInfoListParams from .document_get_page_info_params import DocumentGetPageInfoParams as DocumentGetPageInfoParams -from .parser_parse_document_response import ParserParseDocumentResponse as ParserParseDocumentResponse from .document_get_info_list_response import DocumentGetInfoListResponse as DocumentGetInfoListResponse from .document_get_page_info_response import DocumentGetPageInfoResponse as DocumentGetPageInfoResponse -from .admin_create_organization_params import AdminCreateOrganizationParams as AdminCreateOrganizationParams -from .admin_create_organization_response import AdminCreateOrganizationResponse as AdminCreateOrganizationResponse diff --git a/src/zeroentropy/types/admin_create_organization_params.py b/src/zeroentropy/types/admin_create_organization_params.py deleted file mode 100644 index f1345b4..0000000 --- a/src/zeroentropy/types/admin_create_organization_params.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, TypedDict - -__all__ = ["AdminCreateOrganizationParams"] - - -class AdminCreateOrganizationParams(TypedDict, total=False): - organization_name: Required[str] - """The orgniazation name to create. Must be unique.""" diff --git a/src/zeroentropy/types/admin_create_organization_response.py b/src/zeroentropy/types/admin_create_organization_response.py deleted file mode 100644 index b2b8992..0000000 --- a/src/zeroentropy/types/admin_create_organization_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .._models import BaseModel - -__all__ = ["AdminCreateOrganizationResponse"] - - -class AdminCreateOrganizationResponse(BaseModel): - api_key: str - """The API Key for this organization.""" - - organization_name: str - """The name of the organization""" diff --git a/src/zeroentropy/types/document_get_info_list_response.py b/src/zeroentropy/types/document_get_info_list_response.py index 1df6f8b..3ffea36 100644 --- a/src/zeroentropy/types/document_get_info_list_response.py +++ b/src/zeroentropy/types/document_get_info_list_response.py @@ -1,6 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Dict, List, Union, Optional +from datetime import datetime from typing_extensions import Literal from .._models import BaseModel @@ -13,7 +14,7 @@ class DocumentGetInfoListResponse(BaseModel): collection_name: str - created_at: str + created_at: datetime file_url: str """ diff --git a/src/zeroentropy/types/document_update_params.py b/src/zeroentropy/types/document_update_params.py index ae49ffe..84acbe0 100644 --- a/src/zeroentropy/types/document_update_params.py +++ b/src/zeroentropy/types/document_update_params.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Dict, List, Union, Optional -from typing_extensions import Required, TypedDict +from typing_extensions import Literal, Required, TypedDict __all__ = ["DocumentUpdateParams"] @@ -19,6 +19,14 @@ class DocumentUpdateParams(TypedDict, total=False): found. """ + index_status: Optional[Literal["not_parsed", "not_indexed"]] + """ + If the document is in the index_status of + `parsing_failed or `indexing_failed`, then this endpoint allows you to update the index status to `not_parsed`and`not_indexed`, + respectively. This allows the document to re-attempt to parse/index after + failure. + """ + metadata: Optional[Dict[str, Union[str, List[str]]]] """ If this field is provided, the given metadata json will replace the document's diff --git a/src/zeroentropy/types/document_update_response.py b/src/zeroentropy/types/document_update_response.py index 13203cd..c936169 100644 --- a/src/zeroentropy/types/document_update_response.py +++ b/src/zeroentropy/types/document_update_response.py @@ -1,11 +1,12 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional + from .._models import BaseModel __all__ = ["DocumentUpdateResponse"] class DocumentUpdateResponse(BaseModel): - new_id: str - - previous_id: str + message: Optional[str] = None + """This string will always be "Success!". This may change in the future.""" diff --git a/src/zeroentropy/types/model_rerank_params.py b/src/zeroentropy/types/model_rerank_params.py new file mode 100644 index 0000000..e6d49fe --- /dev/null +++ b/src/zeroentropy/types/model_rerank_params.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Optional +from typing_extensions import Required, TypedDict + +__all__ = ["ModelRerankParams"] + + +class ModelRerankParams(TypedDict, total=False): + documents: Required[List[str]] + """The list of documents to rerank. Each document is a string.""" + + query: Required[str] + """The query to rerank the documents by. + + Results will be in descending order of relevance. + """ + + model: str + """The model ID to use for reranking. Options are: ["zerank-1-large"]""" + + top_n: Optional[int] + """ + If provided, then only the top `n` documents will be returned in the results + array. + """ diff --git a/src/zeroentropy/types/model_rerank_response.py b/src/zeroentropy/types/model_rerank_response.py new file mode 100644 index 0000000..ff8975a --- /dev/null +++ b/src/zeroentropy/types/model_rerank_response.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel + +__all__ = ["ModelRerankResponse", "Result"] + + +class Result(BaseModel): + index: int + """ + The index of this document, relative to the original document array passed into + the request. + """ + + relevance_score: float + """The relevance score between this document and the query. + + This number will range between 0.0 and 1.0. This score is dependent on only the + query and the scored document; other documents do not affect this score. This + value is deterministic, but may vary slightly due to floating point error. + """ + + +class ModelRerankResponse(BaseModel): + results: List[Result] + """The results, ordered by descending order of relevance to the query.""" diff --git a/src/zeroentropy/types/parser_parse_document_params.py b/src/zeroentropy/types/parser_parse_document_params.py deleted file mode 100644 index ebbd1e5..0000000 --- a/src/zeroentropy/types/parser_parse_document_params.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, TypedDict - -__all__ = ["ParserParseDocumentParams"] - - -class ParserParseDocumentParams(TypedDict, total=False): - base64_data: Required[str] - """The document's raw data, as a base64-encoded string""" diff --git a/src/zeroentropy/types/parser_parse_document_response.py b/src/zeroentropy/types/parser_parse_document_response.py deleted file mode 100644 index ed72ee8..0000000 --- a/src/zeroentropy/types/parser_parse_document_response.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List - -from .._models import BaseModel - -__all__ = ["ParserParseDocumentResponse"] - - -class ParserParseDocumentResponse(BaseModel): - pages: List[str] - """The parsed pages. Each string will contain the full contents of a page.""" diff --git a/src/zeroentropy/types/query_top_documents_params.py b/src/zeroentropy/types/query_top_documents_params.py index d5b3e10..0eb1eed 100644 --- a/src/zeroentropy/types/query_top_documents_params.py +++ b/src/zeroentropy/types/query_top_documents_params.py @@ -20,11 +20,7 @@ class QueryTopDocumentsParams(TypedDict, total=False): """ query: Required[str] - """The natural language query to search with. - - This cannot exceed 4096 characters (A single UTF-8 codepoint, is considered to - be 1 character). - """ + """The natural language query to search with. This cannot exceed 4096 UTF-8 bytes.""" filter: Optional[Dict[str, object]] """The query filter to apply. @@ -47,3 +43,10 @@ class QueryTopDocumentsParams(TypedDict, total=False): use-case, or use the default of "low" and only swap if you need an additional improvement in search result quality. """ + + reranker: Optional[str] + """The reranker to use after initial retrieval. + + The default is `null`. You can find available model ids along with more + information at [/models/rerank](/api-reference/models/rerank). + """ diff --git a/src/zeroentropy/types/query_top_pages_params.py b/src/zeroentropy/types/query_top_pages_params.py index 6c7024f..8a97698 100644 --- a/src/zeroentropy/types/query_top_pages_params.py +++ b/src/zeroentropy/types/query_top_pages_params.py @@ -16,15 +16,11 @@ class QueryTopPagesParams(TypedDict, total=False): """The number of pages to return. If there are not enough pages matching your filters, then fewer may be returned. - This number must be between 1 and 2048, inclusive. + This number must be between 1 and 1024, inclusive. """ query: Required[str] - """The natural language query to search with. - - This cannot exceed 4096 characters (A single UTF-8 codepoint, is considered to - be 1 character). - """ + """The natural language query to search with. This cannot exceed 4096 UTF-8 bytes.""" filter: Optional[Dict[str, object]] """The query filter to apply. diff --git a/src/zeroentropy/types/query_top_snippets_params.py b/src/zeroentropy/types/query_top_snippets_params.py index e7a29e2..036d657 100644 --- a/src/zeroentropy/types/query_top_snippets_params.py +++ b/src/zeroentropy/types/query_top_snippets_params.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Dict, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Required, TypedDict __all__ = ["QueryTopSnippetsParams"] @@ -16,7 +16,7 @@ class QueryTopSnippetsParams(TypedDict, total=False): """The number of snippets to return. If there are not enough snippets matching your filters, then fewer may be - returned. This number must be between 1 and 2048, inclusive. + returned. This number must be between 1 and 128, inclusive. """ query: Required[str] @@ -40,16 +40,6 @@ class QueryTopSnippetsParams(TypedDict, total=False): the amount of data to return is large. """ - latency_mode: Literal["low"] - """Note that for Top K Snippets, only latency_mode "low" is available. - - This option selects between our latency modes. The higher latency mode takes - longer, but can allow for more accurate responses. If desired, test both to - customize your search experience for your particular use-case, or use the - default of "low" and only swap if you need an additional improvement in search - result quality. - """ - precise_responses: bool """Enable precise responses. @@ -58,3 +48,10 @@ class QueryTopSnippetsParams(TypedDict, total=False): 200 characters. If set to `false`, the responses will average 2000 characters. The default is `false`. """ + + reranker: Optional[str] + """The reranker to use after initial retrieval. + + The default is `null`. You can find available model ids, along with more + information, at [/models/rerank](/api-reference/models/rerank). + """ diff --git a/src/zeroentropy/types/query_top_snippets_response.py b/src/zeroentropy/types/query_top_snippets_response.py index 3fc1450..9f21eb7 100644 --- a/src/zeroentropy/types/query_top_snippets_response.py +++ b/src/zeroentropy/types/query_top_snippets_response.py @@ -31,8 +31,8 @@ class DocumentResult(BaseModel): class Result(BaseModel): - content: Optional[str] = None - """If requested, this contains the full string content of this snippet.""" + content: str + """The full string content of this snippet.""" end_index: int """The end index of this snippet.""" diff --git a/tests/api_resources/test_admin.py b/tests/api_resources/test_admin.py deleted file mode 100644 index 32d06b9..0000000 --- a/tests/api_resources/test_admin.py +++ /dev/null @@ -1,84 +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 tests.utils import assert_matches_type -from zeroentropy import ZeroEntropy, AsyncZeroEntropy -from zeroentropy.types import AdminCreateOrganizationResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestAdmin: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - def test_method_create_organization(self, client: ZeroEntropy) -> None: - admin = client.admin.create_organization( - organization_name="organization_name", - ) - assert_matches_type(AdminCreateOrganizationResponse, admin, path=["response"]) - - @parametrize - def test_raw_response_create_organization(self, client: ZeroEntropy) -> None: - response = client.admin.with_raw_response.create_organization( - organization_name="organization_name", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - admin = response.parse() - assert_matches_type(AdminCreateOrganizationResponse, admin, path=["response"]) - - @parametrize - def test_streaming_response_create_organization(self, client: ZeroEntropy) -> None: - with client.admin.with_streaming_response.create_organization( - organization_name="organization_name", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - admin = response.parse() - assert_matches_type(AdminCreateOrganizationResponse, admin, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncAdmin: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - async def test_method_create_organization(self, async_client: AsyncZeroEntropy) -> None: - admin = await async_client.admin.create_organization( - organization_name="organization_name", - ) - assert_matches_type(AdminCreateOrganizationResponse, admin, path=["response"]) - - @parametrize - async def test_raw_response_create_organization(self, async_client: AsyncZeroEntropy) -> None: - response = await async_client.admin.with_raw_response.create_organization( - organization_name="organization_name", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - admin = await response.parse() - assert_matches_type(AdminCreateOrganizationResponse, admin, path=["response"]) - - @parametrize - async def test_streaming_response_create_organization(self, async_client: AsyncZeroEntropy) -> None: - async with async_client.admin.with_streaming_response.create_organization( - organization_name="organization_name", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - admin = await response.parse() - assert_matches_type(AdminCreateOrganizationResponse, admin, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_collections.py b/tests/api_resources/test_collections.py index de9f533..249c483 100644 --- a/tests/api_resources/test_collections.py +++ b/tests/api_resources/test_collections.py @@ -110,7 +110,9 @@ def test_streaming_response_get_list(self, client: ZeroEntropy) -> None: class TestAsyncCollections: - 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_delete(self, async_client: AsyncZeroEntropy) -> None: diff --git a/tests/api_resources/test_documents.py b/tests/api_resources/test_documents.py index dd80f98..bd094f7 100644 --- a/tests/api_resources/test_documents.py +++ b/tests/api_resources/test_documents.py @@ -38,6 +38,7 @@ def test_method_update_with_all_params(self, client: ZeroEntropy) -> None: document = client.documents.update( collection_name="collection_name", path="path", + index_status="not_parsed", metadata={"foo": "string"}, ) assert_matches_type(DocumentUpdateResponse, document, path=["response"]) @@ -295,7 +296,9 @@ def test_streaming_response_get_page_info(self, client: ZeroEntropy) -> None: class TestAsyncDocuments: - 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_update(self, async_client: AsyncZeroEntropy) -> None: @@ -310,6 +313,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncZeroEntrop document = await async_client.documents.update( collection_name="collection_name", path="path", + index_status="not_parsed", metadata={"foo": "string"}, ) assert_matches_type(DocumentUpdateResponse, document, path=["response"]) diff --git a/tests/api_resources/test_models.py b/tests/api_resources/test_models.py new file mode 100644 index 0000000..23ffa4f --- /dev/null +++ b/tests/api_resources/test_models.py @@ -0,0 +1,112 @@ +# 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 tests.utils import assert_matches_type +from zeroentropy import ZeroEntropy, AsyncZeroEntropy +from zeroentropy.types import ModelRerankResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestModels: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_rerank(self, client: ZeroEntropy) -> None: + model = client.models.rerank( + documents=["string"], + query="query", + ) + assert_matches_type(ModelRerankResponse, model, path=["response"]) + + @parametrize + def test_method_rerank_with_all_params(self, client: ZeroEntropy) -> None: + model = client.models.rerank( + documents=["string"], + query="query", + model="model", + top_n=0, + ) + assert_matches_type(ModelRerankResponse, model, path=["response"]) + + @parametrize + def test_raw_response_rerank(self, client: ZeroEntropy) -> None: + response = client.models.with_raw_response.rerank( + documents=["string"], + query="query", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + model = response.parse() + assert_matches_type(ModelRerankResponse, model, path=["response"]) + + @parametrize + def test_streaming_response_rerank(self, client: ZeroEntropy) -> None: + with client.models.with_streaming_response.rerank( + documents=["string"], + query="query", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + model = response.parse() + assert_matches_type(ModelRerankResponse, model, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncModels: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_rerank(self, async_client: AsyncZeroEntropy) -> None: + model = await async_client.models.rerank( + documents=["string"], + query="query", + ) + assert_matches_type(ModelRerankResponse, model, path=["response"]) + + @parametrize + async def test_method_rerank_with_all_params(self, async_client: AsyncZeroEntropy) -> None: + model = await async_client.models.rerank( + documents=["string"], + query="query", + model="model", + top_n=0, + ) + assert_matches_type(ModelRerankResponse, model, path=["response"]) + + @parametrize + async def test_raw_response_rerank(self, async_client: AsyncZeroEntropy) -> None: + response = await async_client.models.with_raw_response.rerank( + documents=["string"], + query="query", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + model = await response.parse() + assert_matches_type(ModelRerankResponse, model, path=["response"]) + + @parametrize + async def test_streaming_response_rerank(self, async_client: AsyncZeroEntropy) -> None: + async with async_client.models.with_streaming_response.rerank( + documents=["string"], + query="query", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + model = await response.parse() + assert_matches_type(ModelRerankResponse, model, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_parsers.py b/tests/api_resources/test_parsers.py deleted file mode 100644 index c38088a..0000000 --- a/tests/api_resources/test_parsers.py +++ /dev/null @@ -1,84 +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 tests.utils import assert_matches_type -from zeroentropy import ZeroEntropy, AsyncZeroEntropy -from zeroentropy.types import ParserParseDocumentResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestParsers: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - def test_method_parse_document(self, client: ZeroEntropy) -> None: - parser = client.parsers.parse_document( - base64_data="base64_data", - ) - assert_matches_type(ParserParseDocumentResponse, parser, path=["response"]) - - @parametrize - def test_raw_response_parse_document(self, client: ZeroEntropy) -> None: - response = client.parsers.with_raw_response.parse_document( - base64_data="base64_data", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - parser = response.parse() - assert_matches_type(ParserParseDocumentResponse, parser, path=["response"]) - - @parametrize - def test_streaming_response_parse_document(self, client: ZeroEntropy) -> None: - with client.parsers.with_streaming_response.parse_document( - base64_data="base64_data", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - parser = response.parse() - assert_matches_type(ParserParseDocumentResponse, parser, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncParsers: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - async def test_method_parse_document(self, async_client: AsyncZeroEntropy) -> None: - parser = await async_client.parsers.parse_document( - base64_data="base64_data", - ) - assert_matches_type(ParserParseDocumentResponse, parser, path=["response"]) - - @parametrize - async def test_raw_response_parse_document(self, async_client: AsyncZeroEntropy) -> None: - response = await async_client.parsers.with_raw_response.parse_document( - base64_data="base64_data", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - parser = await response.parse() - assert_matches_type(ParserParseDocumentResponse, parser, path=["response"]) - - @parametrize - async def test_streaming_response_parse_document(self, async_client: AsyncZeroEntropy) -> None: - async with async_client.parsers.with_streaming_response.parse_document( - base64_data="base64_data", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - parser = await response.parse() - assert_matches_type(ParserParseDocumentResponse, parser, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_queries.py b/tests/api_resources/test_queries.py index f406a4f..80e938c 100644 --- a/tests/api_resources/test_queries.py +++ b/tests/api_resources/test_queries.py @@ -39,6 +39,7 @@ def test_method_top_documents_with_all_params(self, client: ZeroEntropy) -> None filter={"foo": "bar"}, include_metadata=True, latency_mode="low", + reranker="reranker", ) assert_matches_type(QueryTopDocumentsResponse, query, path=["response"]) @@ -136,8 +137,8 @@ def test_method_top_snippets_with_all_params(self, client: ZeroEntropy) -> None: query="query", filter={"foo": "bar"}, include_document_metadata=True, - latency_mode="low", precise_responses=True, + reranker="reranker", ) assert_matches_type(QueryTopSnippetsResponse, query, path=["response"]) @@ -171,7 +172,9 @@ def test_streaming_response_top_snippets(self, client: ZeroEntropy) -> None: class TestAsyncQueries: - 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_top_documents(self, async_client: AsyncZeroEntropy) -> None: @@ -191,6 +194,7 @@ async def test_method_top_documents_with_all_params(self, async_client: AsyncZer filter={"foo": "bar"}, include_metadata=True, latency_mode="low", + reranker="reranker", ) assert_matches_type(QueryTopDocumentsResponse, query, path=["response"]) @@ -288,8 +292,8 @@ async def test_method_top_snippets_with_all_params(self, async_client: AsyncZero query="query", filter={"foo": "bar"}, include_document_metadata=True, - latency_mode="low", precise_responses=True, + reranker="reranker", ) assert_matches_type(QueryTopSnippetsResponse, query, path=["response"]) diff --git a/tests/api_resources/test_status.py b/tests/api_resources/test_status.py index fb7a261..9d2787c 100644 --- a/tests/api_resources/test_status.py +++ b/tests/api_resources/test_status.py @@ -51,7 +51,9 @@ def test_streaming_response_get_status(self, client: ZeroEntropy) -> None: class TestAsyncStatus: - 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_get_status(self, async_client: AsyncZeroEntropy) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index 9ec316c..fc50145 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + from __future__ import annotations import os import logging from typing import TYPE_CHECKING, Iterator, AsyncIterator +import httpx import pytest from pytest_asyncio import is_async_test -from zeroentropy import ZeroEntropy, AsyncZeroEntropy +from zeroentropy import ZeroEntropy, AsyncZeroEntropy, DefaultAioHttpClient +from zeroentropy._utils import is_dict if TYPE_CHECKING: from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] @@ -25,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") @@ -43,9 +60,25 @@ def client(request: FixtureRequest) -> Iterator[ZeroEntropy]: @pytest.fixture(scope="session") async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncZeroEntropy]: - strict = getattr(request, "param", True) - if not isinstance(strict, bool): - raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - - async with AsyncZeroEntropy(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 AsyncZeroEntropy( + base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client + ) as client: yield client diff --git a/tests/test_client.py b/tests/test_client.py index 8113248..b7b0f8c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,17 +23,16 @@ from zeroentropy import ZeroEntropy, AsyncZeroEntropy, APIResponseValidationError from zeroentropy._types import Omit -from zeroentropy._utils import maybe_transform from zeroentropy._models import BaseModel, FinalRequestOptions -from zeroentropy._constants import RAW_RESPONSE_HEADER from zeroentropy._exceptions import APIStatusError, APITimeoutError, ZeroEntropyError, APIResponseValidationError from zeroentropy._base_client import ( DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, + DefaultHttpxClient, + DefaultAsyncHttpxClient, make_request_options, ) -from zeroentropy.types.status_get_status_params import StatusGetStatusParams from .utils import update_env @@ -192,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") @@ -723,32 +723,21 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str @mock.patch("zeroentropy._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: ZeroEntropy) -> None: respx_mock.post("/status/get-status").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - self.client.post( - "/status/get-status", - body=cast(object, maybe_transform({}, StatusGetStatusParams)), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) + client.status.with_streaming_response.get_status().__enter__() assert _get_open_connections(self.client) == 0 @mock.patch("zeroentropy._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: ZeroEntropy) -> None: respx_mock.post("/status/get-status").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - self.client.post( - "/status/get-status", - body=cast(object, maybe_transform({}, StatusGetStatusParams)), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) - + client.status.with_streaming_response.get_status().__enter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -828,6 +817,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 @@ -991,6 +1002,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") @@ -1526,32 +1538,25 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte @mock.patch("zeroentropy._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: AsyncZeroEntropy + ) -> None: respx_mock.post("/status/get-status").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await self.client.post( - "/status/get-status", - body=cast(object, maybe_transform({}, StatusGetStatusParams)), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) + await async_client.status.with_streaming_response.get_status().__aenter__() assert _get_open_connections(self.client) == 0 @mock.patch("zeroentropy._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: AsyncZeroEntropy + ) -> None: respx_mock.post("/status/get-status").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await self.client.post( - "/status/get-status", - body=cast(object, maybe_transform({}, StatusGetStatusParams)), - cast_to=httpx.Response, - options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, - ) - + await async_client.status.with_streaming_response.get_status().__aenter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1679,6 +1684,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