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..4465de7 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-7e6397bddc220d1a59b5e2c7e7c3ff38f1a6eb174f4e383e03bc49cf78c8c44f.yml
+openapi_spec_hash: cb852eeb4ce89c80f4246815cbe21f72
config_hash: cb5d75abef6264b5d86448caf7295afa
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eff9d05..24613cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,33 @@
# Changelog
+## 1.2.0 (2026-01-05)
+
+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 ([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..bfab47b 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[)](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).
@@ -85,6 +85,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 +93,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 +381,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..9cfe0c2 100644
--- a/src/cas_parser/_base_client.py
+++ b/src/cas_parser/_base_client.py
@@ -42,7 +42,6 @@
from ._qs import Querystring
from ._files import to_httpx_files, async_to_httpx_files
from ._types import (
- NOT_GIVEN,
Body,
Omit,
Query,
@@ -57,6 +56,7 @@
RequestOptions,
HttpxRequestFiles,
ModelBuilderProtocol,
+ not_given,
)
from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
from ._compat import PYDANTIC_V1, model_copy, model_dump
@@ -145,9 +145,9 @@ def __init__(
def __init__(
self,
*,
- url: URL | NotGiven = NOT_GIVEN,
- json: Body | NotGiven = NOT_GIVEN,
- params: Query | NotGiven = NOT_GIVEN,
+ url: URL | NotGiven = not_given,
+ json: Body | NotGiven = not_given,
+ params: Query | NotGiven = not_given,
) -> None:
self.url = url
self.json = json
@@ -595,7 +595,7 @@ def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalReques
# we internally support defining a temporary header to override the
# default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response`
# see _response.py for implementation details
- override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, NOT_GIVEN)
+ override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, not_given)
if is_given(override_cast_to):
options.headers = headers
return cast(Type[ResponseT], override_cast_to)
@@ -825,7 +825,7 @@ def __init__(
version: str,
base_url: str | URL,
max_retries: int = DEFAULT_MAX_RETRIES,
- timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
+ timeout: float | Timeout | None | NotGiven = not_given,
http_client: httpx.Client | None = None,
custom_headers: Mapping[str, str] | None = None,
custom_query: Mapping[str, object] | None = None,
@@ -1247,9 +1247,12 @@ def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options)
+ opts = FinalRequestOptions.construct(
+ method="patch", url=path, json_data=body, files=to_httpx_files(files), **options
+ )
return self.request(cast_to, opts)
def put(
@@ -1356,7 +1359,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,
@@ -1767,9 +1770,12 @@ async def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options)
+ opts = FinalRequestOptions.construct(
+ method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options
+ )
return await self.request(cast_to, opts)
async def put(
@@ -1818,8 +1824,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..ca9500b 100644
--- a/src/cas_parser/_models.py
+++ b/src/cas_parser/_models.py
@@ -2,6 +2,7 @@
import os
import inspect
+import weakref
from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
from datetime import date, datetime
from typing_extensions import (
@@ -256,13 +257,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 +274,24 @@ def model_dump(
Args:
mode: The mode in which `to_python` should run.
- If mode is 'json', the dictionary will only contain JSON serializable types.
- If mode is 'python', the dictionary may contain any Python objects.
- include: A list of fields to include in the output.
- exclude: A list of fields to exclude from the output.
+ If mode is 'json', the output will only contain JSON serializable types.
+ If mode is 'python', the output may contain non-JSON-serializable Python objects.
+ include: A set of fields to include in the output.
+ exclude: A set of fields to exclude from the output.
+ context: Additional context to pass to the serializer.
by_alias: Whether to use the field's alias in the dictionary key if defined.
- exclude_unset: Whether to exclude fields that are unset or None from the output.
- exclude_defaults: Whether to exclude fields that are set to their default value from the output.
- exclude_none: Whether to exclude fields that have a value of `None` from the output.
- round_trip: Whether to enable serialization and deserialization round-trip support.
- warnings: Whether to log warnings when invalid fields are encountered.
+ exclude_unset: Whether to exclude fields that have not been explicitly set.
+ exclude_defaults: Whether to exclude fields that are set to their default value.
+ exclude_none: Whether to exclude fields that have a value of `None`.
+ exclude_computed_fields: Whether to exclude computed fields.
+ While this can be useful for round-tripping, it is usually recommended to use the dedicated
+ `round_trip` parameter instead.
+ round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].
+ warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors,
+ "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].
+ fallback: A function to call when an unknown value is encountered. If not provided,
+ a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.
+ serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.
Returns:
A dictionary representation of the model.
@@ -295,10 +306,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 +326,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 +366,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 +591,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 +636,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 +691,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
discriminator_field=discriminator_field_name,
discriminator_alias=discriminator_alias,
)
- cast(CachedDiscriminatorType, union).__discriminator__ = details
+ DISCRIMINATOR_CACHE.setdefault(union, details)
return details
diff --git a/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..2c15258 100644
--- a/src/cas_parser/_types.py
+++ b/src/cas_parser/_types.py
@@ -117,18 +117,21 @@ class RequestOptions(TypedDict, total=False):
# Sentinel class used until PEP 0661 is accepted
class NotGiven:
"""
- A sentinel singleton class used to distinguish omitted keyword arguments
- from those passed in with the value None (which may have different behavior).
+ For parameters with a meaningful None value, we need to distinguish between
+ the user explicitly passing None, and the user not passing the parameter at
+ all.
+
+ User code shouldn't need to use not_given directly.
For example:
```py
- def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ...
+ def create(timeout: Timeout | None | NotGiven = not_given): ...
- get(timeout=1) # 1s timeout
- get(timeout=None) # No timeout
- get() # Default timeout behavior, which may not be statically known at the method definition.
+ create(timeout=1) # 1s timeout
+ create(timeout=None) # No timeout
+ create() # Default timeout behavior
```
"""
@@ -140,13 +143,14 @@ def __repr__(self) -> str:
return "NOT_GIVEN"
-NotGivenOr = Union[_T, NotGiven]
+not_given = NotGiven()
+# for backwards compatibility:
NOT_GIVEN = NotGiven()
class Omit:
- """In certain situations you need to be able to represent a case where a default value has
- to be explicitly removed and `None` is not an appropriate substitute, for example:
+ """
+ To explicitly omit something from being sent in a request, use `omit`.
```py
# as the default `Content-Type` header is `application/json` that will be sent
@@ -156,8 +160,8 @@ class Omit:
# to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983'
client.post(..., headers={"Content-Type": "multipart/form-data"})
- # instead you can remove the default `application/json` header by passing Omit
- client.post(..., headers={"Content-Type": Omit()})
+ # instead you can remove the default `application/json` header by passing omit
+ client.post(..., headers={"Content-Type": omit})
```
"""
@@ -165,6 +169,9 @@ def __bool__(self) -> Literal[False]:
return False
+omit = Omit()
+
+
@runtime_checkable
class ModelBuilderProtocol(Protocol):
@classmethod
@@ -236,6 +243,9 @@ class HttpxSendArgs(TypedDict, total=False):
if TYPE_CHECKING:
# This works because str.__contains__ does not accept object (either in typeshed or at runtime)
# https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285
+ #
+ # Note: index() and count() methods are intentionally omitted to allow pyright to properly
+ # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr.
class SequenceNotStr(Protocol[_T_co]):
@overload
def __getitem__(self, index: SupportsIndex, /) -> _T_co: ...
@@ -244,8 +254,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ...
def __contains__(self, value: object, /) -> bool: ...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[_T_co]: ...
- def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ...
- def count(self, value: Any, /) -> int: ...
def __reversed__(self) -> Iterator[_T_co]: ...
else:
# just point this to a normal `Sequence` at runtime to avoid having to special case
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..47523fb 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -59,51 +59,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 +136,7 @@ def test_copy_default_headers(self) -> None:
match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
):
client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
+ client.close()
def test_copy_default_query(self) -> None:
client = CasParser(
@@ -175,13 +174,15 @@ def test_copy_default_query(self) -> None:
):
client.copy(set_default_query={}, default_query={"foo": "Bar"})
- def test_copy_signature(self) -> None:
+ client.close()
+
+ def test_copy_signature(self, client: 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 +193,12 @@ def test_copy_signature(self) -> None:
assert copy_param is not None, f"copy() signature is missing the {name} param"
@pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
- def test_copy_build_request(self) -> None:
+ def test_copy_build_request(self, client: 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 +255,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic
print(frame)
raise AssertionError()
- def test_request_timeout(self) -> None:
- request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
+ def test_request_timeout(self, client: 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 +273,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 +286,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 +298,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 +310,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 +323,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 +339,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 +374,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 +388,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 +399,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 +410,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 +421,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 +432,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 +446,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 +460,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 +503,7 @@ 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_basic_union_response(self, respx_mock: MockRouter, client: CasParser) -> None:
class Model1(BaseModel):
name: str
@@ -500,12 +512,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 +528,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 +555,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 +567,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 +596,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 +620,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 +644,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 +693,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 +723,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 +739,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 +748,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 +850,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 +953,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 +991,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 +1010,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 +1072,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 +1092,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 +1105,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 +1117,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 +1129,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 +1141,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 +1158,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 +1175,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 +1193,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 +1207,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 +1218,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 +1229,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 +1240,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 +1251,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 +1265,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 +1279,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 +1322,7 @@ 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_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None:
class Model1(BaseModel):
name: str
@@ -1301,12 +1331,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 +1347,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 +1376,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 +1390,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 +1412,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 +1421,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 +1438,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 +1447,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 +1464,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 +1473,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 +1514,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 +1525,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 +1555,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 +1573,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 +1584,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 +1620,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 +1645,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 +1694,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) == {}