cnovatech-sdk-core 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. cnovatech_sdk_core-0.1.0/.gitignore +26 -0
  2. cnovatech_sdk_core-0.1.0/CHANGELOG.md +57 -0
  3. cnovatech_sdk_core-0.1.0/LICENSE +21 -0
  4. cnovatech_sdk_core-0.1.0/PKG-INFO +53 -0
  5. cnovatech_sdk_core-0.1.0/README.md +23 -0
  6. cnovatech_sdk_core-0.1.0/pyproject.toml +86 -0
  7. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/__init__.py +24 -0
  8. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/client.py +146 -0
  9. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/config.py +257 -0
  10. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/dataframe.py +159 -0
  11. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/errors.py +450 -0
  12. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/metadata.py +23 -0
  13. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/models.py +95 -0
  14. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/operation.py +96 -0
  15. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/pagination.py +211 -0
  16. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/pricing.py +21 -0
  17. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/py.typed +0 -0
  18. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/resource.py +71 -0
  19. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/retry.py +119 -0
  20. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/transport.py +530 -0
  21. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/usage.py +110 -0
  22. cnovatech_sdk_core-0.1.0/src/cnovatech/sdk_core/validation.py +92 -0
  23. cnovatech_sdk_core-0.1.0/tests/conftest.py +162 -0
  24. cnovatech_sdk_core-0.1.0/tests/test_client.py +128 -0
  25. cnovatech_sdk_core-0.1.0/tests/test_config.py +227 -0
  26. cnovatech_sdk_core-0.1.0/tests/test_dataframe.py +212 -0
  27. cnovatech_sdk_core-0.1.0/tests/test_errors.py +308 -0
  28. cnovatech_sdk_core-0.1.0/tests/test_pagination.py +245 -0
  29. cnovatech_sdk_core-0.1.0/tests/test_pricing.py +23 -0
  30. cnovatech_sdk_core-0.1.0/tests/test_resource.py +117 -0
  31. cnovatech_sdk_core-0.1.0/tests/test_retry.py +307 -0
  32. cnovatech_sdk_core-0.1.0/tests/test_transport.py +621 -0
  33. cnovatech_sdk_core-0.1.0/tests/test_usage.py +179 -0
  34. cnovatech_sdk_core-0.1.0/tests/test_validation.py +120 -0
