diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3a9eb32..e945b15 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,7 +19,7 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
@@ -44,7 +44,7 @@ jobs:
id-token: write
runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
@@ -81,7 +81,7 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index f0a5b3c..9a3087b 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index ea04f96..a77924a 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'CASParser/cas-parser-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Check release environment
run: |
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 2601677..d0ab664 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "1.1.0"
+ ".": "1.2.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 92721c7..968a0a4 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 5
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml
-openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff
+configured_endpoints: 4
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-ce2296c4b14d27c141bb2745607d2456c923fdca3ae0a0a0800c26e564333850.yml
+openapi_spec_hash: 8eb586ccf16b534c0c15ff6a22274c7d
config_hash: cb5d75abef6264b5d86448caf7295afa
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eff9d05..156d0dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,34 @@
# Changelog
+## 1.2.0 (2026-01-20)
+
+Full Changelog: [v1.1.0...v1.2.0](https://github.com/CASParser/cas-parser-python/compare/v1.1.0...v1.2.0)
+
+### Features
+
+* **api:** api update ([93a9613](https://github.com/CASParser/cas-parser-python/commit/93a9613c79ec70869cf11dd0b9bac0a8c6194a31))
+* **api:** api update ([bd6977a](https://github.com/CASParser/cas-parser-python/commit/bd6977a8a78c4a1633e4e6a1dc1d3335b1aa6611))
+* **api:** api update ([3fda81d](https://github.com/CASParser/cas-parser-python/commit/3fda81deb938a9b689cbb04f839e3b815259a9c5))
+* **api:** api update ([f1838dc](https://github.com/CASParser/cas-parser-python/commit/f1838dcb901635626cc87cb55dfaa4ef33ba5092))
+
+
+### Bug Fixes
+
+* **client:** close streams without requiring full consumption ([7090ef5](https://github.com/CASParser/cas-parser-python/commit/7090ef51af296fa6d6be8af8137543ef2023cbd7))
+
+
+### Chores
+
+* bump `httpx-aiohttp` version to 0.1.9 ([e1b65fb](https://github.com/CASParser/cas-parser-python/commit/e1b65fb2bd146a68ef50438899406ae2fb6178c3))
+* do not install brew dependencies in ./scripts/bootstrap by default ([35b17eb](https://github.com/CASParser/cas-parser-python/commit/35b17eb26264ab66e24b074bcb1790f6c33b7b9c))
+* **internal/tests:** avoid race condition with implicit client cleanup ([2a58fc0](https://github.com/CASParser/cas-parser-python/commit/2a58fc0e260b52ee314ac6d14676b2140711bd0b))
+* **internal:** codegen related update ([8e6c5b2](https://github.com/CASParser/cas-parser-python/commit/8e6c5b210e14602af113fa9fef5c789d6238419a))
+* **internal:** codegen related update ([20bcea0](https://github.com/CASParser/cas-parser-python/commit/20bcea057ce1974149394c899581ed31ffb56a4a))
+* **internal:** detect missing future annotations with ruff ([8c35489](https://github.com/CASParser/cas-parser-python/commit/8c354893c00887af1da9c197dc21dd4d6f0033af))
+* **internal:** grammar fix (it's -> its) ([d2d29bc](https://github.com/CASParser/cas-parser-python/commit/d2d29bcc46989573e27c2178785c6b38df65bd90))
+* **internal:** update pydantic dependency ([1c3104b](https://github.com/CASParser/cas-parser-python/commit/1c3104b27350f4c906973bb56f89d5a16f55d35e))
+* **types:** change optional parameter type from NotGiven to Omit ([e739e12](https://github.com/CASParser/cas-parser-python/commit/e739e12ade4f91e52f0285c866354e970195aacf))
+
## 1.1.0 (2025-09-06)
Full Changelog: [v1.0.2...v1.1.0](https://github.com/CASParser/cas-parser-python/compare/v1.0.2...v1.1.0)
diff --git a/LICENSE b/LICENSE
index f1756ce..6bbb512 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2025 Cas Parser
+ Copyright 2026 Cas Parser
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index c13a2c4..d3f8ab4 100644
--- a/README.md
+++ b/README.md
@@ -3,12 +3,21 @@
[)](https://pypi.org/project/cas-parser-python/)
-The Cas Parser Python library provides convenient access to the Cas Parser REST API from any Python 3.8+
+The Cas Parser Python library provides convenient access to the Cas Parser REST API from any Python 3.9+
application. The library includes type definitions for all request params and response fields,
and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).
It is generated with [Stainless](https://www.stainless.com/).
+## MCP Server
+
+Use the Cas Parser MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.
+
+[](https://cursor.com/en-US/install-mcp?name=cas-parser-node-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImNhcy1wYXJzZXItbm9kZS1tY3AiXX0)
+[](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22cas-parser-node-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22cas-parser-node-mcp%22%5D%7D)
+
+> Note: You may need to set environment variables in your MCP client.
+
## Documentation
The REST API documentation can be found on [docs.casparser.in](https://docs.casparser.in/reference). The full API of this library can be found in [api.md](api.md).
@@ -85,6 +94,7 @@ pip install cas-parser-python[aiohttp]
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
```python
+import os
import asyncio
from cas_parser import DefaultAioHttpClient
from cas_parser import AsyncCasParser
@@ -92,7 +102,7 @@ from cas_parser import AsyncCasParser
async def main() -> None:
async with AsyncCasParser(
- api_key="My API Key",
+ api_key=os.environ.get("CAS_PARSER_API_KEY"), # This is the default and can be omitted
http_client=DefaultAioHttpClient(),
) as client:
unified_response = await client.cas_parser.smart_parse(
@@ -380,7 +390,7 @@ print(cas_parser.__version__)
## Requirements
-Python 3.8 or higher.
+Python 3.9 or higher.
## Contributing
diff --git a/api.md b/api.md
index 9f56f41..7a55253 100644
--- a/api.md
+++ b/api.md
@@ -12,15 +12,3 @@ Methods:
- client.cas_parser.cdsl(\*\*params) -> UnifiedResponse
- client.cas_parser.nsdl(\*\*params) -> UnifiedResponse
- client.cas_parser.smart_parse(\*\*params) -> UnifiedResponse
-
-# CasGenerator
-
-Types:
-
-```python
-from cas_parser.types import CasGeneratorGenerateCasResponse
-```
-
-Methods:
-
-- client.cas_generator.generate_cas(\*\*params) -> CasGeneratorGenerateCasResponse
diff --git a/pyproject.toml b/pyproject.toml
index 33ccf0d..d00318b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,30 +1,32 @@
[project]
name = "cas-parser-python"
-version = "1.1.0"
+version = "1.2.0"
description = "The official Python library for the CAS Parser API"
dynamic = ["readme"]
license = "Apache-2.0"
authors = [
{ name = "Cas Parser", email = "sameer@casparser.in" },
]
+
dependencies = [
- "httpx>=0.23.0, <1",
- "pydantic>=1.9.0, <3",
- "typing-extensions>=4.10, <5",
- "anyio>=3.5.0, <5",
- "distro>=1.7.0, <2",
- "sniffio",
+ "httpx>=0.23.0, <1",
+ "pydantic>=1.9.0, <3",
+ "typing-extensions>=4.10, <5",
+ "anyio>=3.5.0, <5",
+ "distro>=1.7.0, <2",
+ "sniffio",
]
-requires-python = ">= 3.8"
+
+requires-python = ">= 3.9"
classifiers = [
"Typing :: Typed",
"Intended Audience :: Developers",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Operating System :: OS Independent",
"Operating System :: POSIX",
"Operating System :: MacOS",
@@ -39,14 +41,14 @@ Homepage = "https://github.com/CASParser/cas-parser-python"
Repository = "https://github.com/CASParser/cas-parser-python"
[project.optional-dependencies]
-aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"]
+aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"]
[tool.rye]
managed = true
# version pins are in requirements-dev.lock
dev-dependencies = [
"pyright==1.1.399",
- "mypy",
+ "mypy==1.17",
"respx",
"pytest",
"pytest-asyncio",
@@ -141,7 +143,7 @@ filterwarnings = [
# there are a couple of flags that are still disabled by
# default in strict mode as they are experimental and niche.
typeCheckingMode = "strict"
-pythonVersion = "3.8"
+pythonVersion = "3.9"
exclude = [
"_dev",
@@ -224,6 +226,8 @@ select = [
"B",
# remove unused imports
"F401",
+ # check for missing future annotations
+ "FA102",
# bare except statements
"E722",
# unused arguments
@@ -246,6 +250,8 @@ unfixable = [
"T203",
]
+extend-safe-fixes = ["FA102"]
+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
"functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead"
diff --git a/requirements-dev.lock b/requirements-dev.lock
index d000467..1a3f9c1 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -12,40 +12,45 @@
-e file:.
aiohappyeyeballs==2.6.1
# via aiohttp
-aiohttp==3.12.8
+aiohttp==3.13.2
# via cas-parser-python
# via httpx-aiohttp
-aiosignal==1.3.2
+aiosignal==1.4.0
# via aiohttp
-annotated-types==0.6.0
+annotated-types==0.7.0
# via pydantic
-anyio==4.4.0
+anyio==4.12.0
# via cas-parser-python
# via httpx
-argcomplete==3.1.2
+argcomplete==3.6.3
# via nox
async-timeout==5.0.1
# via aiohttp
-attrs==25.3.0
+attrs==25.4.0
# via aiohttp
-certifi==2023.7.22
+ # via nox
+backports-asyncio-runner==1.2.0
+ # via pytest-asyncio
+certifi==2025.11.12
# via httpcore
# via httpx
-colorlog==6.7.0
+colorlog==6.10.1
+ # via nox
+dependency-groups==1.3.1
# via nox
-dirty-equals==0.6.0
-distlib==0.3.7
+dirty-equals==0.11
+distlib==0.4.0
# via virtualenv
-distro==1.8.0
+distro==1.9.0
# via cas-parser-python
-exceptiongroup==1.2.2
+exceptiongroup==1.3.1
# via anyio
# via pytest
-execnet==2.1.1
+execnet==2.1.2
# via pytest-xdist
-filelock==3.12.4
+filelock==3.19.1
# via virtualenv
-frozenlist==1.6.2
+frozenlist==1.8.0
# via aiohttp
# via aiosignal
h11==0.16.0
@@ -56,79 +61,89 @@ httpx==0.28.1
# via cas-parser-python
# via httpx-aiohttp
# via respx
-httpx-aiohttp==0.1.8
+httpx-aiohttp==0.1.9
# via cas-parser-python
-idna==3.4
+humanize==4.13.0
+ # via nox
+idna==3.11
# via anyio
# via httpx
# via yarl
-importlib-metadata==7.0.0
-iniconfig==2.0.0
+importlib-metadata==8.7.0
+iniconfig==2.1.0
# via pytest
markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
-multidict==6.4.4
+multidict==6.7.0
# via aiohttp
# via yarl
-mypy==1.14.1
-mypy-extensions==1.0.0
+mypy==1.17.0
+mypy-extensions==1.1.0
# via mypy
-nodeenv==1.8.0
+nodeenv==1.9.1
# via pyright
-nox==2023.4.22
-packaging==23.2
+nox==2025.11.12
+packaging==25.0
+ # via dependency-groups
# via nox
# via pytest
-platformdirs==3.11.0
+pathspec==0.12.1
+ # via mypy
+platformdirs==4.4.0
# via virtualenv
-pluggy==1.5.0
+pluggy==1.6.0
# via pytest
-propcache==0.3.1
+propcache==0.4.1
# via aiohttp
# via yarl
-pydantic==2.10.3
+pydantic==2.12.5
# via cas-parser-python
-pydantic-core==2.27.1
+pydantic-core==2.41.5
# via pydantic
-pygments==2.18.0
+pygments==2.19.2
+ # via pytest
# via rich
pyright==1.1.399
-pytest==8.3.3
+pytest==8.4.2
# via pytest-asyncio
# via pytest-xdist
-pytest-asyncio==0.24.0
-pytest-xdist==3.7.0
-python-dateutil==2.8.2
+pytest-asyncio==1.2.0
+pytest-xdist==3.8.0
+python-dateutil==2.9.0.post0
# via time-machine
-pytz==2023.3.post1
- # via dirty-equals
respx==0.22.0
-rich==13.7.1
-ruff==0.9.4
-setuptools==68.2.2
- # via nodeenv
-six==1.16.0
+rich==14.2.0
+ruff==0.14.7
+six==1.17.0
# via python-dateutil
-sniffio==1.3.0
- # via anyio
+sniffio==1.3.1
# via cas-parser-python
-time-machine==2.9.0
-tomli==2.0.2
+time-machine==2.19.0
+tomli==2.3.0
+ # via dependency-groups
# via mypy
+ # via nox
# via pytest
-typing-extensions==4.12.2
+typing-extensions==4.15.0
+ # via aiosignal
# via anyio
# via cas-parser-python
+ # via exceptiongroup
# via multidict
# via mypy
# via pydantic
# via pydantic-core
# via pyright
-virtualenv==20.24.5
+ # via pytest-asyncio
+ # via typing-inspection
+ # via virtualenv
+typing-inspection==0.4.2
+ # via pydantic
+virtualenv==20.35.4
# via nox
-yarl==1.20.0
+yarl==1.22.0
# via aiohttp
-zipp==3.17.0
+zipp==3.23.0
# via importlib-metadata
diff --git a/requirements.lock b/requirements.lock
index 46d36df..4fdd1ca 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -12,28 +12,28 @@
-e file:.
aiohappyeyeballs==2.6.1
# via aiohttp
-aiohttp==3.12.8
+aiohttp==3.13.2
# via cas-parser-python
# via httpx-aiohttp
-aiosignal==1.3.2
+aiosignal==1.4.0
# via aiohttp
-annotated-types==0.6.0
+annotated-types==0.7.0
# via pydantic
-anyio==4.4.0
+anyio==4.12.0
# via cas-parser-python
# via httpx
async-timeout==5.0.1
# via aiohttp
-attrs==25.3.0
+attrs==25.4.0
# via aiohttp
-certifi==2023.7.22
+certifi==2025.11.12
# via httpcore
# via httpx
-distro==1.8.0
+distro==1.9.0
# via cas-parser-python
-exceptiongroup==1.2.2
+exceptiongroup==1.3.1
# via anyio
-frozenlist==1.6.2
+frozenlist==1.8.0
# via aiohttp
# via aiosignal
h11==0.16.0
@@ -43,30 +43,34 @@ httpcore==1.0.9
httpx==0.28.1
# via cas-parser-python
# via httpx-aiohttp
-httpx-aiohttp==0.1.8
+httpx-aiohttp==0.1.9
# via cas-parser-python
-idna==3.4
+idna==3.11
# via anyio
# via httpx
# via yarl
-multidict==6.4.4
+multidict==6.7.0
# via aiohttp
# via yarl
-propcache==0.3.1
+propcache==0.4.1
# via aiohttp
# via yarl
-pydantic==2.10.3
+pydantic==2.12.5
# via cas-parser-python
-pydantic-core==2.27.1
+pydantic-core==2.41.5
# via pydantic
-sniffio==1.3.0
- # via anyio
+sniffio==1.3.1
# via cas-parser-python
-typing-extensions==4.12.2
+typing-extensions==4.15.0
+ # via aiosignal
# via anyio
# via cas-parser-python
+ # via exceptiongroup
# via multidict
# via pydantic
# via pydantic-core
-yarl==1.20.0
+ # via typing-inspection
+typing-inspection==0.4.2
+ # via pydantic
+yarl==1.22.0
# via aiohttp
diff --git a/scripts/bootstrap b/scripts/bootstrap
index e84fe62..b430fee 100755
--- a/scripts/bootstrap
+++ b/scripts/bootstrap
@@ -4,10 +4,18 @@ set -e
cd "$(dirname "$0")/.."
-if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then
+if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then
brew bundle check >/dev/null 2>&1 || {
- echo "==> Installing Homebrew dependencies…"
- brew bundle
+ echo -n "==> Install Homebrew dependencies? (y/N): "
+ read -r response
+ case "$response" in
+ [yY][eE][sS]|[yY])
+ brew bundle
+ ;;
+ *)
+ ;;
+ esac
+ echo
}
fi
diff --git a/scripts/lint b/scripts/lint
index d325f0b..e1bf7a7 100755
--- a/scripts/lint
+++ b/scripts/lint
@@ -4,8 +4,13 @@ set -e
cd "$(dirname "$0")/.."
-echo "==> Running lints"
-rye run lint
+if [ "$1" = "--fix" ]; then
+ echo "==> Running lints with --fix"
+ rye run fix:ruff
+else
+ echo "==> Running lints"
+ rye run lint
+fi
echo "==> Making sure it imports"
rye run python -c 'import cas_parser'
diff --git a/src/cas_parser/__init__.py b/src/cas_parser/__init__.py
index a6c342f..1e1d246 100644
--- a/src/cas_parser/__init__.py
+++ b/src/cas_parser/__init__.py
@@ -3,7 +3,7 @@
import typing as _t
from . import types
-from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes
+from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given
from ._utils import file_from_path
from ._client import (
Client,
@@ -48,7 +48,9 @@
"ProxiesTypes",
"NotGiven",
"NOT_GIVEN",
+ "not_given",
"Omit",
+ "omit",
"CasParserError",
"APIError",
"APIStatusError",
diff --git a/src/cas_parser/_base_client.py b/src/cas_parser/_base_client.py
index 8a47ab7..da01b6f 100644
--- a/src/cas_parser/_base_client.py
+++ b/src/cas_parser/_base_client.py
@@ -9,6 +9,7 @@
import inspect
import logging
import platform
+import warnings
import email.utils
from types import TracebackType
from random import random
@@ -42,7 +43,6 @@
from ._qs import Querystring
from ._files import to_httpx_files, async_to_httpx_files
from ._types import (
- NOT_GIVEN,
Body,
Omit,
Query,
@@ -52,11 +52,14 @@
ResponseT,
AnyMapping,
PostParser,
+ BinaryTypes,
RequestFiles,
HttpxSendArgs,
RequestOptions,
+ AsyncBinaryTypes,
HttpxRequestFiles,
ModelBuilderProtocol,
+ not_given,
)
from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
from ._compat import PYDANTIC_V1, model_copy, model_dump
@@ -145,9 +148,9 @@ def __init__(
def __init__(
self,
*,
- url: URL | NotGiven = NOT_GIVEN,
- json: Body | NotGiven = NOT_GIVEN,
- params: Query | NotGiven = NOT_GIVEN,
+ url: URL | NotGiven = not_given,
+ json: Body | NotGiven = not_given,
+ params: Query | NotGiven = not_given,
) -> None:
self.url = url
self.json = json
@@ -477,8 +480,19 @@ def _build_request(
retries_taken: int = 0,
) -> httpx.Request:
if log.isEnabledFor(logging.DEBUG):
- log.debug("Request options: %s", model_dump(options, exclude_unset=True))
-
+ log.debug(
+ "Request options: %s",
+ model_dump(
+ options,
+ exclude_unset=True,
+ # Pydantic v1 can't dump every type we support in content, so we exclude it for now.
+ exclude={
+ "content",
+ }
+ if PYDANTIC_V1
+ else {},
+ ),
+ )
kwargs: dict[str, Any] = {}
json_data = options.json_data
@@ -532,7 +546,13 @@ def _build_request(
is_body_allowed = options.method.lower() != "get"
if is_body_allowed:
- if isinstance(json_data, bytes):
+ if options.content is not None and json_data is not None:
+ raise TypeError("Passing both `content` and `json_data` is not supported")
+ if options.content is not None and files is not None:
+ raise TypeError("Passing both `content` and `files` is not supported")
+ if options.content is not None:
+ kwargs["content"] = options.content
+ elif isinstance(json_data, bytes):
kwargs["content"] = json_data
else:
kwargs["json"] = json_data if is_given(json_data) else None
@@ -595,7 +615,7 @@ def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalReques
# we internally support defining a temporary header to override the
# default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response`
# see _response.py for implementation details
- override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, NOT_GIVEN)
+ override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, not_given)
if is_given(override_cast_to):
options.headers = headers
return cast(Type[ResponseT], override_cast_to)
@@ -825,7 +845,7 @@ def __init__(
version: str,
base_url: str | URL,
max_retries: int = DEFAULT_MAX_RETRIES,
- timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.Client | None = None,
custom_headers: Mapping[str, str] | None = None,
custom_query: Mapping[str, object] | None = None,
@@ -1194,6 +1214,7 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: Literal[False] = False,
@@ -1206,6 +1227,7 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: Literal[True],
@@ -1219,6 +1241,7 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: bool,
@@ -1231,13 +1254,25 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: bool = False,
stream_cls: type[_StreamT] | None = None,
) -> ResponseT | _StreamT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="post", url=path, json_data=body, files=to_httpx_files(files), **options
+ method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options
)
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
@@ -1247,9 +1282,24 @@ def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options)
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ opts = FinalRequestOptions.construct(
+ method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options
+ )
return self.request(cast_to, opts)
def put(
@@ -1258,11 +1308,23 @@ def put(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="put", url=path, json_data=body, files=to_httpx_files(files), **options
+ method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options
)
return self.request(cast_to, opts)
@@ -1272,9 +1334,19 @@ def delete(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options)
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options)
return self.request(cast_to, opts)
def get_api_list(
@@ -1356,7 +1428,7 @@ def __init__(
base_url: str | URL,
_strict_response_validation: bool,
max_retries: int = DEFAULT_MAX_RETRIES,
- timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.AsyncClient | None = None,
custom_headers: Mapping[str, str] | None = None,
custom_query: Mapping[str, object] | None = None,
@@ -1714,6 +1786,7 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: Literal[False] = False,
@@ -1726,6 +1799,7 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: Literal[True],
@@ -1739,6 +1813,7 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: bool,
@@ -1751,13 +1826,25 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: bool = False,
stream_cls: type[_AsyncStreamT] | None = None,
) -> ResponseT | _AsyncStreamT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options
+ method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options
)
return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)
@@ -1767,9 +1854,29 @@ async def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options)
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ opts = FinalRequestOptions.construct(
+ method="patch",
+ url=path,
+ json_data=body,
+ content=content,
+ files=await async_to_httpx_files(files),
+ **options,
+ )
return await self.request(cast_to, opts)
async def put(
@@ -1778,11 +1885,23 @@ async def put(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options
+ method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options
)
return await self.request(cast_to, opts)
@@ -1792,9 +1911,19 @@ async def delete(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options)
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options)
return await self.request(cast_to, opts)
def get_api_list(
@@ -1818,8 +1947,8 @@ def make_request_options(
extra_query: Query | None = None,
extra_body: Body | None = None,
idempotency_key: str | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- post_parser: PostParser | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ post_parser: PostParser | NotGiven = not_given,
) -> RequestOptions:
"""Create a dict of type RequestOptions without keys of NotGiven values."""
options: RequestOptions = {}
diff --git a/src/cas_parser/_client.py b/src/cas_parser/_client.py
index 27572c6..b84a489 100644
--- a/src/cas_parser/_client.py
+++ b/src/cas_parser/_client.py
@@ -3,7 +3,7 @@
from __future__ import annotations
import os
-from typing import Any, Union, Mapping
+from typing import TYPE_CHECKING, Any, Mapping
from typing_extensions import Self, override
import httpx
@@ -11,17 +11,17 @@
from . import _exceptions
from ._qs import Querystring
from ._types import (
- NOT_GIVEN,
Omit,
Timeout,
NotGiven,
Transport,
ProxiesTypes,
RequestOptions,
+ not_given,
)
from ._utils import is_given, get_async_library
+from ._compat import cached_property
from ._version import __version__
-from .resources import cas_parser, cas_generator
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
from ._exceptions import APIStatusError, CasParserError
from ._base_client import (
@@ -30,6 +30,10 @@
AsyncAPIClient,
)
+if TYPE_CHECKING:
+ from .resources import cas_parser
+ from .resources.cas_parser import CasParserResource, AsyncCasParserResource
+
__all__ = [
"Timeout",
"Transport",
@@ -43,11 +47,6 @@
class CasParser(SyncAPIClient):
- cas_parser: cas_parser.CasParserResource
- cas_generator: cas_generator.CasGeneratorResource
- with_raw_response: CasParserWithRawResponse
- with_streaming_response: CasParserWithStreamedResponse
-
# client options
api_key: str
@@ -56,7 +55,7 @@ def __init__(
*,
api_key: str | None = None,
base_url: str | httpx.URL | None = None,
- timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN,
+ timeout: float | Timeout | None | NotGiven = not_given,
max_retries: int = DEFAULT_MAX_RETRIES,
default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
@@ -102,10 +101,19 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.cas_parser = cas_parser.CasParserResource(self)
- self.cas_generator = cas_generator.CasGeneratorResource(self)
- self.with_raw_response = CasParserWithRawResponse(self)
- self.with_streaming_response = CasParserWithStreamedResponse(self)
+ @cached_property
+ def cas_parser(self) -> CasParserResource:
+ from .resources.cas_parser import CasParserResource
+
+ return CasParserResource(self)
+
+ @cached_property
+ def with_raw_response(self) -> CasParserWithRawResponse:
+ return CasParserWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> CasParserWithStreamedResponse:
+ return CasParserWithStreamedResponse(self)
@property
@override
@@ -132,9 +140,9 @@ def copy(
*,
api_key: str | None = None,
base_url: str | httpx.URL | None = None,
- timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.Client | None = None,
- max_retries: int | NotGiven = NOT_GIVEN,
+ max_retries: int | NotGiven = not_given,
default_headers: Mapping[str, str] | None = None,
set_default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
@@ -213,11 +221,6 @@ def _make_status_error(
class AsyncCasParser(AsyncAPIClient):
- cas_parser: cas_parser.AsyncCasParserResource
- cas_generator: cas_generator.AsyncCasGeneratorResource
- with_raw_response: AsyncCasParserWithRawResponse
- with_streaming_response: AsyncCasParserWithStreamedResponse
-
# client options
api_key: str
@@ -226,7 +229,7 @@ def __init__(
*,
api_key: str | None = None,
base_url: str | httpx.URL | None = None,
- timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN,
+ timeout: float | Timeout | None | NotGiven = not_given,
max_retries: int = DEFAULT_MAX_RETRIES,
default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
@@ -272,10 +275,19 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
- self.cas_parser = cas_parser.AsyncCasParserResource(self)
- self.cas_generator = cas_generator.AsyncCasGeneratorResource(self)
- self.with_raw_response = AsyncCasParserWithRawResponse(self)
- self.with_streaming_response = AsyncCasParserWithStreamedResponse(self)
+ @cached_property
+ def cas_parser(self) -> AsyncCasParserResource:
+ from .resources.cas_parser import AsyncCasParserResource
+
+ return AsyncCasParserResource(self)
+
+ @cached_property
+ def with_raw_response(self) -> AsyncCasParserWithRawResponse:
+ return AsyncCasParserWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncCasParserWithStreamedResponse:
+ return AsyncCasParserWithStreamedResponse(self)
@property
@override
@@ -302,9 +314,9 @@ def copy(
*,
api_key: str | None = None,
base_url: str | httpx.URL | None = None,
- timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.AsyncClient | None = None,
- max_retries: int | NotGiven = NOT_GIVEN,
+ max_retries: int | NotGiven = not_given,
default_headers: Mapping[str, str] | None = None,
set_default_headers: Mapping[str, str] | None = None,
default_query: Mapping[str, object] | None = None,
@@ -383,27 +395,55 @@ def _make_status_error(
class CasParserWithRawResponse:
+ _client: CasParser
+
def __init__(self, client: CasParser) -> None:
- self.cas_parser = cas_parser.CasParserResourceWithRawResponse(client.cas_parser)
- self.cas_generator = cas_generator.CasGeneratorResourceWithRawResponse(client.cas_generator)
+ self._client = client
+
+ @cached_property
+ def cas_parser(self) -> cas_parser.CasParserResourceWithRawResponse:
+ from .resources.cas_parser import CasParserResourceWithRawResponse
+
+ return CasParserResourceWithRawResponse(self._client.cas_parser)
class AsyncCasParserWithRawResponse:
+ _client: AsyncCasParser
+
def __init__(self, client: AsyncCasParser) -> None:
- self.cas_parser = cas_parser.AsyncCasParserResourceWithRawResponse(client.cas_parser)
- self.cas_generator = cas_generator.AsyncCasGeneratorResourceWithRawResponse(client.cas_generator)
+ self._client = client
+
+ @cached_property
+ def cas_parser(self) -> cas_parser.AsyncCasParserResourceWithRawResponse:
+ from .resources.cas_parser import AsyncCasParserResourceWithRawResponse
+
+ return AsyncCasParserResourceWithRawResponse(self._client.cas_parser)
class CasParserWithStreamedResponse:
+ _client: CasParser
+
def __init__(self, client: CasParser) -> None:
- self.cas_parser = cas_parser.CasParserResourceWithStreamingResponse(client.cas_parser)
- self.cas_generator = cas_generator.CasGeneratorResourceWithStreamingResponse(client.cas_generator)
+ self._client = client
+
+ @cached_property
+ def cas_parser(self) -> cas_parser.CasParserResourceWithStreamingResponse:
+ from .resources.cas_parser import CasParserResourceWithStreamingResponse
+
+ return CasParserResourceWithStreamingResponse(self._client.cas_parser)
class AsyncCasParserWithStreamedResponse:
+ _client: AsyncCasParser
+
def __init__(self, client: AsyncCasParser) -> None:
- self.cas_parser = cas_parser.AsyncCasParserResourceWithStreamingResponse(client.cas_parser)
- self.cas_generator = cas_generator.AsyncCasGeneratorResourceWithStreamingResponse(client.cas_generator)
+ self._client = client
+
+ @cached_property
+ def cas_parser(self) -> cas_parser.AsyncCasParserResourceWithStreamingResponse:
+ from .resources.cas_parser import AsyncCasParserResourceWithStreamingResponse
+
+ return AsyncCasParserResourceWithStreamingResponse(self._client.cas_parser)
Client = CasParser
diff --git a/src/cas_parser/_models.py b/src/cas_parser/_models.py
index 3a6017e..29070e0 100644
--- a/src/cas_parser/_models.py
+++ b/src/cas_parser/_models.py
@@ -2,7 +2,21 @@
import os
import inspect
-from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
+import weakref
+from typing import (
+ IO,
+ TYPE_CHECKING,
+ Any,
+ Type,
+ Union,
+ Generic,
+ TypeVar,
+ Callable,
+ Iterable,
+ Optional,
+ AsyncIterable,
+ cast,
+)
from datetime import date, datetime
from typing_extensions import (
List,
@@ -256,13 +270,15 @@ def model_dump(
mode: Literal["json", "python"] | str = "python",
include: IncEx | None = None,
exclude: IncEx | None = None,
- by_alias: bool = False,
+ context: Any | None = None,
+ by_alias: bool | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
+ exclude_computed_fields: bool = False,
round_trip: bool = False,
warnings: bool | Literal["none", "warn", "error"] = True,
- context: dict[str, Any] | None = None,
+ fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
) -> dict[str, Any]:
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump
@@ -271,16 +287,24 @@ def model_dump(
Args:
mode: The mode in which `to_python` should run.
- If mode is 'json', the dictionary will only contain JSON serializable types.
- If mode is 'python', the dictionary may contain any Python objects.
- include: A list of fields to include in the output.
- exclude: A list of fields to exclude from the output.
+ If mode is 'json', the output will only contain JSON serializable types.
+ If mode is 'python', the output may contain non-JSON-serializable Python objects.
+ include: A set of fields to include in the output.
+ exclude: A set of fields to exclude from the output.
+ context: Additional context to pass to the serializer.
by_alias: Whether to use the field's alias in the dictionary key if defined.
- exclude_unset: Whether to exclude fields that are unset or None from the output.
- exclude_defaults: Whether to exclude fields that are set to their default value from the output.
- exclude_none: Whether to exclude fields that have a value of `None` from the output.
- round_trip: Whether to enable serialization and deserialization round-trip support.
- warnings: Whether to log warnings when invalid fields are encountered.
+ exclude_unset: Whether to exclude fields that have not been explicitly set.
+ exclude_defaults: Whether to exclude fields that are set to their default value.
+ exclude_none: Whether to exclude fields that have a value of `None`.
+ exclude_computed_fields: Whether to exclude computed fields.
+ While this can be useful for round-tripping, it is usually recommended to use the dedicated
+ `round_trip` parameter instead.
+ round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].
+ warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors,
+ "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].
+ fallback: A function to call when an unknown value is encountered. If not provided,
+ a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.
+ serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.
Returns:
A dictionary representation of the model.
@@ -295,10 +319,14 @@ def model_dump(
raise ValueError("context is only supported in Pydantic v2")
if serialize_as_any != False:
raise ValueError("serialize_as_any is only supported in Pydantic v2")
+ if fallback is not None:
+ raise ValueError("fallback is only supported in Pydantic v2")
+ if exclude_computed_fields != False:
+ raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
dumped = super().dict( # pyright: ignore[reportDeprecated]
include=include,
exclude=exclude,
- by_alias=by_alias,
+ by_alias=by_alias if by_alias is not None else False,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
@@ -311,15 +339,18 @@ def model_dump_json(
self,
*,
indent: int | None = None,
+ ensure_ascii: bool = False,
include: IncEx | None = None,
exclude: IncEx | None = None,
- by_alias: bool = False,
+ context: Any | None = None,
+ by_alias: bool | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
+ exclude_computed_fields: bool = False,
round_trip: bool = False,
warnings: bool | Literal["none", "warn", "error"] = True,
- context: dict[str, Any] | None = None,
+ fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
) -> str:
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json
@@ -348,11 +379,17 @@ def model_dump_json(
raise ValueError("context is only supported in Pydantic v2")
if serialize_as_any != False:
raise ValueError("serialize_as_any is only supported in Pydantic v2")
+ if fallback is not None:
+ raise ValueError("fallback is only supported in Pydantic v2")
+ if ensure_ascii != False:
+ raise ValueError("ensure_ascii is only supported in Pydantic v2")
+ if exclude_computed_fields != False:
+ raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
return super().json( # type: ignore[reportDeprecated]
indent=indent,
include=include,
exclude=exclude,
- by_alias=by_alias,
+ by_alias=by_alias if by_alias is not None else False,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
@@ -567,6 +604,9 @@ class CachedDiscriminatorType(Protocol):
__discriminator__: DiscriminatorDetails
+DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary()
+
+
class DiscriminatorDetails:
field_name: str
"""The name of the discriminator field in the variant class, e.g.
@@ -609,8 +649,9 @@ def __init__(
def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None:
- if isinstance(union, CachedDiscriminatorType):
- return union.__discriminator__
+ cached = DISCRIMINATOR_CACHE.get(union)
+ if cached is not None:
+ return cached
discriminator_field_name: str | None = None
@@ -663,7 +704,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
discriminator_field=discriminator_field_name,
discriminator_alias=discriminator_alias,
)
- cast(CachedDiscriminatorType, union).__discriminator__ = details
+ DISCRIMINATOR_CACHE.setdefault(union, details)
return details
@@ -759,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
timeout: float | Timeout | None
files: HttpxRequestFiles | None
idempotency_key: str
+ content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None]
json_data: Body
extra_json: AnyMapping
follow_redirects: bool
@@ -777,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel):
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
follow_redirects: Union[bool, None] = None
+ content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None
# It should be noted that we cannot use `json` here as that would override
# a BaseModel method in an incompatible fashion.
json_data: Union[Body, None] = None
diff --git a/src/cas_parser/_qs.py b/src/cas_parser/_qs.py
index 274320c..ada6fd3 100644
--- a/src/cas_parser/_qs.py
+++ b/src/cas_parser/_qs.py
@@ -4,7 +4,7 @@
from urllib.parse import parse_qs, urlencode
from typing_extensions import Literal, get_args
-from ._types import NOT_GIVEN, NotGiven, NotGivenOr
+from ._types import NotGiven, not_given
from ._utils import flatten
_T = TypeVar("_T")
@@ -41,8 +41,8 @@ def stringify(
self,
params: Params,
*,
- array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN,
- nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN,
+ array_format: ArrayFormat | NotGiven = not_given,
+ nested_format: NestedFormat | NotGiven = not_given,
) -> str:
return urlencode(
self.stringify_items(
@@ -56,8 +56,8 @@ def stringify_items(
self,
params: Params,
*,
- array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN,
- nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN,
+ array_format: ArrayFormat | NotGiven = not_given,
+ nested_format: NestedFormat | NotGiven = not_given,
) -> list[tuple[str, str]]:
opts = Options(
qs=self,
@@ -143,8 +143,8 @@ def __init__(
self,
qs: Querystring = _qs,
*,
- array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN,
- nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN,
+ array_format: ArrayFormat | NotGiven = not_given,
+ nested_format: NestedFormat | NotGiven = not_given,
) -> None:
self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format
self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format
diff --git a/src/cas_parser/_streaming.py b/src/cas_parser/_streaming.py
index 9c9eb3e..00e2105 100644
--- a/src/cas_parser/_streaming.py
+++ b/src/cas_parser/_streaming.py
@@ -54,12 +54,12 @@ def __stream__(self) -> Iterator[_T]:
process_data = self._client._process_response_data
iterator = self._iter_events()
- for sse in iterator:
- yield process_data(data=sse.json(), cast_to=cast_to, response=response)
-
- # Ensure the entire stream is consumed
- for _sse in iterator:
- ...
+ try:
+ for sse in iterator:
+ yield process_data(data=sse.json(), cast_to=cast_to, response=response)
+ finally:
+ # Ensure the response is closed even if the consumer doesn't read all data
+ response.close()
def __enter__(self) -> Self:
return self
@@ -118,12 +118,12 @@ async def __stream__(self) -> AsyncIterator[_T]:
process_data = self._client._process_response_data
iterator = self._iter_events()
- async for sse in iterator:
- yield process_data(data=sse.json(), cast_to=cast_to, response=response)
-
- # Ensure the entire stream is consumed
- async for _sse in iterator:
- ...
+ try:
+ async for sse in iterator:
+ yield process_data(data=sse.json(), cast_to=cast_to, response=response)
+ finally:
+ # Ensure the response is closed even if the consumer doesn't read all data
+ await response.aclose()
async def __aenter__(self) -> Self:
return self
diff --git a/src/cas_parser/_types.py b/src/cas_parser/_types.py
index 920e967..3f7802c 100644
--- a/src/cas_parser/_types.py
+++ b/src/cas_parser/_types.py
@@ -13,9 +13,11 @@
Mapping,
TypeVar,
Callable,
+ Iterable,
Iterator,
Optional,
Sequence,
+ AsyncIterable,
)
from typing_extensions import (
Set,
@@ -56,6 +58,13 @@
else:
Base64FileInput = Union[IO[bytes], PathLike]
FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8.
+
+
+# Used for sending raw binary data / streaming data in request bodies
+# e.g. for file uploads without multipart encoding
+BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]]
+AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]]
+
FileTypes = Union[
# file (or bytes)
FileContent,
@@ -117,18 +126,21 @@ class RequestOptions(TypedDict, total=False):
# Sentinel class used until PEP 0661 is accepted
class NotGiven:
"""
- A sentinel singleton class used to distinguish omitted keyword arguments
- from those passed in with the value None (which may have different behavior).
+ For parameters with a meaningful None value, we need to distinguish between
+ the user explicitly passing None, and the user not passing the parameter at
+ all.
+
+ User code shouldn't need to use not_given directly.
For example:
```py
- def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ...
+ def create(timeout: Timeout | None | NotGiven = not_given): ...
- get(timeout=1) # 1s timeout
- get(timeout=None) # No timeout
- get() # Default timeout behavior, which may not be statically known at the method definition.
+ create(timeout=1) # 1s timeout
+ create(timeout=None) # No timeout
+ create() # Default timeout behavior
```
"""
@@ -140,13 +152,14 @@ def __repr__(self) -> str:
return "NOT_GIVEN"
-NotGivenOr = Union[_T, NotGiven]
+not_given = NotGiven()
+# for backwards compatibility:
NOT_GIVEN = NotGiven()
class Omit:
- """In certain situations you need to be able to represent a case where a default value has
- to be explicitly removed and `None` is not an appropriate substitute, for example:
+ """
+ To explicitly omit something from being sent in a request, use `omit`.
```py
# as the default `Content-Type` header is `application/json` that will be sent
@@ -156,8 +169,8 @@ class Omit:
# to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983'
client.post(..., headers={"Content-Type": "multipart/form-data"})
- # instead you can remove the default `application/json` header by passing Omit
- client.post(..., headers={"Content-Type": Omit()})
+ # instead you can remove the default `application/json` header by passing omit
+ client.post(..., headers={"Content-Type": omit})
```
"""
@@ -165,6 +178,9 @@ def __bool__(self) -> Literal[False]:
return False
+omit = Omit()
+
+
@runtime_checkable
class ModelBuilderProtocol(Protocol):
@classmethod
@@ -236,6 +252,9 @@ class HttpxSendArgs(TypedDict, total=False):
if TYPE_CHECKING:
# This works because str.__contains__ does not accept object (either in typeshed or at runtime)
# https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285
+ #
+ # Note: index() and count() methods are intentionally omitted to allow pyright to properly
+ # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr.
class SequenceNotStr(Protocol[_T_co]):
@overload
def __getitem__(self, index: SupportsIndex, /) -> _T_co: ...
@@ -244,8 +263,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ...
def __contains__(self, value: object, /) -> bool: ...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[_T_co]: ...
- def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ...
- def count(self, value: Any, /) -> int: ...
def __reversed__(self) -> Iterator[_T_co]: ...
else:
# just point this to a normal `Sequence` at runtime to avoid having to special case
diff --git a/src/cas_parser/_utils/_sync.py b/src/cas_parser/_utils/_sync.py
index ad7ec71..f6027c1 100644
--- a/src/cas_parser/_utils/_sync.py
+++ b/src/cas_parser/_utils/_sync.py
@@ -1,10 +1,8 @@
from __future__ import annotations
-import sys
import asyncio
import functools
-import contextvars
-from typing import Any, TypeVar, Callable, Awaitable
+from typing import TypeVar, Callable, Awaitable
from typing_extensions import ParamSpec
import anyio
@@ -15,34 +13,11 @@
T_ParamSpec = ParamSpec("T_ParamSpec")
-if sys.version_info >= (3, 9):
- _asyncio_to_thread = asyncio.to_thread
-else:
- # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
- # for Python 3.8 support
- async def _asyncio_to_thread(
- func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
- ) -> Any:
- """Asynchronously run function *func* in a separate thread.
-
- Any *args and **kwargs supplied for this function are directly passed
- to *func*. Also, the current :class:`contextvars.Context` is propagated,
- allowing context variables from the main thread to be accessed in the
- separate thread.
-
- Returns a coroutine that can be awaited to get the eventual result of *func*.
- """
- loop = asyncio.events.get_running_loop()
- ctx = contextvars.copy_context()
- func_call = functools.partial(ctx.run, func, *args, **kwargs)
- return await loop.run_in_executor(None, func_call)
-
-
async def to_thread(
func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
) -> T_Retval:
if sniffio.current_async_library() == "asyncio":
- return await _asyncio_to_thread(func, *args, **kwargs)
+ return await asyncio.to_thread(func, *args, **kwargs)
return await anyio.to_thread.run_sync(
functools.partial(func, *args, **kwargs),
@@ -53,10 +28,7 @@ async def to_thread(
def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
"""
Take a blocking function and create an async one that receives the same
- positional and keyword arguments. For python version 3.9 and above, it uses
- asyncio.to_thread to run the function in a separate thread. For python version
- 3.8, it uses locally defined copy of the asyncio.to_thread function which was
- introduced in python 3.9.
+ positional and keyword arguments.
Usage:
diff --git a/src/cas_parser/_utils/_transform.py b/src/cas_parser/_utils/_transform.py
index c19124f..5207549 100644
--- a/src/cas_parser/_utils/_transform.py
+++ b/src/cas_parser/_utils/_transform.py
@@ -268,7 +268,7 @@ def _transform_typeddict(
annotations = get_type_hints(expected_type, include_extras=True)
for key, value in data.items():
if not is_given(value):
- # we don't need to include `NotGiven` values here as they'll
+ # we don't need to include omitted values here as they'll
# be stripped out before the request is sent anyway
continue
@@ -434,7 +434,7 @@ async def _async_transform_typeddict(
annotations = get_type_hints(expected_type, include_extras=True)
for key, value in data.items():
if not is_given(value):
- # we don't need to include `NotGiven` values here as they'll
+ # we don't need to include omitted values here as they'll
# be stripped out before the request is sent anyway
continue
diff --git a/src/cas_parser/_utils/_utils.py b/src/cas_parser/_utils/_utils.py
index f081859..eec7f4a 100644
--- a/src/cas_parser/_utils/_utils.py
+++ b/src/cas_parser/_utils/_utils.py
@@ -21,7 +21,7 @@
import sniffio
-from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike
+from .._types import Omit, NotGiven, FileTypes, HeadersLike
_T = TypeVar("_T")
_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...])
@@ -63,7 +63,7 @@ def _extract_items(
try:
key = path[index]
except IndexError:
- if isinstance(obj, NotGiven):
+ if not is_given(obj):
# no value was provided - we can safely ignore
return []
@@ -126,14 +126,14 @@ def _extract_items(
return []
-def is_given(obj: NotGivenOr[_T]) -> TypeGuard[_T]:
- return not isinstance(obj, NotGiven)
+def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]:
+ return not isinstance(obj, NotGiven) and not isinstance(obj, Omit)
# Type safe methods for narrowing types with TypeVars.
# The default narrowing for isinstance(obj, dict) is dict[unknown, unknown],
# however this cause Pyright to rightfully report errors. As we know we don't
-# care about the contained types we can safely use `object` in it's place.
+# care about the contained types we can safely use `object` in its place.
#
# There are two separate functions defined, `is_*` and `is_*_t` for different use cases.
# `is_*` is for when you're dealing with an unknown input
diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py
index 69821a2..6ae1318 100644
--- a/src/cas_parser/_version.py
+++ b/src/cas_parser/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "cas_parser"
-__version__ = "1.1.0" # x-release-please-version
+__version__ = "1.2.0" # x-release-please-version
diff --git a/src/cas_parser/resources/__init__.py b/src/cas_parser/resources/__init__.py
index f1bb2bf..5da0162 100644
--- a/src/cas_parser/resources/__init__.py
+++ b/src/cas_parser/resources/__init__.py
@@ -8,14 +8,6 @@
CasParserResourceWithStreamingResponse,
AsyncCasParserResourceWithStreamingResponse,
)
-from .cas_generator import (
- CasGeneratorResource,
- AsyncCasGeneratorResource,
- CasGeneratorResourceWithRawResponse,
- AsyncCasGeneratorResourceWithRawResponse,
- CasGeneratorResourceWithStreamingResponse,
- AsyncCasGeneratorResourceWithStreamingResponse,
-)
__all__ = [
"CasParserResource",
@@ -24,10 +16,4 @@
"AsyncCasParserResourceWithRawResponse",
"CasParserResourceWithStreamingResponse",
"AsyncCasParserResourceWithStreamingResponse",
- "CasGeneratorResource",
- "AsyncCasGeneratorResource",
- "CasGeneratorResourceWithRawResponse",
- "AsyncCasGeneratorResourceWithRawResponse",
- "CasGeneratorResourceWithStreamingResponse",
- "AsyncCasGeneratorResourceWithStreamingResponse",
]
diff --git a/src/cas_parser/resources/cas_generator.py b/src/cas_parser/resources/cas_generator.py
deleted file mode 100644
index 511b893..0000000
--- a/src/cas_parser/resources/cas_generator.py
+++ /dev/null
@@ -1,225 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing_extensions import Literal
-
-import httpx
-
-from ..types import cas_generator_generate_cas_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.cas_generator_generate_cas_response import CasGeneratorGenerateCasResponse
-
-__all__ = ["CasGeneratorResource", "AsyncCasGeneratorResource"]
-
-
-class CasGeneratorResource(SyncAPIResource):
- @cached_property
- def with_raw_response(self) -> CasGeneratorResourceWithRawResponse:
- """
- 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/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers
- """
- return CasGeneratorResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> CasGeneratorResourceWithStreamingResponse:
- """
- An alternative to `.with_raw_response` that doesn't eagerly read the response body.
-
- For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response
- """
- return CasGeneratorResourceWithStreamingResponse(self)
-
- def generate_cas(
- self,
- *,
- email: str,
- from_date: str,
- password: str,
- to_date: str,
- cas_authority: Literal["kfintech", "cams", "cdsl", "nsdl"] | NotGiven = NOT_GIVEN,
- pan_no: str | NotGiven = NOT_GIVEN,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- ) -> CasGeneratorGenerateCasResponse:
- """
- This endpoint generates CAS (Consolidated Account Statement) documents by
- submitting a mailback request to the specified CAS authority. Currently only
- supports KFintech, with plans to support CAMS, CDSL, and NSDL in the future.
-
- Args:
- email: Email address to receive the CAS document
-
- from_date: Start date for the CAS period (format YYYY-MM-DD)
-
- password: Password to protect the generated CAS PDF
-
- to_date: End date for the CAS period (format YYYY-MM-DD)
-
- cas_authority: CAS authority to generate the document from (currently only kfintech is
- supported)
-
- pan_no: PAN number (optional for some CAS authorities)
-
- 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(
- "/v4/generate",
- body=maybe_transform(
- {
- "email": email,
- "from_date": from_date,
- "password": password,
- "to_date": to_date,
- "cas_authority": cas_authority,
- "pan_no": pan_no,
- },
- cas_generator_generate_cas_params.CasGeneratorGenerateCasParams,
- ),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=CasGeneratorGenerateCasResponse,
- )
-
-
-class AsyncCasGeneratorResource(AsyncAPIResource):
- @cached_property
- def with_raw_response(self) -> AsyncCasGeneratorResourceWithRawResponse:
- """
- 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/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers
- """
- return AsyncCasGeneratorResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> AsyncCasGeneratorResourceWithStreamingResponse:
- """
- An alternative to `.with_raw_response` that doesn't eagerly read the response body.
-
- For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response
- """
- return AsyncCasGeneratorResourceWithStreamingResponse(self)
-
- async def generate_cas(
- self,
- *,
- email: str,
- from_date: str,
- password: str,
- to_date: str,
- cas_authority: Literal["kfintech", "cams", "cdsl", "nsdl"] | NotGiven = NOT_GIVEN,
- pan_no: str | NotGiven = NOT_GIVEN,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
- ) -> CasGeneratorGenerateCasResponse:
- """
- This endpoint generates CAS (Consolidated Account Statement) documents by
- submitting a mailback request to the specified CAS authority. Currently only
- supports KFintech, with plans to support CAMS, CDSL, and NSDL in the future.
-
- Args:
- email: Email address to receive the CAS document
-
- from_date: Start date for the CAS period (format YYYY-MM-DD)
-
- password: Password to protect the generated CAS PDF
-
- to_date: End date for the CAS period (format YYYY-MM-DD)
-
- cas_authority: CAS authority to generate the document from (currently only kfintech is
- supported)
-
- pan_no: PAN number (optional for some CAS authorities)
-
- 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(
- "/v4/generate",
- body=await async_maybe_transform(
- {
- "email": email,
- "from_date": from_date,
- "password": password,
- "to_date": to_date,
- "cas_authority": cas_authority,
- "pan_no": pan_no,
- },
- cas_generator_generate_cas_params.CasGeneratorGenerateCasParams,
- ),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=CasGeneratorGenerateCasResponse,
- )
-
-
-class CasGeneratorResourceWithRawResponse:
- def __init__(self, cas_generator: CasGeneratorResource) -> None:
- self._cas_generator = cas_generator
-
- self.generate_cas = to_raw_response_wrapper(
- cas_generator.generate_cas,
- )
-
-
-class AsyncCasGeneratorResourceWithRawResponse:
- def __init__(self, cas_generator: AsyncCasGeneratorResource) -> None:
- self._cas_generator = cas_generator
-
- self.generate_cas = async_to_raw_response_wrapper(
- cas_generator.generate_cas,
- )
-
-
-class CasGeneratorResourceWithStreamingResponse:
- def __init__(self, cas_generator: CasGeneratorResource) -> None:
- self._cas_generator = cas_generator
-
- self.generate_cas = to_streamed_response_wrapper(
- cas_generator.generate_cas,
- )
-
-
-class AsyncCasGeneratorResourceWithStreamingResponse:
- def __init__(self, cas_generator: AsyncCasGeneratorResource) -> None:
- self._cas_generator = cas_generator
-
- self.generate_cas = async_to_streamed_response_wrapper(
- cas_generator.generate_cas,
- )
diff --git a/src/cas_parser/resources/cas_parser.py b/src/cas_parser/resources/cas_parser.py
index a64b7dd..e82b0e9 100644
--- a/src/cas_parser/resources/cas_parser.py
+++ b/src/cas_parser/resources/cas_parser.py
@@ -12,7 +12,7 @@
cas_parser_smart_parse_params,
cas_parser_cams_kfintech_params,
)
-from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven
+from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
from .._compat import cached_property
from .._resource import SyncAPIResource, AsyncAPIResource
@@ -51,15 +51,15 @@ def with_streaming_response(self) -> CasParserResourceWithStreamingResponse:
def cams_kfintech(
self,
*,
- password: str | NotGiven = NOT_GIVEN,
- pdf_file: str | NotGiven = NOT_GIVEN,
- pdf_url: str | NotGiven = NOT_GIVEN,
+ password: str | Omit = omit,
+ pdf_file: str | Omit = omit,
+ pdf_url: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> UnifiedResponse:
"""
This endpoint specifically parses CAMS/KFintech CAS (Consolidated Account
@@ -107,15 +107,15 @@ def cams_kfintech(
def cdsl(
self,
*,
- password: str | NotGiven = NOT_GIVEN,
- pdf_file: str | NotGiven = NOT_GIVEN,
- pdf_url: str | NotGiven = NOT_GIVEN,
+ password: str | Omit = omit,
+ pdf_file: str | Omit = omit,
+ pdf_url: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> UnifiedResponse:
"""
This endpoint specifically parses CDSL CAS (Consolidated Account Statement) PDF
@@ -163,15 +163,15 @@ def cdsl(
def nsdl(
self,
*,
- password: str | NotGiven = NOT_GIVEN,
- pdf_file: str | NotGiven = NOT_GIVEN,
- pdf_url: str | NotGiven = NOT_GIVEN,
+ password: str | Omit = omit,
+ pdf_file: str | Omit = omit,
+ pdf_url: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> UnifiedResponse:
"""
This endpoint specifically parses NSDL CAS (Consolidated Account Statement) PDF
@@ -219,15 +219,15 @@ def nsdl(
def smart_parse(
self,
*,
- password: str | NotGiven = NOT_GIVEN,
- pdf_file: str | NotGiven = NOT_GIVEN,
- pdf_url: str | NotGiven = NOT_GIVEN,
+ password: str | Omit = omit,
+ pdf_file: str | Omit = omit,
+ pdf_url: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> UnifiedResponse:
"""
This endpoint parses CAS (Consolidated Account Statement) PDF files from NSDL,
@@ -297,15 +297,15 @@ def with_streaming_response(self) -> AsyncCasParserResourceWithStreamingResponse
async def cams_kfintech(
self,
*,
- password: str | NotGiven = NOT_GIVEN,
- pdf_file: str | NotGiven = NOT_GIVEN,
- pdf_url: str | NotGiven = NOT_GIVEN,
+ password: str | Omit = omit,
+ pdf_file: str | Omit = omit,
+ pdf_url: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> UnifiedResponse:
"""
This endpoint specifically parses CAMS/KFintech CAS (Consolidated Account
@@ -353,15 +353,15 @@ async def cams_kfintech(
async def cdsl(
self,
*,
- password: str | NotGiven = NOT_GIVEN,
- pdf_file: str | NotGiven = NOT_GIVEN,
- pdf_url: str | NotGiven = NOT_GIVEN,
+ password: str | Omit = omit,
+ pdf_file: str | Omit = omit,
+ pdf_url: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> UnifiedResponse:
"""
This endpoint specifically parses CDSL CAS (Consolidated Account Statement) PDF
@@ -409,15 +409,15 @@ async def cdsl(
async def nsdl(
self,
*,
- password: str | NotGiven = NOT_GIVEN,
- pdf_file: str | NotGiven = NOT_GIVEN,
- pdf_url: str | NotGiven = NOT_GIVEN,
+ password: str | Omit = omit,
+ pdf_file: str | Omit = omit,
+ pdf_url: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> UnifiedResponse:
"""
This endpoint specifically parses NSDL CAS (Consolidated Account Statement) PDF
@@ -465,15 +465,15 @@ async def nsdl(
async def smart_parse(
self,
*,
- password: str | NotGiven = NOT_GIVEN,
- pdf_file: str | NotGiven = NOT_GIVEN,
- pdf_url: str | NotGiven = NOT_GIVEN,
+ password: str | Omit = omit,
+ pdf_file: str | Omit = omit,
+ pdf_url: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> UnifiedResponse:
"""
This endpoint parses CAS (Consolidated Account Statement) PDF files from NSDL,
diff --git a/src/cas_parser/types/__init__.py b/src/cas_parser/types/__init__.py
index 4dbdba1..fcdbc0b 100644
--- a/src/cas_parser/types/__init__.py
+++ b/src/cas_parser/types/__init__.py
@@ -7,5 +7,3 @@
from .cas_parser_nsdl_params import CasParserNsdlParams as CasParserNsdlParams
from .cas_parser_smart_parse_params import CasParserSmartParseParams as CasParserSmartParseParams
from .cas_parser_cams_kfintech_params import CasParserCamsKfintechParams as CasParserCamsKfintechParams
-from .cas_generator_generate_cas_params import CasGeneratorGenerateCasParams as CasGeneratorGenerateCasParams
-from .cas_generator_generate_cas_response import CasGeneratorGenerateCasResponse as CasGeneratorGenerateCasResponse
diff --git a/src/cas_parser/types/cas_generator_generate_cas_params.py b/src/cas_parser/types/cas_generator_generate_cas_params.py
deleted file mode 100644
index 253dcea..0000000
--- a/src/cas_parser/types/cas_generator_generate_cas_params.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing_extensions import Literal, Required, TypedDict
-
-__all__ = ["CasGeneratorGenerateCasParams"]
-
-
-class CasGeneratorGenerateCasParams(TypedDict, total=False):
- email: Required[str]
- """Email address to receive the CAS document"""
-
- from_date: Required[str]
- """Start date for the CAS period (format YYYY-MM-DD)"""
-
- password: Required[str]
- """Password to protect the generated CAS PDF"""
-
- to_date: Required[str]
- """End date for the CAS period (format YYYY-MM-DD)"""
-
- cas_authority: Literal["kfintech", "cams", "cdsl", "nsdl"]
- """
- CAS authority to generate the document from (currently only kfintech is
- supported)
- """
-
- pan_no: str
- """PAN number (optional for some CAS authorities)"""
diff --git a/src/cas_parser/types/cas_generator_generate_cas_response.py b/src/cas_parser/types/cas_generator_generate_cas_response.py
deleted file mode 100644
index e781ef9..0000000
--- a/src/cas_parser/types/cas_generator_generate_cas_response.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import Optional
-
-from .._models import BaseModel
-
-__all__ = ["CasGeneratorGenerateCasResponse"]
-
-
-class CasGeneratorGenerateCasResponse(BaseModel):
- msg: Optional[str] = None
-
- status: Optional[str] = None
diff --git a/src/cas_parser/types/unified_response.py b/src/cas_parser/types/unified_response.py
index 7dc5439..2a8ab94 100644
--- a/src/cas_parser/types/unified_response.py
+++ b/src/cas_parser/types/unified_response.py
@@ -14,10 +14,26 @@
"DematAccountAdditionalInfo",
"DematAccountHoldings",
"DematAccountHoldingsAif",
+ "DematAccountHoldingsAifAdditionalInfo",
+ "DematAccountHoldingsAifTransaction",
+ "DematAccountHoldingsAifTransactionAdditionalInfo",
"DematAccountHoldingsCorporateBond",
+ "DematAccountHoldingsCorporateBondAdditionalInfo",
+ "DematAccountHoldingsCorporateBondTransaction",
+ "DematAccountHoldingsCorporateBondTransactionAdditionalInfo",
"DematAccountHoldingsDematMutualFund",
+ "DematAccountHoldingsDematMutualFundAdditionalInfo",
+ "DematAccountHoldingsDematMutualFundTransaction",
+ "DematAccountHoldingsDematMutualFundTransactionAdditionalInfo",
"DematAccountHoldingsEquity",
+ "DematAccountHoldingsEquityAdditionalInfo",
+ "DematAccountHoldingsEquityTransaction",
+ "DematAccountHoldingsEquityTransactionAdditionalInfo",
"DematAccountHoldingsGovernmentSecurity",
+ "DematAccountHoldingsGovernmentSecurityAdditionalInfo",
+ "DematAccountHoldingsGovernmentSecurityTransaction",
+ "DematAccountHoldingsGovernmentSecurityTransactionAdditionalInfo",
+ "DematAccountLinkedHolder",
"Insurance",
"InsuranceLifeInsurancePolicy",
"Investor",
@@ -25,19 +41,28 @@
"MetaStatementPeriod",
"MutualFund",
"MutualFundAdditionalInfo",
+ "MutualFundLinkedHolder",
"MutualFundScheme",
"MutualFundSchemeAdditionalInfo",
"MutualFundSchemeGain",
"MutualFundSchemeTransaction",
+ "MutualFundSchemeTransactionAdditionalInfo",
+ "Np",
+ "NpFund",
+ "NpFundAdditionalInfo",
+ "NpLinkedHolder",
"Summary",
"SummaryAccounts",
"SummaryAccountsDemat",
"SummaryAccountsInsurance",
"SummaryAccountsMutualFunds",
+ "SummaryAccountsNps",
]
class DematAccountAdditionalInfo(BaseModel):
+ """Additional information specific to the demat account type"""
+
bo_status: Optional[str] = None
"""Beneficiary Owner status (CDSL)"""
@@ -63,8 +88,101 @@ class DematAccountAdditionalInfo(BaseModel):
"""Account status (CDSL)"""
+class DematAccountHoldingsAifAdditionalInfo(BaseModel):
+ """Additional information specific to the AIF"""
+
+ close_units: Optional[float] = None
+ """Closing balance units for the statement period (beta)"""
+
+ open_units: Optional[float] = None
+ """Opening balance units for the statement period (beta)"""
+
+
+class DematAccountHoldingsAifTransactionAdditionalInfo(BaseModel):
+ """Additional transaction-specific fields that vary by source"""
+
+ capital_withdrawal: Optional[float] = None
+ """Capital withdrawal amount (CDSL MF transactions)"""
+
+ credit: Optional[float] = None
+ """Units credited (demat transactions)"""
+
+ debit: Optional[float] = None
+ """Units debited (demat transactions)"""
+
+ income_distribution: Optional[float] = None
+ """Income distribution amount (CDSL MF transactions)"""
+
+ order_no: Optional[str] = None
+ """Order/transaction reference number (demat transactions)"""
+
+ price: Optional[float] = None
+ """Price per unit (NSDL/CDSL MF transactions)"""
+
+ stamp_duty: Optional[float] = None
+ """Stamp duty charged"""
+
+
+class DematAccountHoldingsAifTransaction(BaseModel):
+ """
+ Unified transaction schema for all holding types (MF folios, equities, bonds, etc.)
+ """
+
+ additional_info: Optional[DematAccountHoldingsAifTransactionAdditionalInfo] = None
+ """Additional transaction-specific fields that vary by source"""
+
+ amount: Optional[float] = None
+ """Transaction amount in currency (computed from units Ă— price/NAV)"""
+
+ balance: Optional[float] = None
+ """Balance units after transaction"""
+
+ date: Optional[datetime.date] = None
+ """Transaction date (YYYY-MM-DD)"""
+
+ description: Optional[str] = None
+ """Transaction description/particulars"""
+
+ dividend_rate: Optional[float] = None
+ """Dividend rate (for DIVIDEND_PAYOUT transactions)"""
+
+ nav: Optional[float] = None
+ """NAV/price per unit on transaction date"""
+
+ type: Optional[
+ Literal[
+ "PURCHASE",
+ "PURCHASE_SIP",
+ "REDEMPTION",
+ "SWITCH_IN",
+ "SWITCH_IN_MERGER",
+ "SWITCH_OUT",
+ "SWITCH_OUT_MERGER",
+ "DIVIDEND_PAYOUT",
+ "DIVIDEND_REINVEST",
+ "SEGREGATION",
+ "STAMP_DUTY_TAX",
+ "TDS_TAX",
+ "STT_TAX",
+ "MISC",
+ "REVERSAL",
+ "UNKNOWN",
+ ]
+ ] = None
+ """Transaction type.
+
+ Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN,
+ SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT,
+ DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC,
+ REVERSAL, UNKNOWN.
+ """
+
+ units: Optional[float] = None
+ """Number of units involved in transaction"""
+
+
class DematAccountHoldingsAif(BaseModel):
- additional_info: Optional[object] = None
+ additional_info: Optional[DematAccountHoldingsAifAdditionalInfo] = None
"""Additional information specific to the AIF"""
isin: Optional[str] = None
@@ -73,6 +191,9 @@ class DematAccountHoldingsAif(BaseModel):
name: Optional[str] = None
"""Name of the AIF"""
+ transactions: Optional[List[DematAccountHoldingsAifTransaction]] = None
+ """List of transactions for this holding (beta)"""
+
units: Optional[float] = None
"""Number of units held"""
@@ -80,8 +201,101 @@ class DematAccountHoldingsAif(BaseModel):
"""Current market value of the holding"""
+class DematAccountHoldingsCorporateBondAdditionalInfo(BaseModel):
+ """Additional information specific to the corporate bond"""
+
+ close_units: Optional[float] = None
+ """Closing balance units for the statement period (beta)"""
+
+ open_units: Optional[float] = None
+ """Opening balance units for the statement period (beta)"""
+
+
+class DematAccountHoldingsCorporateBondTransactionAdditionalInfo(BaseModel):
+ """Additional transaction-specific fields that vary by source"""
+
+ capital_withdrawal: Optional[float] = None
+ """Capital withdrawal amount (CDSL MF transactions)"""
+
+ credit: Optional[float] = None
+ """Units credited (demat transactions)"""
+
+ debit: Optional[float] = None
+ """Units debited (demat transactions)"""
+
+ income_distribution: Optional[float] = None
+ """Income distribution amount (CDSL MF transactions)"""
+
+ order_no: Optional[str] = None
+ """Order/transaction reference number (demat transactions)"""
+
+ price: Optional[float] = None
+ """Price per unit (NSDL/CDSL MF transactions)"""
+
+ stamp_duty: Optional[float] = None
+ """Stamp duty charged"""
+
+
+class DematAccountHoldingsCorporateBondTransaction(BaseModel):
+ """
+ Unified transaction schema for all holding types (MF folios, equities, bonds, etc.)
+ """
+
+ additional_info: Optional[DematAccountHoldingsCorporateBondTransactionAdditionalInfo] = None
+ """Additional transaction-specific fields that vary by source"""
+
+ amount: Optional[float] = None
+ """Transaction amount in currency (computed from units Ă— price/NAV)"""
+
+ balance: Optional[float] = None
+ """Balance units after transaction"""
+
+ date: Optional[datetime.date] = None
+ """Transaction date (YYYY-MM-DD)"""
+
+ description: Optional[str] = None
+ """Transaction description/particulars"""
+
+ dividend_rate: Optional[float] = None
+ """Dividend rate (for DIVIDEND_PAYOUT transactions)"""
+
+ nav: Optional[float] = None
+ """NAV/price per unit on transaction date"""
+
+ type: Optional[
+ Literal[
+ "PURCHASE",
+ "PURCHASE_SIP",
+ "REDEMPTION",
+ "SWITCH_IN",
+ "SWITCH_IN_MERGER",
+ "SWITCH_OUT",
+ "SWITCH_OUT_MERGER",
+ "DIVIDEND_PAYOUT",
+ "DIVIDEND_REINVEST",
+ "SEGREGATION",
+ "STAMP_DUTY_TAX",
+ "TDS_TAX",
+ "STT_TAX",
+ "MISC",
+ "REVERSAL",
+ "UNKNOWN",
+ ]
+ ] = None
+ """Transaction type.
+
+ Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN,
+ SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT,
+ DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC,
+ REVERSAL, UNKNOWN.
+ """
+
+ units: Optional[float] = None
+ """Number of units involved in transaction"""
+
+
class DematAccountHoldingsCorporateBond(BaseModel):
- additional_info: Optional[object] = None
+ additional_info: Optional[DematAccountHoldingsCorporateBondAdditionalInfo] = None
"""Additional information specific to the corporate bond"""
isin: Optional[str] = None
@@ -90,6 +304,9 @@ class DematAccountHoldingsCorporateBond(BaseModel):
name: Optional[str] = None
"""Name of the corporate bond"""
+ transactions: Optional[List[DematAccountHoldingsCorporateBondTransaction]] = None
+ """List of transactions for this holding (beta)"""
+
units: Optional[float] = None
"""Number of units held"""
@@ -97,8 +314,101 @@ class DematAccountHoldingsCorporateBond(BaseModel):
"""Current market value of the holding"""
+class DematAccountHoldingsDematMutualFundAdditionalInfo(BaseModel):
+ """Additional information specific to the mutual fund"""
+
+ close_units: Optional[float] = None
+ """Closing balance units for the statement period (beta)"""
+
+ open_units: Optional[float] = None
+ """Opening balance units for the statement period (beta)"""
+
+
+class DematAccountHoldingsDematMutualFundTransactionAdditionalInfo(BaseModel):
+ """Additional transaction-specific fields that vary by source"""
+
+ capital_withdrawal: Optional[float] = None
+ """Capital withdrawal amount (CDSL MF transactions)"""
+
+ credit: Optional[float] = None
+ """Units credited (demat transactions)"""
+
+ debit: Optional[float] = None
+ """Units debited (demat transactions)"""
+
+ income_distribution: Optional[float] = None
+ """Income distribution amount (CDSL MF transactions)"""
+
+ order_no: Optional[str] = None
+ """Order/transaction reference number (demat transactions)"""
+
+ price: Optional[float] = None
+ """Price per unit (NSDL/CDSL MF transactions)"""
+
+ stamp_duty: Optional[float] = None
+ """Stamp duty charged"""
+
+
+class DematAccountHoldingsDematMutualFundTransaction(BaseModel):
+ """
+ Unified transaction schema for all holding types (MF folios, equities, bonds, etc.)
+ """
+
+ additional_info: Optional[DematAccountHoldingsDematMutualFundTransactionAdditionalInfo] = None
+ """Additional transaction-specific fields that vary by source"""
+
+ amount: Optional[float] = None
+ """Transaction amount in currency (computed from units Ă— price/NAV)"""
+
+ balance: Optional[float] = None
+ """Balance units after transaction"""
+
+ date: Optional[datetime.date] = None
+ """Transaction date (YYYY-MM-DD)"""
+
+ description: Optional[str] = None
+ """Transaction description/particulars"""
+
+ dividend_rate: Optional[float] = None
+ """Dividend rate (for DIVIDEND_PAYOUT transactions)"""
+
+ nav: Optional[float] = None
+ """NAV/price per unit on transaction date"""
+
+ type: Optional[
+ Literal[
+ "PURCHASE",
+ "PURCHASE_SIP",
+ "REDEMPTION",
+ "SWITCH_IN",
+ "SWITCH_IN_MERGER",
+ "SWITCH_OUT",
+ "SWITCH_OUT_MERGER",
+ "DIVIDEND_PAYOUT",
+ "DIVIDEND_REINVEST",
+ "SEGREGATION",
+ "STAMP_DUTY_TAX",
+ "TDS_TAX",
+ "STT_TAX",
+ "MISC",
+ "REVERSAL",
+ "UNKNOWN",
+ ]
+ ] = None
+ """Transaction type.
+
+ Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN,
+ SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT,
+ DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC,
+ REVERSAL, UNKNOWN.
+ """
+
+ units: Optional[float] = None
+ """Number of units involved in transaction"""
+
+
class DematAccountHoldingsDematMutualFund(BaseModel):
- additional_info: Optional[object] = None
+ additional_info: Optional[DematAccountHoldingsDematMutualFundAdditionalInfo] = None
"""Additional information specific to the mutual fund"""
isin: Optional[str] = None
@@ -107,6 +417,9 @@ class DematAccountHoldingsDematMutualFund(BaseModel):
name: Optional[str] = None
"""Name of the mutual fund"""
+ transactions: Optional[List[DematAccountHoldingsDematMutualFundTransaction]] = None
+ """List of transactions for this holding (beta)"""
+
units: Optional[float] = None
"""Number of units held"""
@@ -114,8 +427,101 @@ class DematAccountHoldingsDematMutualFund(BaseModel):
"""Current market value of the holding"""
+class DematAccountHoldingsEquityAdditionalInfo(BaseModel):
+ """Additional information specific to the equity"""
+
+ close_units: Optional[float] = None
+ """Closing balance units for the statement period (beta)"""
+
+ open_units: Optional[float] = None
+ """Opening balance units for the statement period (beta)"""
+
+
+class DematAccountHoldingsEquityTransactionAdditionalInfo(BaseModel):
+ """Additional transaction-specific fields that vary by source"""
+
+ capital_withdrawal: Optional[float] = None
+ """Capital withdrawal amount (CDSL MF transactions)"""
+
+ credit: Optional[float] = None
+ """Units credited (demat transactions)"""
+
+ debit: Optional[float] = None
+ """Units debited (demat transactions)"""
+
+ income_distribution: Optional[float] = None
+ """Income distribution amount (CDSL MF transactions)"""
+
+ order_no: Optional[str] = None
+ """Order/transaction reference number (demat transactions)"""
+
+ price: Optional[float] = None
+ """Price per unit (NSDL/CDSL MF transactions)"""
+
+ stamp_duty: Optional[float] = None
+ """Stamp duty charged"""
+
+
+class DematAccountHoldingsEquityTransaction(BaseModel):
+ """
+ Unified transaction schema for all holding types (MF folios, equities, bonds, etc.)
+ """
+
+ additional_info: Optional[DematAccountHoldingsEquityTransactionAdditionalInfo] = None
+ """Additional transaction-specific fields that vary by source"""
+
+ amount: Optional[float] = None
+ """Transaction amount in currency (computed from units Ă— price/NAV)"""
+
+ balance: Optional[float] = None
+ """Balance units after transaction"""
+
+ date: Optional[datetime.date] = None
+ """Transaction date (YYYY-MM-DD)"""
+
+ description: Optional[str] = None
+ """Transaction description/particulars"""
+
+ dividend_rate: Optional[float] = None
+ """Dividend rate (for DIVIDEND_PAYOUT transactions)"""
+
+ nav: Optional[float] = None
+ """NAV/price per unit on transaction date"""
+
+ type: Optional[
+ Literal[
+ "PURCHASE",
+ "PURCHASE_SIP",
+ "REDEMPTION",
+ "SWITCH_IN",
+ "SWITCH_IN_MERGER",
+ "SWITCH_OUT",
+ "SWITCH_OUT_MERGER",
+ "DIVIDEND_PAYOUT",
+ "DIVIDEND_REINVEST",
+ "SEGREGATION",
+ "STAMP_DUTY_TAX",
+ "TDS_TAX",
+ "STT_TAX",
+ "MISC",
+ "REVERSAL",
+ "UNKNOWN",
+ ]
+ ] = None
+ """Transaction type.
+
+ Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN,
+ SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT,
+ DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC,
+ REVERSAL, UNKNOWN.
+ """
+
+ units: Optional[float] = None
+ """Number of units involved in transaction"""
+
+
class DematAccountHoldingsEquity(BaseModel):
- additional_info: Optional[object] = None
+ additional_info: Optional[DematAccountHoldingsEquityAdditionalInfo] = None
"""Additional information specific to the equity"""
isin: Optional[str] = None
@@ -124,6 +530,9 @@ class DematAccountHoldingsEquity(BaseModel):
name: Optional[str] = None
"""Name of the equity"""
+ transactions: Optional[List[DematAccountHoldingsEquityTransaction]] = None
+ """List of transactions for this holding (beta)"""
+
units: Optional[float] = None
"""Number of units held"""
@@ -131,8 +540,101 @@ class DematAccountHoldingsEquity(BaseModel):
"""Current market value of the holding"""
+class DematAccountHoldingsGovernmentSecurityAdditionalInfo(BaseModel):
+ """Additional information specific to the government security"""
+
+ close_units: Optional[float] = None
+ """Closing balance units for the statement period (beta)"""
+
+ open_units: Optional[float] = None
+ """Opening balance units for the statement period (beta)"""
+
+
+class DematAccountHoldingsGovernmentSecurityTransactionAdditionalInfo(BaseModel):
+ """Additional transaction-specific fields that vary by source"""
+
+ capital_withdrawal: Optional[float] = None
+ """Capital withdrawal amount (CDSL MF transactions)"""
+
+ credit: Optional[float] = None
+ """Units credited (demat transactions)"""
+
+ debit: Optional[float] = None
+ """Units debited (demat transactions)"""
+
+ income_distribution: Optional[float] = None
+ """Income distribution amount (CDSL MF transactions)"""
+
+ order_no: Optional[str] = None
+ """Order/transaction reference number (demat transactions)"""
+
+ price: Optional[float] = None
+ """Price per unit (NSDL/CDSL MF transactions)"""
+
+ stamp_duty: Optional[float] = None
+ """Stamp duty charged"""
+
+
+class DematAccountHoldingsGovernmentSecurityTransaction(BaseModel):
+ """
+ Unified transaction schema for all holding types (MF folios, equities, bonds, etc.)
+ """
+
+ additional_info: Optional[DematAccountHoldingsGovernmentSecurityTransactionAdditionalInfo] = None
+ """Additional transaction-specific fields that vary by source"""
+
+ amount: Optional[float] = None
+ """Transaction amount in currency (computed from units Ă— price/NAV)"""
+
+ balance: Optional[float] = None
+ """Balance units after transaction"""
+
+ date: Optional[datetime.date] = None
+ """Transaction date (YYYY-MM-DD)"""
+
+ description: Optional[str] = None
+ """Transaction description/particulars"""
+
+ dividend_rate: Optional[float] = None
+ """Dividend rate (for DIVIDEND_PAYOUT transactions)"""
+
+ nav: Optional[float] = None
+ """NAV/price per unit on transaction date"""
+
+ type: Optional[
+ Literal[
+ "PURCHASE",
+ "PURCHASE_SIP",
+ "REDEMPTION",
+ "SWITCH_IN",
+ "SWITCH_IN_MERGER",
+ "SWITCH_OUT",
+ "SWITCH_OUT_MERGER",
+ "DIVIDEND_PAYOUT",
+ "DIVIDEND_REINVEST",
+ "SEGREGATION",
+ "STAMP_DUTY_TAX",
+ "TDS_TAX",
+ "STT_TAX",
+ "MISC",
+ "REVERSAL",
+ "UNKNOWN",
+ ]
+ ] = None
+ """Transaction type.
+
+ Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN,
+ SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT,
+ DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC,
+ REVERSAL, UNKNOWN.
+ """
+
+ units: Optional[float] = None
+ """Number of units involved in transaction"""
+
+
class DematAccountHoldingsGovernmentSecurity(BaseModel):
- additional_info: Optional[object] = None
+ additional_info: Optional[DematAccountHoldingsGovernmentSecurityAdditionalInfo] = None
"""Additional information specific to the government security"""
isin: Optional[str] = None
@@ -141,6 +643,9 @@ class DematAccountHoldingsGovernmentSecurity(BaseModel):
name: Optional[str] = None
"""Name of the government security"""
+ transactions: Optional[List[DematAccountHoldingsGovernmentSecurityTransaction]] = None
+ """List of transactions for this holding (beta)"""
+
units: Optional[float] = None
"""Number of units held"""
@@ -160,6 +665,14 @@ class DematAccountHoldings(BaseModel):
government_securities: Optional[List[DematAccountHoldingsGovernmentSecurity]] = None
+class DematAccountLinkedHolder(BaseModel):
+ name: Optional[str] = None
+ """Name of the account holder"""
+
+ pan: Optional[str] = None
+ """PAN of the account holder"""
+
+
class DematAccount(BaseModel):
additional_info: Optional[DematAccountAdditionalInfo] = None
"""Additional information specific to the demat account type"""
@@ -181,6 +694,9 @@ class DematAccount(BaseModel):
holdings: Optional[DematAccountHoldings] = None
+ linked_holders: Optional[List[DematAccountLinkedHolder]] = None
+ """List of account holders linked to this demat account"""
+
value: Optional[float] = None
"""Total value of the demat account"""
@@ -260,6 +776,8 @@ class Meta(BaseModel):
class MutualFundAdditionalInfo(BaseModel):
+ """Additional folio information"""
+
kyc: Optional[str] = None
"""KYC status of the folio"""
@@ -270,7 +788,17 @@ class MutualFundAdditionalInfo(BaseModel):
"""PAN KYC status"""
+class MutualFundLinkedHolder(BaseModel):
+ name: Optional[str] = None
+ """Name of the account holder"""
+
+ pan: Optional[str] = None
+ """PAN of the account holder"""
+
+
class MutualFundSchemeAdditionalInfo(BaseModel):
+ """Additional information specific to the scheme"""
+
advisor: Optional[str] = None
"""Financial advisor name (CAMS/KFintech)"""
@@ -278,10 +806,10 @@ class MutualFundSchemeAdditionalInfo(BaseModel):
"""AMFI code for the scheme (CAMS/KFintech)"""
close_units: Optional[float] = None
- """Closing balance units (CAMS/KFintech)"""
+ """Closing balance units for the statement period"""
open_units: Optional[float] = None
- """Opening balance units (CAMS/KFintech)"""
+ """Opening balance units for the statement period"""
rta_code: Optional[str] = None
"""RTA code for the scheme (CAMS/KFintech)"""
@@ -295,36 +823,87 @@ class MutualFundSchemeGain(BaseModel):
"""Percentage gain or loss"""
+class MutualFundSchemeTransactionAdditionalInfo(BaseModel):
+ """Additional transaction-specific fields that vary by source"""
+
+ capital_withdrawal: Optional[float] = None
+ """Capital withdrawal amount (CDSL MF transactions)"""
+
+ credit: Optional[float] = None
+ """Units credited (demat transactions)"""
+
+ debit: Optional[float] = None
+ """Units debited (demat transactions)"""
+
+ income_distribution: Optional[float] = None
+ """Income distribution amount (CDSL MF transactions)"""
+
+ order_no: Optional[str] = None
+ """Order/transaction reference number (demat transactions)"""
+
+ price: Optional[float] = None
+ """Price per unit (NSDL/CDSL MF transactions)"""
+
+ stamp_duty: Optional[float] = None
+ """Stamp duty charged"""
+
+
class MutualFundSchemeTransaction(BaseModel):
+ """
+ Unified transaction schema for all holding types (MF folios, equities, bonds, etc.)
+ """
+
+ additional_info: Optional[MutualFundSchemeTransactionAdditionalInfo] = None
+ """Additional transaction-specific fields that vary by source"""
+
amount: Optional[float] = None
- """Transaction amount"""
+ """Transaction amount in currency (computed from units Ă— price/NAV)"""
balance: Optional[float] = None
"""Balance units after transaction"""
date: Optional[datetime.date] = None
- """Transaction date"""
+ """Transaction date (YYYY-MM-DD)"""
description: Optional[str] = None
- """Transaction description"""
+ """Transaction description/particulars"""
dividend_rate: Optional[float] = None
- """Dividend rate (for dividend transactions)"""
+ """Dividend rate (for DIVIDEND_PAYOUT transactions)"""
nav: Optional[float] = None
- """NAV on transaction date"""
-
- type: Optional[str] = None
- """Transaction type detected based on description.
-
- Possible values are
- PURCHASE,PURCHASE_SIP,REDEMPTION,SWITCH_IN,SWITCH_IN_MERGER,SWITCH_OUT,SWITCH_OUT_MERGER,DIVIDEND_PAYOUT,DIVIDEND_REINVESTMENT,SEGREGATION,STAMP_DUTY_TAX,TDS_TAX,STT_TAX,MISC.
- If dividend_rate is present, then possible values are dividend_rate is
- applicable only for DIVIDEND_PAYOUT and DIVIDEND_REINVESTMENT.
+ """NAV/price per unit on transaction date"""
+
+ type: Optional[
+ Literal[
+ "PURCHASE",
+ "PURCHASE_SIP",
+ "REDEMPTION",
+ "SWITCH_IN",
+ "SWITCH_IN_MERGER",
+ "SWITCH_OUT",
+ "SWITCH_OUT_MERGER",
+ "DIVIDEND_PAYOUT",
+ "DIVIDEND_REINVEST",
+ "SEGREGATION",
+ "STAMP_DUTY_TAX",
+ "TDS_TAX",
+ "STT_TAX",
+ "MISC",
+ "REVERSAL",
+ "UNKNOWN",
+ ]
+ ] = None
+ """Transaction type.
+
+ Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN,
+ SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT,
+ DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC,
+ REVERSAL, UNKNOWN.
"""
units: Optional[float] = None
- """Number of units involved"""
+ """Number of units involved in transaction"""
class MutualFundScheme(BaseModel):
@@ -370,6 +949,9 @@ class MutualFund(BaseModel):
folio_number: Optional[str] = None
"""Folio number"""
+ linked_holders: Optional[List[MutualFundLinkedHolder]] = None
+ """List of account holders linked to this mutual fund folio"""
+
registrar: Optional[str] = None
"""Registrar and Transfer Agent name"""
@@ -379,6 +961,63 @@ class MutualFund(BaseModel):
"""Total value of the folio"""
+class NpFundAdditionalInfo(BaseModel):
+ """Additional information specific to the NPS fund"""
+
+ manager: Optional[str] = None
+ """Fund manager name"""
+
+ tier: Optional[Literal[1, 2]] = None
+ """NPS tier (Tier I or Tier II)"""
+
+
+class NpFund(BaseModel):
+ additional_info: Optional[NpFundAdditionalInfo] = None
+ """Additional information specific to the NPS fund"""
+
+ cost: Optional[float] = None
+ """Cost of investment"""
+
+ name: Optional[str] = None
+ """Name of the NPS fund"""
+
+ nav: Optional[float] = None
+ """Net Asset Value per unit"""
+
+ units: Optional[float] = None
+ """Number of units held"""
+
+ value: Optional[float] = None
+ """Current market value of the holding"""
+
+
+class NpLinkedHolder(BaseModel):
+ name: Optional[str] = None
+ """Name of the account holder"""
+
+ pan: Optional[str] = None
+ """PAN of the account holder"""
+
+
+class Np(BaseModel):
+ additional_info: Optional[object] = None
+ """Additional information specific to the NPS account"""
+
+ cra: Optional[str] = None
+ """Central Record Keeping Agency name"""
+
+ funds: Optional[List[NpFund]] = None
+
+ linked_holders: Optional[List[NpLinkedHolder]] = None
+ """List of account holders linked to this NPS account"""
+
+ pran: Optional[str] = None
+ """Permanent Retirement Account Number (PRAN)"""
+
+ value: Optional[float] = None
+ """Total value of the NPS account"""
+
+
class SummaryAccountsDemat(BaseModel):
count: Optional[int] = None
"""Number of demat accounts"""
@@ -403,6 +1042,14 @@ class SummaryAccountsMutualFunds(BaseModel):
"""Total value of mutual funds"""
+class SummaryAccountsNps(BaseModel):
+ count: Optional[int] = None
+ """Number of NPS accounts"""
+
+ total_value: Optional[float] = None
+ """Total value of NPS accounts"""
+
+
class SummaryAccounts(BaseModel):
demat: Optional[SummaryAccountsDemat] = None
@@ -410,6 +1057,8 @@ class SummaryAccounts(BaseModel):
mutual_funds: Optional[SummaryAccountsMutualFunds] = None
+ nps: Optional[SummaryAccountsNps] = None
+
class Summary(BaseModel):
accounts: Optional[SummaryAccounts] = None
@@ -429,4 +1078,7 @@ class UnifiedResponse(BaseModel):
mutual_funds: Optional[List[MutualFund]] = None
+ nps: Optional[List[Np]] = None
+ """List of NPS accounts"""
+
summary: Optional[Summary] = None
diff --git a/tests/api_resources/test_cas_generator.py b/tests/api_resources/test_cas_generator.py
deleted file mode 100644
index d0d591d..0000000
--- a/tests/api_resources/test_cas_generator.py
+++ /dev/null
@@ -1,136 +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 cas_parser import CasParser, AsyncCasParser
-from tests.utils import assert_matches_type
-from cas_parser.types import CasGeneratorGenerateCasResponse
-
-base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
-
-
-class TestCasGenerator:
- parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
-
- @pytest.mark.skip(reason="Prism tests are disabled")
- @parametrize
- def test_method_generate_cas(self, client: CasParser) -> None:
- cas_generator = client.cas_generator.generate_cas(
- email="user@example.com",
- from_date="2023-01-01",
- password="Abcdefghi12$",
- to_date="2023-12-31",
- )
- assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"])
-
- @pytest.mark.skip(reason="Prism tests are disabled")
- @parametrize
- def test_method_generate_cas_with_all_params(self, client: CasParser) -> None:
- cas_generator = client.cas_generator.generate_cas(
- email="user@example.com",
- from_date="2023-01-01",
- password="Abcdefghi12$",
- to_date="2023-12-31",
- cas_authority="kfintech",
- pan_no="ABCDE1234F",
- )
- assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"])
-
- @pytest.mark.skip(reason="Prism tests are disabled")
- @parametrize
- def test_raw_response_generate_cas(self, client: CasParser) -> None:
- response = client.cas_generator.with_raw_response.generate_cas(
- email="user@example.com",
- from_date="2023-01-01",
- password="Abcdefghi12$",
- to_date="2023-12-31",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- cas_generator = response.parse()
- assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"])
-
- @pytest.mark.skip(reason="Prism tests are disabled")
- @parametrize
- def test_streaming_response_generate_cas(self, client: CasParser) -> None:
- with client.cas_generator.with_streaming_response.generate_cas(
- email="user@example.com",
- from_date="2023-01-01",
- password="Abcdefghi12$",
- to_date="2023-12-31",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- cas_generator = response.parse()
- assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
-
-class TestAsyncCasGenerator:
- parametrize = pytest.mark.parametrize(
- "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
- )
-
- @pytest.mark.skip(reason="Prism tests are disabled")
- @parametrize
- async def test_method_generate_cas(self, async_client: AsyncCasParser) -> None:
- cas_generator = await async_client.cas_generator.generate_cas(
- email="user@example.com",
- from_date="2023-01-01",
- password="Abcdefghi12$",
- to_date="2023-12-31",
- )
- assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"])
-
- @pytest.mark.skip(reason="Prism tests are disabled")
- @parametrize
- async def test_method_generate_cas_with_all_params(self, async_client: AsyncCasParser) -> None:
- cas_generator = await async_client.cas_generator.generate_cas(
- email="user@example.com",
- from_date="2023-01-01",
- password="Abcdefghi12$",
- to_date="2023-12-31",
- cas_authority="kfintech",
- pan_no="ABCDE1234F",
- )
- assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"])
-
- @pytest.mark.skip(reason="Prism tests are disabled")
- @parametrize
- async def test_raw_response_generate_cas(self, async_client: AsyncCasParser) -> None:
- response = await async_client.cas_generator.with_raw_response.generate_cas(
- email="user@example.com",
- from_date="2023-01-01",
- password="Abcdefghi12$",
- to_date="2023-12-31",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- cas_generator = await response.parse()
- assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"])
-
- @pytest.mark.skip(reason="Prism tests are disabled")
- @parametrize
- async def test_streaming_response_generate_cas(self, async_client: AsyncCasParser) -> None:
- async with async_client.cas_generator.with_streaming_response.generate_cas(
- email="user@example.com",
- from_date="2023-01-01",
- password="Abcdefghi12$",
- to_date="2023-12-31",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- cas_generator = await response.parse()
- assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"])
-
- assert cast(Any, response.is_closed) is True
diff --git a/tests/test_client.py b/tests/test_client.py
index e5b787e..6f70fd4 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -8,10 +8,11 @@
import json
import asyncio
import inspect
+import dataclasses
import tracemalloc
-from typing import Any, Union, cast
+from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast
from unittest import mock
-from typing_extensions import Literal
+from typing_extensions import Literal, AsyncIterator, override
import httpx
import pytest
@@ -36,6 +37,7 @@
from .utils import update_env
+T = TypeVar("T")
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
api_key = "My API Key"
@@ -50,6 +52,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float:
return 0.1
+def mirror_request_content(request: httpx.Request) -> httpx.Response:
+ return httpx.Response(200, content=request.content)
+
+
+# note: we can't use the httpx.MockTransport class as it consumes the request
+# body itself, which means we can't test that the body is read lazily
+class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport):
+ def __init__(
+ self,
+ handler: Callable[[httpx.Request], httpx.Response]
+ | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]],
+ ) -> None:
+ self.handler = handler
+
+ @override
+ def handle_request(
+ self,
+ request: httpx.Request,
+ ) -> httpx.Response:
+ assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function"
+ assert inspect.isfunction(self.handler), "handler must be a function"
+ return self.handler(request)
+
+ @override
+ async def handle_async_request(
+ self,
+ request: httpx.Request,
+ ) -> httpx.Response:
+ assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function"
+ return await self.handler(request)
+
+
+@dataclasses.dataclass
+class Counter:
+ value: int = 0
+
+
+def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]:
+ for item in iterable:
+ if counter:
+ counter.value += 1
+ yield item
+
+
+async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]:
+ for item in iterable:
+ if counter:
+ counter.value += 1
+ yield item
+
+
def _get_open_connections(client: CasParser | AsyncCasParser) -> int:
transport = client._client._transport
assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport)
@@ -59,51 +112,49 @@ def _get_open_connections(client: CasParser | AsyncCasParser) -> int:
class TestCasParser:
- client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True)
-
@pytest.mark.respx(base_url=base_url)
- def test_raw_response(self, respx_mock: MockRouter) -> None:
+ def test_raw_response(self, respx_mock: MockRouter, client: CasParser) -> None:
respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = self.client.post("/foo", cast_to=httpx.Response)
+ response = client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
@pytest.mark.respx(base_url=base_url)
- def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
+ def test_raw_response_for_binary(self, respx_mock: MockRouter, client: CasParser) -> None:
respx_mock.post("/foo").mock(
return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
)
- response = self.client.post("/foo", cast_to=httpx.Response)
+ response = client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
- def test_copy(self) -> None:
- copied = self.client.copy()
- assert id(copied) != id(self.client)
+ def test_copy(self, client: CasParser) -> None:
+ copied = client.copy()
+ assert id(copied) != id(client)
- copied = self.client.copy(api_key="another My API Key")
+ copied = client.copy(api_key="another My API Key")
assert copied.api_key == "another My API Key"
- assert self.client.api_key == "My API Key"
+ assert client.api_key == "My API Key"
- def test_copy_default_options(self) -> None:
+ def test_copy_default_options(self, client: CasParser) -> None:
# options that have a default are overridden correctly
- copied = self.client.copy(max_retries=7)
+ copied = client.copy(max_retries=7)
assert copied.max_retries == 7
- assert self.client.max_retries == 2
+ assert client.max_retries == 2
copied2 = copied.copy(max_retries=6)
assert copied2.max_retries == 6
assert copied.max_retries == 7
# timeout
- assert isinstance(self.client.timeout, httpx.Timeout)
- copied = self.client.copy(timeout=None)
+ assert isinstance(client.timeout, httpx.Timeout)
+ copied = client.copy(timeout=None)
assert copied.timeout is None
- assert isinstance(self.client.timeout, httpx.Timeout)
+ assert isinstance(client.timeout, httpx.Timeout)
def test_copy_default_headers(self) -> None:
client = CasParser(
@@ -138,6 +189,7 @@ def test_copy_default_headers(self) -> None:
match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
):
client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
+ client.close()
def test_copy_default_query(self) -> None:
client = CasParser(
@@ -175,13 +227,15 @@ def test_copy_default_query(self) -> None:
):
client.copy(set_default_query={}, default_query={"foo": "Bar"})
- def test_copy_signature(self) -> None:
+ client.close()
+
+ def test_copy_signature(self, client: CasParser) -> None:
# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
init_signature = inspect.signature(
# mypy doesn't like that we access the `__init__` property.
- self.client.__init__, # type: ignore[misc]
+ client.__init__, # type: ignore[misc]
)
- copy_signature = inspect.signature(self.client.copy)
+ copy_signature = inspect.signature(client.copy)
exclude_params = {"transport", "proxies", "_strict_response_validation"}
for name in init_signature.parameters.keys():
@@ -192,12 +246,12 @@ def test_copy_signature(self) -> None:
assert copy_param is not None, f"copy() signature is missing the {name} param"
@pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
- def test_copy_build_request(self) -> None:
+ def test_copy_build_request(self, client: CasParser) -> None:
options = FinalRequestOptions(method="get", url="/foo")
def build_request(options: FinalRequestOptions) -> None:
- client = self.client.copy()
- client._build_request(options)
+ client_copy = client.copy()
+ client_copy._build_request(options)
# ensure that the machinery is warmed up before tracing starts.
build_request(options)
@@ -254,14 +308,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic
print(frame)
raise AssertionError()
- def test_request_timeout(self) -> None:
- request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
+ def test_request_timeout(self, client: CasParser) -> None:
+ request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
- request = self.client._build_request(
- FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
- )
+ request = client._build_request(FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)))
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(100.0)
@@ -274,6 +326,8 @@ def test_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(0)
+ client.close()
+
def test_http_client_timeout_option(self) -> None:
# custom timeout given to the httpx client should be used
with httpx.Client(timeout=None) as http_client:
@@ -285,6 +339,8 @@ def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(None)
+ client.close()
+
# no timeout given to the httpx client should not use the httpx default
with httpx.Client() as http_client:
client = CasParser(
@@ -295,6 +351,8 @@ def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
+ client.close()
+
# explicitly passing the default timeout currently results in it being ignored
with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
client = CasParser(
@@ -305,6 +363,8 @@ def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT # our default
+ client.close()
+
async def test_invalid_http_client(self) -> None:
with pytest.raises(TypeError, match="Invalid `http_client` arg"):
async with httpx.AsyncClient() as http_client:
@@ -316,14 +376,14 @@ async def test_invalid_http_client(self) -> None:
)
def test_default_headers_option(self) -> None:
- client = CasParser(
+ test_client = CasParser(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
)
- request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
+ request = test_client._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "bar"
assert request.headers.get("x-stainless-lang") == "python"
- client2 = CasParser(
+ test_client2 = CasParser(
base_url=base_url,
api_key=api_key,
_strict_response_validation=True,
@@ -332,10 +392,13 @@ def test_default_headers_option(self) -> None:
"X-Stainless-Lang": "my-overriding-header",
},
)
- request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
+ request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "stainless"
assert request.headers.get("x-stainless-lang") == "my-overriding-header"
+ test_client.close()
+ test_client2.close()
+
def test_validate_headers(self) -> None:
client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True)
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
@@ -364,8 +427,10 @@ def test_default_query_option(self) -> None:
url = httpx.URL(request.url)
assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
- def test_request_extra_json(self) -> None:
- request = self.client._build_request(
+ client.close()
+
+ def test_request_extra_json(self, client: CasParser) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -376,7 +441,7 @@ def test_request_extra_json(self) -> None:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": False}
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -387,7 +452,7 @@ def test_request_extra_json(self) -> None:
assert data == {"baz": False}
# `extra_json` takes priority over `json_data` when keys clash
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -398,8 +463,8 @@ def test_request_extra_json(self) -> None:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": None}
- def test_request_extra_headers(self) -> None:
- request = self.client._build_request(
+ def test_request_extra_headers(self, client: CasParser) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -409,7 +474,7 @@ def test_request_extra_headers(self) -> None:
assert request.headers.get("X-Foo") == "Foo"
# `extra_headers` takes priority over `default_headers` when keys clash
- request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
+ request = client.with_options(default_headers={"X-Bar": "true"})._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -420,8 +485,8 @@ def test_request_extra_headers(self) -> None:
)
assert request.headers.get("X-Bar") == "false"
- def test_request_extra_query(self) -> None:
- request = self.client._build_request(
+ def test_request_extra_query(self, client: CasParser) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -434,7 +499,7 @@ def test_request_extra_query(self) -> None:
assert params == {"my_query_param": "Foo"}
# if both `query` and `extra_query` are given, they are merged
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -448,7 +513,7 @@ def test_request_extra_query(self) -> None:
assert params == {"bar": "1", "foo": "2"}
# `extra_query` takes priority over `query` when keys clash
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -491,7 +556,71 @@ def test_multipart_repeating_array(self, client: CasParser) -> None:
]
@pytest.mark.respx(base_url=base_url)
- def test_basic_union_response(self, respx_mock: MockRouter) -> None:
+ def test_binary_content_upload(self, respx_mock: MockRouter, client: CasParser) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ response = client.post(
+ "/upload",
+ content=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
+ def test_binary_content_upload_with_iterator(self) -> None:
+ file_content = b"Hello, this is a test file."
+ counter = Counter()
+ iterator = _make_sync_iterator([file_content], counter=counter)
+
+ def mock_handler(request: httpx.Request) -> httpx.Response:
+ assert counter.value == 0, "the request body should not have been read"
+ return httpx.Response(200, content=request.read())
+
+ with CasParser(
+ base_url=base_url,
+ api_key=api_key,
+ _strict_response_validation=True,
+ http_client=httpx.Client(transport=MockTransport(handler=mock_handler)),
+ ) as client:
+ response = client.post(
+ "/upload",
+ content=iterator,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+ assert counter.value == 1
+
+ @pytest.mark.respx(base_url=base_url)
+ def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: CasParser) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ with pytest.deprecated_call(
+ match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead."
+ ):
+ response = client.post(
+ "/upload",
+ body=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
+ @pytest.mark.respx(base_url=base_url)
+ def test_basic_union_response(self, respx_mock: MockRouter, client: CasParser) -> None:
class Model1(BaseModel):
name: str
@@ -500,12 +629,12 @@ class Model2(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
@pytest.mark.respx(base_url=base_url)
- def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
+ def test_union_response_different_types(self, respx_mock: MockRouter, client: CasParser) -> None:
"""Union of objects with the same field name using a different type"""
class Model1(BaseModel):
@@ -516,18 +645,18 @@ class Model2(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
- response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model1)
assert response.foo == 1
@pytest.mark.respx(base_url=base_url)
- def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
+ def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: CasParser) -> None:
"""
Response that sets Content-Type to something other than application/json but returns json data
"""
@@ -543,7 +672,7 @@ class Model(BaseModel):
)
)
- response = self.client.get("/foo", cast_to=Model)
+ response = client.get("/foo", cast_to=Model)
assert isinstance(response, Model)
assert response.foo == 2
@@ -555,6 +684,8 @@ def test_base_url_setter(self) -> None:
assert client.base_url == "https://example.com/from_setter/"
+ client.close()
+
def test_base_url_env(self) -> None:
with update_env(CAS_PARSER_BASE_URL="http://localhost:5000/from/env"):
client = CasParser(api_key=api_key, _strict_response_validation=True)
@@ -582,6 +713,7 @@ def test_base_url_trailing_slash(self, client: CasParser) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ client.close()
@pytest.mark.parametrize(
"client",
@@ -605,6 +737,7 @@ def test_base_url_no_trailing_slash(self, client: CasParser) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ client.close()
@pytest.mark.parametrize(
"client",
@@ -628,35 +761,36 @@ def test_absolute_request_url(self, client: CasParser) -> None:
),
)
assert request.url == "https://myapi.com/foo"
+ client.close()
def test_copied_client_does_not_close_http(self) -> None:
- client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True)
- assert not client.is_closed()
+ test_client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True)
+ assert not test_client.is_closed()
- copied = client.copy()
- assert copied is not client
+ copied = test_client.copy()
+ assert copied is not test_client
del copied
- assert not client.is_closed()
+ assert not test_client.is_closed()
def test_client_context_manager(self) -> None:
- client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True)
- with client as c2:
- assert c2 is client
+ test_client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True)
+ with test_client as c2:
+ assert c2 is test_client
assert not c2.is_closed()
- assert not client.is_closed()
- assert client.is_closed()
+ assert not test_client.is_closed()
+ assert test_client.is_closed()
@pytest.mark.respx(base_url=base_url)
- def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
+ def test_client_response_validation_error(self, respx_mock: MockRouter, client: CasParser) -> None:
class Model(BaseModel):
foo: str
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
with pytest.raises(APIResponseValidationError) as exc:
- self.client.get("/foo", cast_to=Model)
+ client.get("/foo", cast_to=Model)
assert isinstance(exc.value.__cause__, ValidationError)
@@ -676,11 +810,14 @@ class Model(BaseModel):
with pytest.raises(APIResponseValidationError):
strict_client.get("/foo", cast_to=Model)
- client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=False)
+ non_strict_client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=False)
- response = client.get("/foo", cast_to=Model)
+ response = non_strict_client.get("/foo", cast_to=Model)
assert isinstance(response, str) # type: ignore[unreachable]
+ strict_client.close()
+ non_strict_client.close()
+
@pytest.mark.parametrize(
"remaining_retries,retry_after,timeout",
[
@@ -703,9 +840,9 @@ class Model(BaseModel):
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
- def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
- client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True)
-
+ def test_parse_retry_after_header(
+ self, remaining_retries: int, retry_after: str, timeout: float, client: CasParser
+ ) -> None:
headers = httpx.Headers({"retry-after": retry_after})
options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
@@ -719,7 +856,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien
with pytest.raises(APITimeoutError):
client.cas_parser.with_streaming_response.smart_parse().__enter__()
- assert _get_open_connections(self.client) == 0
+ assert _get_open_connections(client) == 0
@mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
@@ -728,7 +865,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client
with pytest.raises(APIStatusError):
client.cas_parser.with_streaming_response.smart_parse().__enter__()
- assert _get_open_connections(self.client) == 0
+ assert _get_open_connections(client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@@ -830,83 +967,77 @@ def test_default_client_creation(self) -> None:
)
@pytest.mark.respx(base_url=base_url)
- def test_follow_redirects(self, respx_mock: MockRouter) -> None:
+ def test_follow_redirects(self, respx_mock: MockRouter, client: CasParser) -> None:
# Test that the default follow_redirects=True allows following redirects
respx_mock.post("/redirect").mock(
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
)
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
- response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
+ response = client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
assert response.status_code == 200
assert response.json() == {"status": "ok"}
@pytest.mark.respx(base_url=base_url)
- def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
+ def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: CasParser) -> None:
# Test that follow_redirects=False prevents following redirects
respx_mock.post("/redirect").mock(
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
)
with pytest.raises(APIStatusError) as exc_info:
- self.client.post(
- "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
- )
+ client.post("/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response)
assert exc_info.value.response.status_code == 302
assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
class TestAsyncCasParser:
- client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True)
-
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
- async def test_raw_response(self, respx_mock: MockRouter) -> None:
+ async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None:
respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = await self.client.post("/foo", cast_to=httpx.Response)
+ response = await async_client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
- async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
+ async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None:
respx_mock.post("/foo").mock(
return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
)
- response = await self.client.post("/foo", cast_to=httpx.Response)
+ response = await async_client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
- def test_copy(self) -> None:
- copied = self.client.copy()
- assert id(copied) != id(self.client)
+ def test_copy(self, async_client: AsyncCasParser) -> None:
+ copied = async_client.copy()
+ assert id(copied) != id(async_client)
- copied = self.client.copy(api_key="another My API Key")
+ copied = async_client.copy(api_key="another My API Key")
assert copied.api_key == "another My API Key"
- assert self.client.api_key == "My API Key"
+ assert async_client.api_key == "My API Key"
- def test_copy_default_options(self) -> None:
+ def test_copy_default_options(self, async_client: AsyncCasParser) -> None:
# options that have a default are overridden correctly
- copied = self.client.copy(max_retries=7)
+ copied = async_client.copy(max_retries=7)
assert copied.max_retries == 7
- assert self.client.max_retries == 2
+ assert async_client.max_retries == 2
copied2 = copied.copy(max_retries=6)
assert copied2.max_retries == 6
assert copied.max_retries == 7
# timeout
- assert isinstance(self.client.timeout, httpx.Timeout)
- copied = self.client.copy(timeout=None)
+ assert isinstance(async_client.timeout, httpx.Timeout)
+ copied = async_client.copy(timeout=None)
assert copied.timeout is None
- assert isinstance(self.client.timeout, httpx.Timeout)
+ assert isinstance(async_client.timeout, httpx.Timeout)
- def test_copy_default_headers(self) -> None:
+ async def test_copy_default_headers(self) -> None:
client = AsyncCasParser(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
)
@@ -939,8 +1070,9 @@ def test_copy_default_headers(self) -> None:
match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
):
client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
+ await client.close()
- def test_copy_default_query(self) -> None:
+ async def test_copy_default_query(self) -> None:
client = AsyncCasParser(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"}
)
@@ -976,13 +1108,15 @@ def test_copy_default_query(self) -> None:
):
client.copy(set_default_query={}, default_query={"foo": "Bar"})
- def test_copy_signature(self) -> None:
+ await client.close()
+
+ def test_copy_signature(self, async_client: AsyncCasParser) -> None:
# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
init_signature = inspect.signature(
# mypy doesn't like that we access the `__init__` property.
- self.client.__init__, # type: ignore[misc]
+ async_client.__init__, # type: ignore[misc]
)
- copy_signature = inspect.signature(self.client.copy)
+ copy_signature = inspect.signature(async_client.copy)
exclude_params = {"transport", "proxies", "_strict_response_validation"}
for name in init_signature.parameters.keys():
@@ -993,12 +1127,12 @@ def test_copy_signature(self) -> None:
assert copy_param is not None, f"copy() signature is missing the {name} param"
@pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
- def test_copy_build_request(self) -> None:
+ def test_copy_build_request(self, async_client: AsyncCasParser) -> None:
options = FinalRequestOptions(method="get", url="/foo")
def build_request(options: FinalRequestOptions) -> None:
- client = self.client.copy()
- client._build_request(options)
+ client_copy = async_client.copy()
+ client_copy._build_request(options)
# ensure that the machinery is warmed up before tracing starts.
build_request(options)
@@ -1055,12 +1189,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic
print(frame)
raise AssertionError()
- async def test_request_timeout(self) -> None:
- request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
+ async def test_request_timeout(self, async_client: AsyncCasParser) -> None:
+ request = async_client._build_request(FinalRequestOptions(method="get", url="/foo"))
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
- request = self.client._build_request(
+ request = async_client._build_request(
FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
)
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
@@ -1075,6 +1209,8 @@ async def test_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(0)
+ await client.close()
+
async def test_http_client_timeout_option(self) -> None:
# custom timeout given to the httpx client should be used
async with httpx.AsyncClient(timeout=None) as http_client:
@@ -1086,6 +1222,8 @@ async def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(None)
+ await client.close()
+
# no timeout given to the httpx client should not use the httpx default
async with httpx.AsyncClient() as http_client:
client = AsyncCasParser(
@@ -1096,6 +1234,8 @@ async def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
+ await client.close()
+
# explicitly passing the default timeout currently results in it being ignored
async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
client = AsyncCasParser(
@@ -1106,6 +1246,8 @@ async def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT # our default
+ await client.close()
+
def test_invalid_http_client(self) -> None:
with pytest.raises(TypeError, match="Invalid `http_client` arg"):
with httpx.Client() as http_client:
@@ -1116,15 +1258,15 @@ def test_invalid_http_client(self) -> None:
http_client=cast(Any, http_client),
)
- def test_default_headers_option(self) -> None:
- client = AsyncCasParser(
+ async def test_default_headers_option(self) -> None:
+ test_client = AsyncCasParser(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"}
)
- request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
+ request = test_client._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "bar"
assert request.headers.get("x-stainless-lang") == "python"
- client2 = AsyncCasParser(
+ test_client2 = AsyncCasParser(
base_url=base_url,
api_key=api_key,
_strict_response_validation=True,
@@ -1133,10 +1275,13 @@ def test_default_headers_option(self) -> None:
"X-Stainless-Lang": "my-overriding-header",
},
)
- request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
+ request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "stainless"
assert request.headers.get("x-stainless-lang") == "my-overriding-header"
+ await test_client.close()
+ await test_client2.close()
+
def test_validate_headers(self) -> None:
client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True)
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
@@ -1147,7 +1292,7 @@ def test_validate_headers(self) -> None:
client2 = AsyncCasParser(base_url=base_url, api_key=None, _strict_response_validation=True)
_ = client2
- def test_default_query_option(self) -> None:
+ async def test_default_query_option(self) -> None:
client = AsyncCasParser(
base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}
)
@@ -1165,8 +1310,10 @@ def test_default_query_option(self) -> None:
url = httpx.URL(request.url)
assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
- def test_request_extra_json(self) -> None:
- request = self.client._build_request(
+ await client.close()
+
+ def test_request_extra_json(self, client: CasParser) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1177,7 +1324,7 @@ def test_request_extra_json(self) -> None:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": False}
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1188,7 +1335,7 @@ def test_request_extra_json(self) -> None:
assert data == {"baz": False}
# `extra_json` takes priority over `json_data` when keys clash
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1199,8 +1346,8 @@ def test_request_extra_json(self) -> None:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": None}
- def test_request_extra_headers(self) -> None:
- request = self.client._build_request(
+ def test_request_extra_headers(self, client: CasParser) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1210,7 +1357,7 @@ def test_request_extra_headers(self) -> None:
assert request.headers.get("X-Foo") == "Foo"
# `extra_headers` takes priority over `default_headers` when keys clash
- request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
+ request = client.with_options(default_headers={"X-Bar": "true"})._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1221,8 +1368,8 @@ def test_request_extra_headers(self) -> None:
)
assert request.headers.get("X-Bar") == "false"
- def test_request_extra_query(self) -> None:
- request = self.client._build_request(
+ def test_request_extra_query(self, client: CasParser) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1235,7 +1382,7 @@ def test_request_extra_query(self) -> None:
assert params == {"my_query_param": "Foo"}
# if both `query` and `extra_query` are given, they are merged
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1249,7 +1396,7 @@ def test_request_extra_query(self) -> None:
assert params == {"bar": "1", "foo": "2"}
# `extra_query` takes priority over `query` when keys clash
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1292,7 +1439,73 @@ def test_multipart_repeating_array(self, async_client: AsyncCasParser) -> None:
]
@pytest.mark.respx(base_url=base_url)
- async def test_basic_union_response(self, respx_mock: MockRouter) -> None:
+ async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ response = await async_client.post(
+ "/upload",
+ content=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
+ async def test_binary_content_upload_with_asynciterator(self) -> None:
+ file_content = b"Hello, this is a test file."
+ counter = Counter()
+ iterator = _make_async_iterator([file_content], counter=counter)
+
+ async def mock_handler(request: httpx.Request) -> httpx.Response:
+ assert counter.value == 0, "the request body should not have been read"
+ return httpx.Response(200, content=await request.aread())
+
+ async with AsyncCasParser(
+ base_url=base_url,
+ api_key=api_key,
+ _strict_response_validation=True,
+ http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)),
+ ) as client:
+ response = await client.post(
+ "/upload",
+ content=iterator,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+ assert counter.value == 1
+
+ @pytest.mark.respx(base_url=base_url)
+ async def test_binary_content_upload_with_body_is_deprecated(
+ self, respx_mock: MockRouter, async_client: AsyncCasParser
+ ) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ with pytest.deprecated_call(
+ match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead."
+ ):
+ response = await async_client.post(
+ "/upload",
+ body=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
+ @pytest.mark.respx(base_url=base_url)
+ async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None:
class Model1(BaseModel):
name: str
@@ -1301,12 +1514,12 @@ class Model2(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
@pytest.mark.respx(base_url=base_url)
- async def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
+ async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None:
"""Union of objects with the same field name using a different type"""
class Model1(BaseModel):
@@ -1317,18 +1530,20 @@ class Model2(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
- response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model1)
assert response.foo == 1
@pytest.mark.respx(base_url=base_url)
- async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
+ async def test_non_application_json_content_type_for_json_data(
+ self, respx_mock: MockRouter, async_client: AsyncCasParser
+ ) -> None:
"""
Response that sets Content-Type to something other than application/json but returns json data
"""
@@ -1344,11 +1559,11 @@ class Model(BaseModel):
)
)
- response = await self.client.get("/foo", cast_to=Model)
+ response = await async_client.get("/foo", cast_to=Model)
assert isinstance(response, Model)
assert response.foo == 2
- def test_base_url_setter(self) -> None:
+ async def test_base_url_setter(self) -> None:
client = AsyncCasParser(
base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True
)
@@ -1358,7 +1573,9 @@ def test_base_url_setter(self) -> None:
assert client.base_url == "https://example.com/from_setter/"
- def test_base_url_env(self) -> None:
+ await client.close()
+
+ async def test_base_url_env(self) -> None:
with update_env(CAS_PARSER_BASE_URL="http://localhost:5000/from/env"):
client = AsyncCasParser(api_key=api_key, _strict_response_validation=True)
assert client.base_url == "http://localhost:5000/from/env/"
@@ -1378,7 +1595,7 @@ def test_base_url_env(self) -> None:
],
ids=["standard", "custom http client"],
)
- def test_base_url_trailing_slash(self, client: AsyncCasParser) -> None:
+ async def test_base_url_trailing_slash(self, client: AsyncCasParser) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
@@ -1387,6 +1604,7 @@ def test_base_url_trailing_slash(self, client: AsyncCasParser) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ await client.close()
@pytest.mark.parametrize(
"client",
@@ -1403,7 +1621,7 @@ def test_base_url_trailing_slash(self, client: AsyncCasParser) -> None:
],
ids=["standard", "custom http client"],
)
- def test_base_url_no_trailing_slash(self, client: AsyncCasParser) -> None:
+ async def test_base_url_no_trailing_slash(self, client: AsyncCasParser) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
@@ -1412,6 +1630,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncCasParser) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ await client.close()
@pytest.mark.parametrize(
"client",
@@ -1428,7 +1647,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncCasParser) -> None:
],
ids=["standard", "custom http client"],
)
- def test_absolute_request_url(self, client: AsyncCasParser) -> None:
+ async def test_absolute_request_url(self, client: AsyncCasParser) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
@@ -1437,37 +1656,37 @@ def test_absolute_request_url(self, client: AsyncCasParser) -> None:
),
)
assert request.url == "https://myapi.com/foo"
+ await client.close()
async def test_copied_client_does_not_close_http(self) -> None:
- client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True)
- assert not client.is_closed()
+ test_client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True)
+ assert not test_client.is_closed()
- copied = client.copy()
- assert copied is not client
+ copied = test_client.copy()
+ assert copied is not test_client
del copied
await asyncio.sleep(0.2)
- assert not client.is_closed()
+ assert not test_client.is_closed()
async def test_client_context_manager(self) -> None:
- client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True)
- async with client as c2:
- assert c2 is client
+ test_client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True)
+ async with test_client as c2:
+ assert c2 is test_client
assert not c2.is_closed()
- assert not client.is_closed()
- assert client.is_closed()
+ assert not test_client.is_closed()
+ assert test_client.is_closed()
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
- async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
+ async def test_client_response_validation_error(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None:
class Model(BaseModel):
foo: str
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
with pytest.raises(APIResponseValidationError) as exc:
- await self.client.get("/foo", cast_to=Model)
+ await async_client.get("/foo", cast_to=Model)
assert isinstance(exc.value.__cause__, ValidationError)
@@ -1478,7 +1697,6 @@ async def test_client_max_retries_validation(self) -> None:
)
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
class Model(BaseModel):
name: str
@@ -1490,11 +1708,14 @@ class Model(BaseModel):
with pytest.raises(APIResponseValidationError):
await strict_client.get("/foo", cast_to=Model)
- client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=False)
+ non_strict_client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=False)
- response = await client.get("/foo", cast_to=Model)
+ response = await non_strict_client.get("/foo", cast_to=Model)
assert isinstance(response, str) # type: ignore[unreachable]
+ await strict_client.close()
+ await non_strict_client.close()
+
@pytest.mark.parametrize(
"remaining_retries,retry_after,timeout",
[
@@ -1517,13 +1738,12 @@ class Model(BaseModel):
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
- @pytest.mark.asyncio
- async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
- client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True)
-
+ async def test_parse_retry_after_header(
+ self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncCasParser
+ ) -> None:
headers = httpx.Headers({"retry-after": retry_after})
options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
- calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
+ calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers)
assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
@mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@@ -1536,7 +1756,7 @@ async def test_retrying_timeout_errors_doesnt_leak(
with pytest.raises(APITimeoutError):
await async_client.cas_parser.with_streaming_response.smart_parse().__aenter__()
- assert _get_open_connections(self.client) == 0
+ assert _get_open_connections(async_client) == 0
@mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
@@ -1547,12 +1767,11 @@ async def test_retrying_status_errors_doesnt_leak(
with pytest.raises(APIStatusError):
await async_client.cas_parser.with_streaming_response.smart_parse().__aenter__()
- assert _get_open_connections(self.client) == 0
+ assert _get_open_connections(async_client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
@pytest.mark.parametrize("failure_mode", ["status", "exception"])
async def test_retries_taken(
self,
@@ -1584,7 +1803,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
async def test_omit_retry_count_header(
self, async_client: AsyncCasParser, failures_before_success: int, respx_mock: MockRouter
) -> None:
@@ -1610,7 +1828,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
async def test_overwrite_retry_count_header(
self, async_client: AsyncCasParser, failures_before_success: int, respx_mock: MockRouter
) -> None:
@@ -1660,26 +1877,26 @@ async def test_default_client_creation(self) -> None:
)
@pytest.mark.respx(base_url=base_url)
- async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
+ async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None:
# Test that the default follow_redirects=True allows following redirects
respx_mock.post("/redirect").mock(
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
)
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
- response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
+ response = await async_client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
assert response.status_code == 200
assert response.json() == {"status": "ok"}
@pytest.mark.respx(base_url=base_url)
- async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
+ async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None:
# Test that follow_redirects=False prevents following redirects
respx_mock.post("/redirect").mock(
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
)
with pytest.raises(APIStatusError) as exc_info:
- await self.client.post(
+ await async_client.post(
"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
)
diff --git a/tests/test_models.py b/tests/test_models.py
index ffd0d05..82ce6d4 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -9,7 +9,7 @@
from cas_parser._utils import PropertyInfo
from cas_parser._compat import PYDANTIC_V1, parse_obj, model_dump, model_json
-from cas_parser._models import BaseModel, construct_type
+from cas_parser._models import DISCRIMINATOR_CACHE, BaseModel, construct_type
class BasicModel(BaseModel):
@@ -809,7 +809,7 @@ class B(BaseModel):
UnionType = cast(Any, Union[A, B])
- assert not hasattr(UnionType, "__discriminator__")
+ assert not DISCRIMINATOR_CACHE.get(UnionType)
m = construct_type(
value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")])
@@ -818,7 +818,7 @@ class B(BaseModel):
assert m.type == "b"
assert m.data == "foo" # type: ignore[comparison-overlap]
- discriminator = UnionType.__discriminator__
+ discriminator = DISCRIMINATOR_CACHE.get(UnionType)
assert discriminator is not None
m = construct_type(
@@ -830,7 +830,7 @@ class B(BaseModel):
# if the discriminator details object stays the same between invocations then
# we hit the cache
- assert UnionType.__discriminator__ is discriminator
+ assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator
@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1")
diff --git a/tests/test_transform.py b/tests/test_transform.py
index ce97c84..451ddf6 100644
--- a/tests/test_transform.py
+++ b/tests/test_transform.py
@@ -8,7 +8,7 @@
import pytest
-from cas_parser._types import NOT_GIVEN, Base64FileInput
+from cas_parser._types import Base64FileInput, omit, not_given
from cas_parser._utils import (
PropertyInfo,
transform as _transform,
@@ -450,4 +450,11 @@ async def test_transform_skipping(use_async: bool) -> None:
@pytest.mark.asyncio
async def test_strips_notgiven(use_async: bool) -> None:
assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"}
- assert await transform({"foo_bar": NOT_GIVEN}, Foo1, use_async) == {}
+ assert await transform({"foo_bar": not_given}, Foo1, use_async) == {}
+
+
+@parametrize
+@pytest.mark.asyncio
+async def test_strips_omit(use_async: bool) -> None:
+ assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"}
+ assert await transform({"foo_bar": omit}, Foo1, use_async) == {}