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.
@@ -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)