teamver-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.
- teamver_sdk_core-0.1.0/.gitignore +50 -0
- teamver_sdk_core-0.1.0/CHANGELOG.md +21 -0
- teamver_sdk_core-0.1.0/LICENSE +21 -0
- teamver_sdk_core-0.1.0/PKG-INFO +112 -0
- teamver_sdk_core-0.1.0/README.md +82 -0
- teamver_sdk_core-0.1.0/pyproject.toml +66 -0
- teamver_sdk_core-0.1.0/teamver_sdk_core/__init__.py +88 -0
- teamver_sdk_core-0.1.0/teamver_sdk_core/config.py +25 -0
- teamver_sdk_core-0.1.0/teamver_sdk_core/context.py +52 -0
- teamver_sdk_core-0.1.0/teamver_sdk_core/errors.py +161 -0
- teamver_sdk_core-0.1.0/teamver_sdk_core/masking.py +49 -0
- teamver_sdk_core-0.1.0/teamver_sdk_core/pagination.py +52 -0
- teamver_sdk_core-0.1.0/teamver_sdk_core/retry.py +54 -0
- teamver_sdk_core-0.1.0/teamver_sdk_core/transport.py +159 -0
- teamver_sdk_core-0.1.0/teamver_sdk_core/typing.py +18 -0
- teamver_sdk_core-0.1.0/tests/test_context.py +45 -0
- teamver_sdk_core-0.1.0/tests/test_errors.py +92 -0
- teamver_sdk_core-0.1.0/tests/test_masking.py +37 -0
- teamver_sdk_core-0.1.0/tests/test_pagination.py +52 -0
- teamver_sdk_core-0.1.0/tests/test_retry.py +52 -0
- teamver_sdk_core-0.1.0/tests/test_transport.py +140 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
/node_modules
|
|
5
|
+
node_modules/
|
|
6
|
+
/.pnp
|
|
7
|
+
.pnp.js
|
|
8
|
+
|
|
9
|
+
# testing
|
|
10
|
+
/coverage
|
|
11
|
+
|
|
12
|
+
# next.js
|
|
13
|
+
/.next/
|
|
14
|
+
.next/
|
|
15
|
+
/out/
|
|
16
|
+
out/
|
|
17
|
+
|
|
18
|
+
# production
|
|
19
|
+
/build
|
|
20
|
+
|
|
21
|
+
# misc
|
|
22
|
+
.DS_Store
|
|
23
|
+
*.pem
|
|
24
|
+
|
|
25
|
+
# debug
|
|
26
|
+
npm-debug.log*
|
|
27
|
+
yarn-debug.log*
|
|
28
|
+
yarn-error.log*
|
|
29
|
+
|
|
30
|
+
# local env files
|
|
31
|
+
.env*.local
|
|
32
|
+
.env
|
|
33
|
+
|
|
34
|
+
# vercel
|
|
35
|
+
.vercel
|
|
36
|
+
|
|
37
|
+
# typescript
|
|
38
|
+
*.tsbuildinfo
|
|
39
|
+
next-env.d.ts
|
|
40
|
+
dist/
|
|
41
|
+
|
|
42
|
+
# vite build output
|
|
43
|
+
examples/react-ai-app/dist/
|
|
44
|
+
|
|
45
|
+
# python
|
|
46
|
+
__pycache__/
|
|
47
|
+
*.py[cod]
|
|
48
|
+
.venv/
|
|
49
|
+
*.egg-info/
|
|
50
|
+
.pytest_cache/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog — teamver-sdk-core
|
|
2
|
+
|
|
3
|
+
All notable changes to this package are documented here.
|
|
4
|
+
Format loosely follows [Keep a Changelog](https://keepachangelog.com/).
|
|
5
|
+
|
|
6
|
+
## [0.1.0] - 2026-07-23
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- Initial greenfield core shared by all Teamver SDKs (16-1 §4, §13).
|
|
10
|
+
- `TeamverAsyncTransport`: httpx-based async transport with retry, error
|
|
11
|
+
normalization, and request-context header propagation.
|
|
12
|
+
- Unified exception tree rooted at `TeamverSDKError`
|
|
13
|
+
(`Authentication`/`Authorization`/`NotFound`/`Conflict`/`RateLimit`/
|
|
14
|
+
`TemporaryUnavailable`/`Transport` + idempotency/version conflict subtypes).
|
|
15
|
+
- `error_for_status` / `parse_error_body` normalize Main `error`, FastAPI
|
|
16
|
+
`detail`, and legacy shapes into one model while preserving status/code/request_id.
|
|
17
|
+
- `RetryPolicy` with idempotency-aware retry (non-idempotent POST retried only
|
|
18
|
+
when an `Idempotency-Key` is supplied).
|
|
19
|
+
- `RequestContext` for `request_id` / `correlation_id` propagation.
|
|
20
|
+
- `iterate_pages` cursor pagination helper and `extract_page`.
|
|
21
|
+
- `mask_secrets` / `mask_headers` for token/Bearer/internal-key redaction.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Neural Studio
|
|
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,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: teamver-sdk-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared core for Teamver SDKs: async transport, unified errors, retry, context, pagination, masking
|
|
5
|
+
Project-URL: Homepage, https://teamver.com
|
|
6
|
+
Project-URL: Documentation, https://github.com/NeuralStudioKr/ns-teamver-packages/tree/main/packages/python/teamver-sdk-core-python
|
|
7
|
+
Project-URL: Repository, https://github.com/NeuralStudioKr/ns-teamver-packages
|
|
8
|
+
Project-URL: Issues, https://github.com/NeuralStudioKr/ns-teamver-packages/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/NeuralStudioKr/ns-teamver-packages/blob/main/packages/python/teamver-sdk-core-python/CHANGELOG.md
|
|
10
|
+
Author-email: Teamver <dev@teamver.com>
|
|
11
|
+
Maintainer-email: Teamver <dev@teamver.com>
|
|
12
|
+
License: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: async,httpx,retry,sdk,teamver,transport
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: httpx<0.28,>=0.27.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# teamver-sdk-core
|
|
32
|
+
|
|
33
|
+
Shared **async HTTP core** for Teamver Python SDKs: transport, unified errors, retry,
|
|
34
|
+
request context, pagination, and secret masking.
|
|
35
|
+
|
|
36
|
+
Used by `teamver-mail-agent`, `teamver-agent-sdk`, and other Teamver packages.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install teamver-sdk-core
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Python **≥ 3.11**.
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import asyncio
|
|
50
|
+
from teamver_sdk_core import (
|
|
51
|
+
TeamverAsyncTransport,
|
|
52
|
+
TransportConfig,
|
|
53
|
+
RequestContext,
|
|
54
|
+
TeamverSDKError,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
async def main():
|
|
58
|
+
config = TransportConfig(base_url="https://api.teamver.com", token="tv_ak_…")
|
|
59
|
+
async with TeamverAsyncTransport(config) as transport:
|
|
60
|
+
ctx = RequestContext(workspace_id="WS-…")
|
|
61
|
+
try:
|
|
62
|
+
resp = await transport.request("GET", "/api/v2/collab/channels", context=ctx)
|
|
63
|
+
print(resp.json)
|
|
64
|
+
except TeamverSDKError as exc:
|
|
65
|
+
print(exc.code, exc.status_code, exc.request_id)
|
|
66
|
+
|
|
67
|
+
asyncio.run(main())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Features
|
|
71
|
+
|
|
72
|
+
| Module | Role |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `transport` | `TeamverAsyncTransport` — httpx async requests, retry, error normalization |
|
|
75
|
+
| `errors` | `TeamverSDKError` tree + `error_for_status` / `parse_error_body` |
|
|
76
|
+
| `retry` | `RetryPolicy` — safe retries (non-idempotent POST needs `idempotency_key`) |
|
|
77
|
+
| `context` | `RequestContext` — `request_id` / `correlation_id` |
|
|
78
|
+
| `pagination` | cursor `iterate_pages` |
|
|
79
|
+
| `masking` | mask tokens / Bearer / internal keys in logs |
|
|
80
|
+
|
|
81
|
+
## Error tree
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
TeamverSDKError
|
|
85
|
+
├─ ConfigurationError
|
|
86
|
+
├─ AuthenticationError (401)
|
|
87
|
+
├─ AuthorizationError (403)
|
|
88
|
+
├─ NotFoundError (404)
|
|
89
|
+
├─ ConflictError (409)
|
|
90
|
+
│ ├─ IdempotencyConflictError
|
|
91
|
+
│ └─ VersionConflictError
|
|
92
|
+
├─ RateLimitError (429)
|
|
93
|
+
├─ TemporaryUnavailableError (502/503/504)
|
|
94
|
+
└─ TransportError
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Retry rules
|
|
98
|
+
|
|
99
|
+
- Retried: transport failure, `429`, `502`, `503`, `504`
|
|
100
|
+
- `GET/HEAD/PUT/DELETE` — treated as idempotent
|
|
101
|
+
- `POST/PATCH` — retried only when `idempotency_key` is set
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pip install -e ".[dev]"
|
|
107
|
+
pytest
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# teamver-sdk-core
|
|
2
|
+
|
|
3
|
+
Shared **async HTTP core** for Teamver Python SDKs: transport, unified errors, retry,
|
|
4
|
+
request context, pagination, and secret masking.
|
|
5
|
+
|
|
6
|
+
Used by `teamver-mail-agent`, `teamver-agent-sdk`, and other Teamver packages.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install teamver-sdk-core
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Python **≥ 3.11**.
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import asyncio
|
|
20
|
+
from teamver_sdk_core import (
|
|
21
|
+
TeamverAsyncTransport,
|
|
22
|
+
TransportConfig,
|
|
23
|
+
RequestContext,
|
|
24
|
+
TeamverSDKError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
async def main():
|
|
28
|
+
config = TransportConfig(base_url="https://api.teamver.com", token="tv_ak_…")
|
|
29
|
+
async with TeamverAsyncTransport(config) as transport:
|
|
30
|
+
ctx = RequestContext(workspace_id="WS-…")
|
|
31
|
+
try:
|
|
32
|
+
resp = await transport.request("GET", "/api/v2/collab/channels", context=ctx)
|
|
33
|
+
print(resp.json)
|
|
34
|
+
except TeamverSDKError as exc:
|
|
35
|
+
print(exc.code, exc.status_code, exc.request_id)
|
|
36
|
+
|
|
37
|
+
asyncio.run(main())
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
| Module | Role |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `transport` | `TeamverAsyncTransport` — httpx async requests, retry, error normalization |
|
|
45
|
+
| `errors` | `TeamverSDKError` tree + `error_for_status` / `parse_error_body` |
|
|
46
|
+
| `retry` | `RetryPolicy` — safe retries (non-idempotent POST needs `idempotency_key`) |
|
|
47
|
+
| `context` | `RequestContext` — `request_id` / `correlation_id` |
|
|
48
|
+
| `pagination` | cursor `iterate_pages` |
|
|
49
|
+
| `masking` | mask tokens / Bearer / internal keys in logs |
|
|
50
|
+
|
|
51
|
+
## Error tree
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
TeamverSDKError
|
|
55
|
+
├─ ConfigurationError
|
|
56
|
+
├─ AuthenticationError (401)
|
|
57
|
+
├─ AuthorizationError (403)
|
|
58
|
+
├─ NotFoundError (404)
|
|
59
|
+
├─ ConflictError (409)
|
|
60
|
+
│ ├─ IdempotencyConflictError
|
|
61
|
+
│ └─ VersionConflictError
|
|
62
|
+
├─ RateLimitError (429)
|
|
63
|
+
├─ TemporaryUnavailableError (502/503/504)
|
|
64
|
+
└─ TransportError
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Retry rules
|
|
68
|
+
|
|
69
|
+
- Retried: transport failure, `429`, `502`, `503`, `504`
|
|
70
|
+
- `GET/HEAD/PUT/DELETE` — treated as idempotent
|
|
71
|
+
- `POST/PATCH` — retried only when `idempotency_key` is set
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install -e ".[dev]"
|
|
77
|
+
pytest
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "teamver-sdk-core"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Shared core for Teamver SDKs: async transport, unified errors, retry, context, pagination, masking"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Teamver", email = "dev@teamver.com" },
|
|
10
|
+
]
|
|
11
|
+
maintainers = [
|
|
12
|
+
{ name = "Teamver", email = "dev@teamver.com" },
|
|
13
|
+
]
|
|
14
|
+
keywords = [
|
|
15
|
+
"teamver",
|
|
16
|
+
"sdk",
|
|
17
|
+
"httpx",
|
|
18
|
+
"async",
|
|
19
|
+
"transport",
|
|
20
|
+
"retry",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 4 - Beta",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Typing :: Typed",
|
|
32
|
+
]
|
|
33
|
+
dependencies = [
|
|
34
|
+
"httpx>=0.27.0,<0.28",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = ["pytest>=8.0", "pytest-asyncio>=0.24"]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://teamver.com"
|
|
42
|
+
Documentation = "https://github.com/NeuralStudioKr/ns-teamver-packages/tree/main/packages/python/teamver-sdk-core-python"
|
|
43
|
+
Repository = "https://github.com/NeuralStudioKr/ns-teamver-packages"
|
|
44
|
+
Issues = "https://github.com/NeuralStudioKr/ns-teamver-packages/issues"
|
|
45
|
+
Changelog = "https://github.com/NeuralStudioKr/ns-teamver-packages/blob/main/packages/python/teamver-sdk-core-python/CHANGELOG.md"
|
|
46
|
+
|
|
47
|
+
[build-system]
|
|
48
|
+
requires = ["hatchling"]
|
|
49
|
+
build-backend = "hatchling.build"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["teamver_sdk_core"]
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.sdist]
|
|
55
|
+
include = [
|
|
56
|
+
"/teamver_sdk_core",
|
|
57
|
+
"/tests",
|
|
58
|
+
"/README.md",
|
|
59
|
+
"/CHANGELOG.md",
|
|
60
|
+
"/LICENSE",
|
|
61
|
+
"/pyproject.toml",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
asyncio_mode = "auto"
|
|
66
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""teamver-sdk-core — Teamver SDK 공통 코어 (16-1 SDK 재설계 §4, §13).
|
|
2
|
+
|
|
3
|
+
하위 SDK(be-sdk / mail-agent / agent-sdk / apps-auth)가 공유하는 building block:
|
|
4
|
+
|
|
5
|
+
- ``TeamverAsyncTransport`` : httpx 기반 async 요청 + 재시도 + 오류 정규화 + context 전파
|
|
6
|
+
- 통합 예외 트리(``TeamverSDKError`` 이하)
|
|
7
|
+
- ``RequestContext`` : request_id / correlation_id 전파
|
|
8
|
+
- ``RetryPolicy`` : 멱등 안전 재시도 정책
|
|
9
|
+
- pagination / masking 유틸
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from teamver_sdk_core.config import TransportConfig
|
|
13
|
+
from teamver_sdk_core.context import (
|
|
14
|
+
HEADER_CORRELATION_ID,
|
|
15
|
+
HEADER_IDEMPOTENCY_KEY,
|
|
16
|
+
HEADER_REQUEST_ID,
|
|
17
|
+
HEADER_WORKSPACE_ID,
|
|
18
|
+
RequestContext,
|
|
19
|
+
new_correlation_id,
|
|
20
|
+
new_request_id,
|
|
21
|
+
)
|
|
22
|
+
from teamver_sdk_core.errors import (
|
|
23
|
+
AuthenticationError,
|
|
24
|
+
AuthorizationError,
|
|
25
|
+
ConfigurationError,
|
|
26
|
+
ConflictError,
|
|
27
|
+
IdempotencyConflictError,
|
|
28
|
+
NotFoundError,
|
|
29
|
+
RateLimitError,
|
|
30
|
+
TeamverSDKError,
|
|
31
|
+
TemporaryUnavailableError,
|
|
32
|
+
TransportError,
|
|
33
|
+
VersionConflictError,
|
|
34
|
+
error_for_status,
|
|
35
|
+
parse_error_body,
|
|
36
|
+
)
|
|
37
|
+
from teamver_sdk_core.masking import mask_headers, mask_secrets
|
|
38
|
+
from teamver_sdk_core.pagination import extract_page, iterate_pages
|
|
39
|
+
from teamver_sdk_core.retry import (
|
|
40
|
+
RETRYABLE_STATUS,
|
|
41
|
+
RetryPolicy,
|
|
42
|
+
is_retryable_request,
|
|
43
|
+
should_retry,
|
|
44
|
+
)
|
|
45
|
+
from teamver_sdk_core.transport import TeamverAsyncTransport
|
|
46
|
+
from teamver_sdk_core.typing import TeamverResponse
|
|
47
|
+
|
|
48
|
+
__version__ = "0.1.0"
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"__version__",
|
|
52
|
+
# transport / config
|
|
53
|
+
"TeamverAsyncTransport",
|
|
54
|
+
"TransportConfig",
|
|
55
|
+
"TeamverResponse",
|
|
56
|
+
# context
|
|
57
|
+
"RequestContext",
|
|
58
|
+
"new_request_id",
|
|
59
|
+
"new_correlation_id",
|
|
60
|
+
"HEADER_REQUEST_ID",
|
|
61
|
+
"HEADER_CORRELATION_ID",
|
|
62
|
+
"HEADER_WORKSPACE_ID",
|
|
63
|
+
"HEADER_IDEMPOTENCY_KEY",
|
|
64
|
+
# retry
|
|
65
|
+
"RetryPolicy",
|
|
66
|
+
"RETRYABLE_STATUS",
|
|
67
|
+
"should_retry",
|
|
68
|
+
"is_retryable_request",
|
|
69
|
+
# errors
|
|
70
|
+
"TeamverSDKError",
|
|
71
|
+
"ConfigurationError",
|
|
72
|
+
"AuthenticationError",
|
|
73
|
+
"AuthorizationError",
|
|
74
|
+
"NotFoundError",
|
|
75
|
+
"ConflictError",
|
|
76
|
+
"IdempotencyConflictError",
|
|
77
|
+
"VersionConflictError",
|
|
78
|
+
"RateLimitError",
|
|
79
|
+
"TemporaryUnavailableError",
|
|
80
|
+
"TransportError",
|
|
81
|
+
"error_for_status",
|
|
82
|
+
"parse_error_body",
|
|
83
|
+
# pagination / masking
|
|
84
|
+
"iterate_pages",
|
|
85
|
+
"extract_page",
|
|
86
|
+
"mask_secrets",
|
|
87
|
+
"mask_headers",
|
|
88
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Transport configuration (16-1 §4 config)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from teamver_sdk_core.retry import RetryPolicy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class TransportConfig:
|
|
12
|
+
base_url: str
|
|
13
|
+
token: str | None = None
|
|
14
|
+
# Authorization scheme; agent tokens use Bearer.
|
|
15
|
+
auth_scheme: str = "Bearer"
|
|
16
|
+
timeout_seconds: float = 30.0
|
|
17
|
+
retry_policy: RetryPolicy = field(default_factory=RetryPolicy)
|
|
18
|
+
# Extra static headers (e.g. X-Teamver-Internal-Api-Key for M2M).
|
|
19
|
+
default_headers: dict[str, str] = field(default_factory=dict)
|
|
20
|
+
user_agent: str = "teamver-sdk-core/0.1.0"
|
|
21
|
+
|
|
22
|
+
def auth_header(self) -> dict[str, str]:
|
|
23
|
+
if not self.token:
|
|
24
|
+
return {}
|
|
25
|
+
return {"Authorization": f"{self.auth_scheme} {self.token}".strip()}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Request context propagation (16-1 §4 Context; 15-1 §01 §6).
|
|
2
|
+
|
|
3
|
+
SDK 가 ``request_id`` 를 생성하고 서비스가 downstream 으로 전달한다. 동일 correlation_id
|
|
4
|
+
로 Job/Channel/Mail/VM 을 교차 추적한다(§12 §1).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
HEADER_REQUEST_ID = "X-Teamver-Request-Id"
|
|
14
|
+
HEADER_CORRELATION_ID = "X-Teamver-Correlation-Id"
|
|
15
|
+
HEADER_WORKSPACE_ID = "X-Workspace-Id"
|
|
16
|
+
HEADER_IDEMPOTENCY_KEY = "Idempotency-Key"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def new_request_id() -> str:
|
|
20
|
+
return f"req_{uuid.uuid4().hex}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def new_correlation_id() -> str:
|
|
24
|
+
return f"corr_{uuid.uuid4().hex}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class RequestContext:
|
|
29
|
+
request_id: str = field(default_factory=new_request_id)
|
|
30
|
+
correlation_id: Optional[str] = None
|
|
31
|
+
workspace_id: Optional[str] = None
|
|
32
|
+
agent_id: Optional[str] = None
|
|
33
|
+
job_id: Optional[str] = None
|
|
34
|
+
|
|
35
|
+
def to_headers(self) -> dict[str, str]:
|
|
36
|
+
"""컨텍스트 → HTTP 헤더(값이 있는 것만)."""
|
|
37
|
+
headers: dict[str, str] = {HEADER_REQUEST_ID: self.request_id}
|
|
38
|
+
if self.correlation_id:
|
|
39
|
+
headers[HEADER_CORRELATION_ID] = self.correlation_id
|
|
40
|
+
if self.workspace_id:
|
|
41
|
+
headers[HEADER_WORKSPACE_ID] = self.workspace_id
|
|
42
|
+
return headers
|
|
43
|
+
|
|
44
|
+
def child(self) -> "RequestContext":
|
|
45
|
+
"""새 request_id, 동일 correlation_id 로 하위 요청 컨텍스트 생성."""
|
|
46
|
+
return RequestContext(
|
|
47
|
+
request_id=new_request_id(),
|
|
48
|
+
correlation_id=self.correlation_id or self.request_id,
|
|
49
|
+
workspace_id=self.workspace_id,
|
|
50
|
+
agent_id=self.agent_id,
|
|
51
|
+
job_id=self.job_id,
|
|
52
|
+
)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Unified SDK exception hierarchy (16-1 SDK 재설계 §13).
|
|
2
|
+
|
|
3
|
+
모든 하위 SDK(be-sdk / mail-agent / agent-sdk / apps-auth)는 원본 오류(Main ``error``,
|
|
4
|
+
FastAPI ``detail``, legacy shape)를 아래 단일 트리로 **정규화**한다. 원본 status/code/
|
|
5
|
+
request_id 를 보존한다(§13 "원본 status, code, request ID를 유지한다").
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
TeamverSDKError
|
|
9
|
+
├─ ConfigurationError
|
|
10
|
+
├─ AuthenticationError (401)
|
|
11
|
+
├─ AuthorizationError (403)
|
|
12
|
+
├─ NotFoundError (404)
|
|
13
|
+
├─ ConflictError (409)
|
|
14
|
+
│ ├─ IdempotencyConflictError
|
|
15
|
+
│ └─ VersionConflictError
|
|
16
|
+
├─ RateLimitError (429)
|
|
17
|
+
├─ TemporaryUnavailableError (502/503/504)
|
|
18
|
+
└─ TransportError (네트워크/타임아웃)
|
|
19
|
+
```
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any, Optional
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TeamverSDKError(Exception):
|
|
28
|
+
"""모든 SDK 예외의 루트. 원본 진단 정보를 보존한다."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
message: str,
|
|
33
|
+
*,
|
|
34
|
+
code: Optional[str] = None,
|
|
35
|
+
status_code: Optional[int] = None,
|
|
36
|
+
request_id: Optional[str] = None,
|
|
37
|
+
retryable: bool = False,
|
|
38
|
+
details: Optional[dict[str, Any]] = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
super().__init__(message)
|
|
41
|
+
self.message = message
|
|
42
|
+
self.code = code
|
|
43
|
+
self.status_code = status_code
|
|
44
|
+
self.request_id = request_id
|
|
45
|
+
self.retryable = retryable
|
|
46
|
+
self.details = details or {}
|
|
47
|
+
|
|
48
|
+
def __repr__(self) -> str: # secret 노출 방지: message 만 요약.
|
|
49
|
+
return (
|
|
50
|
+
f"{type(self).__name__}(code={self.code!r}, status={self.status_code}, "
|
|
51
|
+
f"request_id={self.request_id!r})"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ConfigurationError(TeamverSDKError):
|
|
56
|
+
"""필수 설정 누락(토큰/base URL 등)."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AuthenticationError(TeamverSDKError):
|
|
60
|
+
"""401 — 자격증명 오류(재시도 금지)."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AuthorizationError(TeamverSDKError):
|
|
64
|
+
"""403 — 권한/정책 오류(재시도 금지)."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class NotFoundError(TeamverSDKError):
|
|
68
|
+
"""404."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ConflictError(TeamverSDKError):
|
|
72
|
+
"""409 계열 기본."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class IdempotencyConflictError(ConflictError):
|
|
76
|
+
"""동일 idempotency key, 다른 body."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class VersionConflictError(ConflictError):
|
|
80
|
+
"""optimistic lock 버전 충돌."""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class RateLimitError(TeamverSDKError):
|
|
84
|
+
"""429 — backoff 재시도 대상."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, *args: Any, retry_after: Optional[float] = None, **kwargs: Any) -> None:
|
|
87
|
+
kwargs.setdefault("retryable", True)
|
|
88
|
+
super().__init__(*args, **kwargs)
|
|
89
|
+
self.retry_after = retry_after
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TemporaryUnavailableError(TeamverSDKError):
|
|
93
|
+
"""502/503/504 — 제한적 재시도 대상."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
96
|
+
kwargs.setdefault("retryable", True)
|
|
97
|
+
super().__init__(*args, **kwargs)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TransportError(TeamverSDKError):
|
|
101
|
+
"""네트워크 실패/타임아웃 — 재시도 대상."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
104
|
+
kwargs.setdefault("retryable", True)
|
|
105
|
+
super().__init__(*args, **kwargs)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# HTTP status → 예외 클래스 매핑(409 은 code 로 세분).
|
|
109
|
+
def error_for_status(
|
|
110
|
+
status_code: int,
|
|
111
|
+
*,
|
|
112
|
+
message: str,
|
|
113
|
+
code: Optional[str] = None,
|
|
114
|
+
request_id: Optional[str] = None,
|
|
115
|
+
details: Optional[dict[str, Any]] = None,
|
|
116
|
+
retry_after: Optional[float] = None,
|
|
117
|
+
) -> TeamverSDKError:
|
|
118
|
+
"""정규화된 오류 payload → 알맞은 예외 인스턴스."""
|
|
119
|
+
common = dict(code=code, status_code=status_code, request_id=request_id, details=details)
|
|
120
|
+
if status_code == 401:
|
|
121
|
+
return AuthenticationError(message, **common)
|
|
122
|
+
if status_code == 403:
|
|
123
|
+
return AuthorizationError(message, **common)
|
|
124
|
+
if status_code == 404:
|
|
125
|
+
return NotFoundError(message, **common)
|
|
126
|
+
if status_code == 409:
|
|
127
|
+
if code == "idempotency_conflict":
|
|
128
|
+
return IdempotencyConflictError(message, **common)
|
|
129
|
+
if code in ("version_conflict", "job_version_conflict"):
|
|
130
|
+
return VersionConflictError(message, **common)
|
|
131
|
+
return ConflictError(message, **common)
|
|
132
|
+
if status_code == 429:
|
|
133
|
+
return RateLimitError(message, retry_after=retry_after, **common)
|
|
134
|
+
if status_code in (502, 503, 504):
|
|
135
|
+
return TemporaryUnavailableError(message, **common)
|
|
136
|
+
return TeamverSDKError(message, **common)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def parse_error_body(body: Any) -> tuple[Optional[str], str, dict[str, Any]]:
|
|
140
|
+
"""Main ``error`` / FastAPI ``detail`` / legacy shape → (code, message, details).
|
|
141
|
+
|
|
142
|
+
16-1 §3 "오류 모델 불일치": Main 오류가 단일 schema 라고 가정하지 않는다.
|
|
143
|
+
"""
|
|
144
|
+
if isinstance(body, dict):
|
|
145
|
+
err = body.get("error")
|
|
146
|
+
if isinstance(err, dict):
|
|
147
|
+
return (
|
|
148
|
+
err.get("code"),
|
|
149
|
+
str(err.get("message") or err.get("code") or "request failed"),
|
|
150
|
+
err.get("details") or {},
|
|
151
|
+
)
|
|
152
|
+
detail = body.get("detail")
|
|
153
|
+
if isinstance(detail, str):
|
|
154
|
+
return (None, detail, {})
|
|
155
|
+
if isinstance(detail, dict):
|
|
156
|
+
return (detail.get("code"), str(detail.get("message") or "request failed"), detail)
|
|
157
|
+
if isinstance(detail, list): # FastAPI validation errors
|
|
158
|
+
return ("validation_error", "request validation failed", {"errors": detail})
|
|
159
|
+
if body.get("message"):
|
|
160
|
+
return (body.get("code"), str(body["message"]), body)
|
|
161
|
+
return (None, "request failed", {} if not isinstance(body, dict) else body)
|