@@ -0,0 +1,26 @@
1
+ # Environments and secrets
2
+ .venv/
3
+ .venv-*/
4
+ .env
5
+ .env.*
6
+ !.env.example
7
+
8
+ # Build output
9
+ dist/
10
+ build/
11
+ *.egg-info/
12
+
13
+ # Tool caches
14
+ .pytest_cache/
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ .coverage
18
+ coverage.xml
19
+ htmlcov/
20
+ __pycache__/
21
+ *.pyc
22
+
23
+ # Editors and OS
24
+ .idea/
25
+ .vscode/
26
+ .DS_Store
@@ -0,0 +1,57 @@
1
+ # Changelog
2
+
3
+ All notable changes to `cnovatech-sdk-core` are documented here. The format
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
+ project uses [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-11-06
10
+
11
+ First release, together with `cnovatech-datamarket`. Rehearsed on TestPyPI as
12
+ 0.1.0.dev1.
13
+
14
+ ### Added
15
+
16
+ - `ProductSettings` and `ClientConfig` with validation of API key (printable
17
+ ASCII only, so a key copied with an invisible character or a line break is
18
+ refused before any request), base URL (HTTPS required outside loopback),
19
+ timeouts (read timeout capped at the server's 300 s), retry count and
20
+ budget. Explicit arguments win over environment variables, which win over
21
+ product defaults.
22
+ - `Operation`: a transport-independent description of one API call, and
23
+ `Endpoint`: where a call lives, declared once per endpoint.
24
+ - `BaseSyncClient` / `BaseAsyncClient` and `SyncResource` / `AsyncResource`:
25
+ the client and resource plumbing every product SDK shares.
26
+ - `SyncTransport` and `AsyncTransport` over httpx, sharing request preparation
27
+ and response interpretation. Correlation ID per request, kept across
28
+ retries and reported on every error, including connection errors and
29
+ timeouts, even when the API does not echo it; descriptive `User-Agent`;
30
+ `None` query parameters omitted. Redirects are never followed and the
31
+ configured timeout applies per request, also through a user-supplied
32
+ `http_client`. Every `httpx.RequestError`, including a response body that
33
+ cannot be decoded, becomes an `APIConnectionError`.
34
+ - `RetryPolicy`: connection failures that happen before the request is sent,
35
+ 502/503 and the per-minute 429 are repeated with capped exponential
36
+ backoff and jitter, honouring `Retry-After`. Read timeouts, 504 (the
37
+ application may still be processing the request), connections dropped
38
+ mid-response, quota errors and other 4xx/5xx are never repeated. Each
39
+ retry is logged at INFO with structured fields.
40
+ - `max_units_per_request`: an operation whose estimated cost is above the
41
+ ceiling raises `BudgetExceededError` before anything is sent.
42
+ - Exception hierarchy rooted at `CnovatechError`, with `APIStatusError`
43
+ subclasses chosen by HTTP status and, for 429, by error code.
44
+ `APIConnectionError.retryable` says whether a repeat could bill twice.
45
+ Every exception can be pickled, so it crosses process pools intact.
46
+ - `UsageTracker` and `RateLimitState`, fed by every response; a quota error
47
+ refreshes the last known balance without counting a call.
48
+ - `APIResponse` base model (immutable, keeps unknown fields) with
49
+ `request_info`, `Record` for nested objects, plus `Usage`, `PaginationMeta`
50
+ and `ListMeta`.
51
+ - `SyncPage` / `AsyncPage` with `iter_pages` and `iter_all` capped by
52
+ `max_pages` or `max_units`; pages can be pickled, without the ability to
53
+ fetch further pages. `TabularResponse.to_dataframe()` with nested rows,
54
+ envelope columns that collide with a row field prefixed by the response's
55
+ name, and pandas imported on first use.
56
+ - Validation helpers accept `uuid.UUID` identifiers and any iterable of
57
+ strings as a batch.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cnovatech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.5
2
+ Name: cnovatech-sdk-core
3
+ Version: 0.1.0
4
+ Summary: Shared HTTP transport, configuration, errors and pagination for the Cnovatech Python SDKs.
5
+ Project-URL: Homepage, https://cnovatech.com.br
6
+ Project-URL: Repository, https://github.com/cnovatech-br/sdks
7
+ Project-URL: Changelog, https://github.com/cnovatech-br/sdks/blob/main/core/CHANGELOG.md
8
+ Author-email: Cnovatech <suporte@cnovatech.com.br>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: client,cnovatech,http,sdk
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: 3.15
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: httpx<1.0,>=0.27
26
+ Requires-Dist: pydantic<3.0,>=2.12
27
+ Provides-Extra: pandas
28
+ Requires-Dist: pandas>=2.0; extra == 'pandas'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # cnovatech-sdk-core
32
+
33
+ Shared foundation of the Cnovatech Python SDKs: configuration, HTTP transport
34
+ (sync and async), error hierarchy, pagination and quota accounting.
35
+
36
+ This package is not meant to be installed on its own. Product SDKs such as
37
+ [`cnovatech-datamarket`](https://pypi.org/project/cnovatech-datamarket/)
38
+ depend on it and re-export everything their users need. If you are
39
+ integrating with a Cnovatech API, install the product SDK. Source and issues:
40
+ <https://github.com/cnovatech-br/sdks>.
41
+
42
+ ## Boundary
43
+
44
+ The core knows nothing about any product: no endpoint, no field name, no domain
45
+ concept. CI fails if a product term appears under `core/src`. Product-specific
46
+ behaviour reaches the core only through `ProductSettings` (environment prefix,
47
+ base URL, API prefix, error overrides) and `Operation` objects built by the
48
+ product's resources.
49
+
50
+ ## Versioning
51
+
52
+ The core and the product SDKs are released together and share a version number.
53
+ A product SDK pins the core to the same minor series.
@@ -0,0 +1,23 @@
1
+ # cnovatech-sdk-core
2
+
3
+ Shared foundation of the Cnovatech Python SDKs: configuration, HTTP transport
4
+ (sync and async), error hierarchy, pagination and quota accounting.
5
+
6
+ This package is not meant to be installed on its own. Product SDKs such as
7
+ [`cnovatech-datamarket`](https://pypi.org/project/cnovatech-datamarket/)
8
+ depend on it and re-export everything their users need. If you are
9
+ integrating with a Cnovatech API, install the product SDK. Source and issues:
10
+ <https://github.com/cnovatech-br/sdks>.
11
+
12
+ ## Boundary
13
+
14
+ The core knows nothing about any product: no endpoint, no field name, no domain
15
+ concept. CI fails if a product term appears under `core/src`. Product-specific
16
+ behaviour reaches the core only through `ProductSettings` (environment prefix,
17
+ base URL, API prefix, error overrides) and `Operation` objects built by the
18
+ product's resources.
19
+
20
+ ## Versioning
21
+
22
+ The core and the product SDKs are released together and share a version number.
23
+ A product SDK pins the core to the same minor series.
@@ -0,0 +1,86 @@
1
+ [project]
2
+ name = "cnovatech-sdk-core"
3
+ version = "0.1.0"
4
+ description = "Shared HTTP transport, configuration, errors and pagination for the Cnovatech Python SDKs."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Cnovatech", email = "suporte@cnovatech.com.br" }]
10
+ keywords = ["cnovatech", "sdk", "http", "client"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Intended Audience :: Developers",
14
+ "Operating System :: OS Independent",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Programming Language :: Python :: 3.15",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ "Typing :: Typed",
24
+ ]
25
+ # pydantic 2.12 is the first release with wheels for Python 3.14.
26
+ dependencies = [
27
+ "httpx>=0.27,<1.0",
28
+ "pydantic>=2.12,<3.0",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ pandas = ["pandas>=2.0"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://cnovatech.com.br"
36
+ Repository = "https://github.com/cnovatech-br/sdks"
37
+ Changelog = "https://github.com/cnovatech-br/sdks/blob/main/core/CHANGELOG.md"
38
+
39
+ [build-system]
40
+ requires = ["hatchling>=1.27,<2"]
41
+ build-backend = "hatchling.build"
42
+
43
+ # `src/cnovatech/` has no __init__.py on purpose: it is a PEP 420 namespace
44
+ # shared with the product SDKs (`cnovatech.datamarket`, ...).
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/cnovatech"]
47
+
48
+ [tool.hatch.build.targets.sdist]
49
+ include = ["src", "tests", "README.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
50
+
51
+ [tool.mypy]
52
+ python_version = "3.10"
53
+ files = ["src", "tests"]
54
+ mypy_path = "src:tests"
55
+ strict = true
56
+ warn_unreachable = true
57
+ namespace_packages = true
58
+ explicit_package_bases = true
59
+ plugins = ["pydantic.mypy"]
60
+
61
+ [tool.pydantic-mypy]
62
+ init_forbid_extra = true
63
+ init_typed = true
64
+ warn_required_dynamic_aliases = true
65
+
66
+ [tool.pytest.ini_options]
67
+ testpaths = ["tests"]
68
+ asyncio_mode = "auto"
69
+ asyncio_default_fixture_loop_scope = "function"
70
+ addopts = "-q --strict-markers --strict-config"
71
+ filterwarnings = ["error"]
72
+
73
+ [tool.coverage.run]
74
+ source = ["src/cnovatech/sdk_core"]
75
+ branch = true
76
+
77
+ [tool.coverage.report]
78
+ fail_under = 90
79
+ show_missing = true
80
+ exclude_also = [
81
+ "if TYPE_CHECKING:",
82
+ "if sys.version_info",
83
+ "@overload",
84
+ "raise NotImplementedError",
85
+ "\\.\\.\\.$",
86
+ ]
@@ -0,0 +1,24 @@
1
+ """Shared foundation of the Cnovatech Python SDKs.
2
+
3
+ This package is not used directly by application code. The product SDKs build
4
+ on its modules (``config``, ``transport``, ``client``, ``resource``, ``models``,
5
+ ``pagination``, ``errors``, ...) and re-export what their users need. It knows
6
+ nothing about any product: no endpoint, field name or domain concept lives
7
+ here, and CI enforces that boundary.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+
14
+ from .config import CORE_DISTRIBUTION_NAME
15
+ from .metadata import read_distribution_version
16
+
17
+ __version__ = read_distribution_version(CORE_DISTRIBUTION_NAME)
18
+
19
+ # Libraries log but never configure logging. The null handler keeps Python from
20
+ # printing "No handlers could be found" when the application has not set up
21
+ # logging; the application routes the `cnovatech` logger wherever it wants.
22
+ logging.getLogger("cnovatech").addHandler(logging.NullHandler())
23
+
24
+ __all__ = ["__version__"]
@@ -0,0 +1,146 @@
1
+ """What every product client shares: configuration, transport, accounting and lifecycle.
2
+
3
+ A product SDK subclasses :class:`BaseSyncClient` and :class:`BaseAsyncClient`,
4
+ binds its :class:`ProductSettings` and attaches its resources. Everything
5
+ else, from reading the environment to closing the connection pool, is here.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ from .config import DEFAULT_MAX_RETRIES, DEFAULT_READ_TIMEOUT_SECONDS, build_client_config
13
+ from .transport import AsyncTransport, SyncTransport
14
+
15
+ if TYPE_CHECKING:
16
+ from types import TracebackType
17
+
18
+ import httpx
19
+ from typing_extensions import Self
20
+
21
+ from .config import ProductSettings
22
+ from .usage import RateLimitState, UsageTracker
23
+
24
+
25
+ class BaseSyncClient:
26
+ """A client using blocking I/O. See the product client for the constructor arguments."""
27
+
28
+ def __init__(
29
+ self,
30
+ product: ProductSettings,
31
+ api_key: str | None = None,
32
+ *,
33
+ base_url: str | None = None,
34
+ timeout: float | httpx.Timeout = DEFAULT_READ_TIMEOUT_SECONDS,
35
+ max_retries: int = DEFAULT_MAX_RETRIES,
36
+ max_units_per_request: int | None = None,
37
+ http_client: httpx.Client | None = None,
38
+ transport: httpx.BaseTransport | None = None,
39
+ ) -> None:
40
+ self._config = build_client_config(
41
+ product,
42
+ api_key=api_key,
43
+ base_url=base_url,
44
+ timeout=timeout,
45
+ max_retries=max_retries,
46
+ max_units_per_request=max_units_per_request,
47
+ )
48
+ self._transport = SyncTransport(self._config, http_client=http_client, transport=transport)
49
+
50
+ @property
51
+ def base_url(self) -> str:
52
+ """Origin the client talks to."""
53
+ return self._config.base_url
54
+
55
+ @property
56
+ def usage(self) -> UsageTracker:
57
+ """Quota spent through this client: ``spent``, ``calls`` and the ``last`` usage block."""
58
+ return self._transport.usage
59
+
60
+ @property
61
+ def rate_limit(self) -> RateLimitState:
62
+ """The per-minute rate limit as last reported by the API."""
63
+ return self._transport.rate_limit
64
+
65
+ def close(self) -> None:
66
+ """Release the connection pool. Clients supplied through ``http_client`` are left open."""
67
+ self._transport.close()
68
+
69
+ def __enter__(self) -> Self:
70
+ return self
71
+
72
+ def __exit__(
73
+ self,
74
+ exc_type: type[BaseException] | None,
75
+ exc: BaseException | None,
76
+ traceback: TracebackType | None,
77
+ ) -> None:
78
+ self.close()
79
+
80
+ def __repr__(self) -> str:
81
+ return (
82
+ f"{type(self).__name__}(base_url={self._config.base_url!r}, "
83
+ f"api_key={self._config.masked_api_key!r})"
84
+ )
85
+
86
+
87
+ class BaseAsyncClient:
88
+ """A client using asyncio. See the product client for the constructor arguments."""
89
+
90
+ def __init__(
91
+ self,
92
+ product: ProductSettings,
93
+ api_key: str | None = None,
94
+ *,
95
+ base_url: str | None = None,
96
+ timeout: float | httpx.Timeout = DEFAULT_READ_TIMEOUT_SECONDS,
97
+ max_retries: int = DEFAULT_MAX_RETRIES,
98
+ max_units_per_request: int | None = None,
99
+ http_client: httpx.AsyncClient | None = None,
100
+ transport: httpx.AsyncBaseTransport | None = None,
101
+ ) -> None:
102
+ self._config = build_client_config(
103
+ product,
104
+ api_key=api_key,
105
+ base_url=base_url,
106
+ timeout=timeout,
107
+ max_retries=max_retries,
108
+ max_units_per_request=max_units_per_request,
109
+ )
110
+ self._transport = AsyncTransport(self._config, http_client=http_client, transport=transport)
111
+
112
+ @property
113
+ def base_url(self) -> str:
114
+ """Origin the client talks to."""
115
+ return self._config.base_url
116
+
117
+ @property
118
+ def usage(self) -> UsageTracker:
119
+ """Quota spent through this client: ``spent``, ``calls`` and the ``last`` usage block."""
120
+ return self._transport.usage
121
+
122
+ @property
123
+ def rate_limit(self) -> RateLimitState:
124
+ """The per-minute rate limit as last reported by the API."""
125
+ return self._transport.rate_limit
126
+
127
+ async def aclose(self) -> None:
128
+ """Release the connection pool. Clients supplied through ``http_client`` are left open."""
129
+ await self._transport.aclose()
130
+
131
+ async def __aenter__(self) -> Self:
132
+ return self
133
+
134
+ async def __aexit__(
135
+ self,
136
+ exc_type: type[BaseException] | None,
137
+ exc: BaseException | None,
138
+ traceback: TracebackType | None,
139
+ ) -> None:
140
+ await self.aclose()
141
+
142
+ def __repr__(self) -> str:
143
+ return (
144
+ f"{type(self).__name__}(base_url={self._config.base_url!r}, "
145
+ f"api_key={self._config.masked_api_key!r})"
146
+ )