vynco 2.0.0__py3-none-any.whl

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 (50) hide show
  1. vynco/__init__.py +166 -0
  2. vynco/_base_client.py +189 -0
  3. vynco/_client.py +267 -0
  4. vynco/_constants.py +7 -0
  5. vynco/_errors.py +68 -0
  6. vynco/_response.py +34 -0
  7. vynco/py.typed +0 -0
  8. vynco/resources/__init__.py +1 -0
  9. vynco/resources/ai.py +93 -0
  10. vynco/resources/analytics.py +216 -0
  11. vynco/resources/api_keys.py +94 -0
  12. vynco/resources/auditors.py +95 -0
  13. vynco/resources/billing.py +59 -0
  14. vynco/resources/changes.py +98 -0
  15. vynco/resources/companies.py +276 -0
  16. vynco/resources/credits.py +84 -0
  17. vynco/resources/dashboard.py +39 -0
  18. vynco/resources/dossiers.py +100 -0
  19. vynco/resources/exports.py +117 -0
  20. vynco/resources/graph.py +88 -0
  21. vynco/resources/health.py +39 -0
  22. vynco/resources/persons.py +42 -0
  23. vynco/resources/screening.py +63 -0
  24. vynco/resources/teams.py +164 -0
  25. vynco/resources/watchlists.py +159 -0
  26. vynco/resources/webhooks.py +200 -0
  27. vynco/types/__init__.py +135 -0
  28. vynco/types/ai.py +46 -0
  29. vynco/types/analytics.py +86 -0
  30. vynco/types/api_keys.py +31 -0
  31. vynco/types/auditors.py +35 -0
  32. vynco/types/billing.py +9 -0
  33. vynco/types/changes.py +29 -0
  34. vynco/types/companies.py +148 -0
  35. vynco/types/credits.py +54 -0
  36. vynco/types/dashboard.py +36 -0
  37. vynco/types/dossiers.py +26 -0
  38. vynco/types/exports.py +24 -0
  39. vynco/types/graph.py +51 -0
  40. vynco/types/health.py +12 -0
  41. vynco/types/persons.py +17 -0
  42. vynco/types/screening.py +28 -0
  43. vynco/types/shared.py +27 -0
  44. vynco/types/teams.py +55 -0
  45. vynco/types/watchlists.py +35 -0
  46. vynco/types/webhooks.py +44 -0
  47. vynco-2.0.0.dist-info/METADATA +200 -0
  48. vynco-2.0.0.dist-info/RECORD +50 -0
  49. vynco-2.0.0.dist-info/WHEEL +4 -0
  50. vynco-2.0.0.dist-info/licenses/LICENSE +201 -0
vynco/__init__.py ADDED
@@ -0,0 +1,166 @@
1
+ """VynCo Python SDK — Python client for the VynCo Swiss Corporate Intelligence API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from vynco._client import AsyncClient, Client
6
+ from vynco._constants import __version__
7
+ from vynco._errors import (
8
+ AuthenticationError,
9
+ ConfigError,
10
+ ConflictError,
11
+ DeserializationError,
12
+ ForbiddenError,
13
+ InsufficientCreditsError,
14
+ NotFoundError,
15
+ RateLimitError,
16
+ ServerError,
17
+ ServiceUnavailableError,
18
+ ValidationError,
19
+ VyncoError,
20
+ )
21
+ from vynco._response import Response, ResponseMeta
22
+ from vynco.types import (
23
+ AddCompaniesResponse,
24
+ AiSearchResponse,
25
+ AnomalyResponse,
26
+ ApiKey,
27
+ ApiKeyCreated,
28
+ AuditCandidate,
29
+ AuditorHistoryResponse,
30
+ AuditorMarketShare,
31
+ AuditorTenure,
32
+ BillingSummary,
33
+ BoardMember,
34
+ CantonDistribution,
35
+ ChangeStatistics,
36
+ ClusterResponse,
37
+ CohortResponse,
38
+ Company,
39
+ CompanyChange,
40
+ CompanyCount,
41
+ CompanyReport,
42
+ CompanyStatistics,
43
+ CompareResponse,
44
+ CreateWebhookResponse,
45
+ CreditBalance,
46
+ CreditHistory,
47
+ CreditLedgerEntry,
48
+ CreditUsage,
49
+ DashboardResponse,
50
+ Dossier,
51
+ DossierResponse,
52
+ DossierSummary,
53
+ EventListResponse,
54
+ ExportDownload,
55
+ ExportJob,
56
+ Fingerprint,
57
+ GraphLink,
58
+ GraphNode,
59
+ GraphResponse,
60
+ HealthResponse,
61
+ HierarchyResponse,
62
+ Invitation,
63
+ NearbyCompany,
64
+ NetworkAnalysisResponse,
65
+ NewsItem,
66
+ PaginatedResponse,
67
+ Relationship,
68
+ RfmSegmentsResponse,
69
+ RiskFactor,
70
+ RiskScoreResponse,
71
+ ScreeningHit,
72
+ ScreeningResponse,
73
+ SessionUrl,
74
+ Team,
75
+ TeamMember,
76
+ TestDeliveryResponse,
77
+ Watchlist,
78
+ WatchlistCompaniesResponse,
79
+ WatchlistSummary,
80
+ WebhookDelivery,
81
+ WebhookSubscription,
82
+ )
83
+
84
+ __all__ = [
85
+ # Version
86
+ "__version__",
87
+ # Clients
88
+ "AsyncClient",
89
+ "Client",
90
+ # Errors
91
+ "VyncoError",
92
+ "AuthenticationError",
93
+ "InsufficientCreditsError",
94
+ "ForbiddenError",
95
+ "NotFoundError",
96
+ "ValidationError",
97
+ "ConflictError",
98
+ "RateLimitError",
99
+ "ServerError",
100
+ "ServiceUnavailableError",
101
+ "ConfigError",
102
+ "DeserializationError",
103
+ # Response
104
+ "Response",
105
+ "ResponseMeta",
106
+ # Types
107
+ "PaginatedResponse",
108
+ "HealthResponse",
109
+ "Company",
110
+ "CompanyCount",
111
+ "CompanyStatistics",
112
+ "EventListResponse",
113
+ "CompareResponse",
114
+ "NewsItem",
115
+ "CompanyReport",
116
+ "Relationship",
117
+ "HierarchyResponse",
118
+ "Fingerprint",
119
+ "NearbyCompany",
120
+ "AuditorHistoryResponse",
121
+ "AuditorTenure",
122
+ "AuditorMarketShare",
123
+ "DashboardResponse",
124
+ "ScreeningResponse",
125
+ "ScreeningHit",
126
+ "Watchlist",
127
+ "WatchlistSummary",
128
+ "WatchlistCompaniesResponse",
129
+ "AddCompaniesResponse",
130
+ "WebhookSubscription",
131
+ "CreateWebhookResponse",
132
+ "TestDeliveryResponse",
133
+ "WebhookDelivery",
134
+ "ExportJob",
135
+ "ExportDownload",
136
+ "DossierResponse",
137
+ "AiSearchResponse",
138
+ "RiskScoreResponse",
139
+ "RiskFactor",
140
+ "ApiKey",
141
+ "ApiKeyCreated",
142
+ "CreditBalance",
143
+ "CreditUsage",
144
+ "CreditHistory",
145
+ "CreditLedgerEntry",
146
+ "SessionUrl",
147
+ "Team",
148
+ "TeamMember",
149
+ "Invitation",
150
+ "BillingSummary",
151
+ "CompanyChange",
152
+ "ChangeStatistics",
153
+ "BoardMember",
154
+ "CantonDistribution",
155
+ "ClusterResponse",
156
+ "AnomalyResponse",
157
+ "RfmSegmentsResponse",
158
+ "CohortResponse",
159
+ "AuditCandidate",
160
+ "Dossier",
161
+ "DossierSummary",
162
+ "GraphResponse",
163
+ "GraphNode",
164
+ "GraphLink",
165
+ "NetworkAnalysisResponse",
166
+ ]
vynco/_base_client.py ADDED
@@ -0,0 +1,189 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any, TypeVar, get_args, get_origin
5
+
6
+ import httpx
7
+ from pydantic import BaseModel
8
+
9
+ from vynco._constants import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, __version__
10
+ from vynco._errors import (
11
+ STATUS_ERROR_MAP,
12
+ ConfigError,
13
+ DeserializationError,
14
+ ServerError,
15
+ VyncoError,
16
+ )
17
+ from vynco._response import Response, ResponseMeta
18
+
19
+ T = TypeVar("T")
20
+
21
+ _PARAM_RENAME = {"query": "search", "change_type": "type"}
22
+
23
+
24
+ def _build_params(kwargs: dict[str, Any]) -> dict[str, str]:
25
+ """Build query parameters from keyword arguments, dropping None values."""
26
+ params: dict[str, str] = {}
27
+ for key, value in kwargs.items():
28
+ if key == "self" or value is None:
29
+ continue
30
+ wire_key = _PARAM_RENAME.get(key, key)
31
+ # Convert snake_case to camelCase for the wire
32
+ parts = wire_key.split("_")
33
+ camel = parts[0] + "".join(p.capitalize() for p in parts[1:])
34
+ params[camel] = str(value).lower() if isinstance(value, bool) else str(value)
35
+ return params
36
+
37
+
38
+ def _parse_meta(headers: httpx.Headers) -> ResponseMeta:
39
+ """Extract metadata from API response headers."""
40
+ return ResponseMeta(
41
+ request_id=headers.get("X-Request-Id"),
42
+ credits_used=_parse_int(headers.get("X-Credits-Used")),
43
+ credits_remaining=_parse_int(headers.get("X-Credits-Remaining")),
44
+ rate_limit_limit=_parse_int(headers.get("X-Rate-Limit-Limit")),
45
+ data_source=headers.get("X-Data-Source"),
46
+ )
47
+
48
+
49
+ def _parse_int(value: str | None) -> int | None:
50
+ if value is None:
51
+ return None
52
+ try:
53
+ return int(value)
54
+ except ValueError:
55
+ return None
56
+
57
+
58
+ def _map_error(status_code: int, body: dict[str, Any]) -> VyncoError:
59
+ """Map an HTTP status code and response body to a typed error."""
60
+ detail = body.get("detail", "")
61
+ message = body.get("message", "")
62
+ status = body.get("status", status_code)
63
+
64
+ error_cls = STATUS_ERROR_MAP.get(status_code)
65
+ if error_cls:
66
+ return error_cls(detail=detail, message=message, status=status)
67
+ if status_code >= 500:
68
+ return ServerError(detail=detail, message=message, status=status)
69
+ return ServerError(detail=detail or f"HTTP {status_code}", message=message, status=status)
70
+
71
+
72
+ def _deserialize(data: Any, response_type: type[T]) -> T:
73
+ """Deserialize JSON data into the given type."""
74
+ origin = get_origin(response_type)
75
+ args = get_args(response_type)
76
+
77
+ # list[X]
78
+ if origin is list and args:
79
+ item_type = args[0]
80
+ items: list[Any]
81
+ if isinstance(data, list):
82
+ items = data
83
+ elif isinstance(data, dict):
84
+ # Try "data" key, then first array-valued key
85
+ maybe = data.get("data")
86
+ if isinstance(maybe, list):
87
+ items = maybe
88
+ else:
89
+ for val in data.values():
90
+ if isinstance(val, list):
91
+ items = val
92
+ break
93
+ else:
94
+ items = [data]
95
+ else:
96
+ items = [data]
97
+ if issubclass(item_type, BaseModel):
98
+ return [item_type.model_validate(item) for item in items] # type: ignore[return-value]
99
+ return items # type: ignore[return-value]
100
+
101
+ # Pydantic model (including generics like PaginatedResponse[Company])
102
+ if isinstance(response_type, type) and issubclass(response_type, BaseModel):
103
+ return response_type.model_validate(data)
104
+ if origin and args and isinstance(origin, type) and issubclass(origin, BaseModel):
105
+ # Generic model like PaginatedResponse[Company]
106
+ concrete = response_type # Already parameterized
107
+ if isinstance(concrete, type) and issubclass(concrete, BaseModel):
108
+ return concrete.model_validate(data)
109
+ # For runtime generics, we need the origin class
110
+ return origin.model_validate(data) # type: ignore[no-any-return]
111
+
112
+ # dict or other simple types
113
+ return data # type: ignore[no-any-return]
114
+
115
+
116
+ class BaseClientConfig:
117
+ """Shared configuration for async and sync clients."""
118
+
119
+ def __init__(
120
+ self,
121
+ api_key: str | None = None,
122
+ base_url: str = DEFAULT_BASE_URL,
123
+ timeout: float = DEFAULT_TIMEOUT,
124
+ max_retries: int = DEFAULT_MAX_RETRIES,
125
+ ) -> None:
126
+ self.api_key = api_key or os.environ.get("VYNCO_API_KEY", "")
127
+ if not self.api_key:
128
+ raise ConfigError("API key must not be empty. Pass api_key or set VYNCO_API_KEY.")
129
+ self.base_url = base_url.rstrip("/")
130
+ self.timeout = timeout
131
+ self.max_retries = max_retries
132
+
133
+ def _headers(self) -> dict[str, str]:
134
+ return {
135
+ "Authorization": f"Bearer {self.api_key}",
136
+ "User-Agent": f"vynco-python/{__version__}",
137
+ }
138
+
139
+ def _url(self, path: str) -> str:
140
+ return f"{self.base_url}{path}"
141
+
142
+ def _retry_delay(self, attempt: int, headers: httpx.Headers | None = None) -> float:
143
+ """Compute retry delay with exponential backoff, respecting Retry-After."""
144
+ if headers:
145
+ retry_after = headers.get("Retry-After")
146
+ if retry_after:
147
+ try:
148
+ return float(retry_after)
149
+ except ValueError:
150
+ pass
151
+ return 0.5 * float(2**attempt)
152
+
153
+ def _should_retry(self, status_code: int) -> bool:
154
+ return status_code == 429 or status_code >= 500
155
+
156
+ def _handle_response(self, resp: httpx.Response, response_type: type[T]) -> Response[T]:
157
+ meta = _parse_meta(resp.headers)
158
+
159
+ if not resp.is_success:
160
+ try:
161
+ body = resp.json()
162
+ except Exception:
163
+ body = {"detail": resp.text, "status": resp.status_code}
164
+ raise _map_error(resp.status_code, body)
165
+
166
+ if resp.status_code == 204 or not resp.content:
167
+ return Response(data=None, meta=meta) # type: ignore[arg-type]
168
+
169
+ try:
170
+ data = resp.json()
171
+ except Exception as e:
172
+ raise DeserializationError(detail=f"Failed to parse JSON: {e}") from e
173
+
174
+ try:
175
+ parsed = _deserialize(data, response_type)
176
+ except Exception as e:
177
+ raise DeserializationError(detail=f"Failed to deserialize response: {e}") from e
178
+
179
+ return Response(data=parsed, meta=meta)
180
+
181
+ def _handle_empty_response(self, resp: httpx.Response) -> ResponseMeta:
182
+ meta = _parse_meta(resp.headers)
183
+ if not resp.is_success:
184
+ try:
185
+ body = resp.json()
186
+ except Exception:
187
+ body = {"detail": resp.text, "status": resp.status_code}
188
+ raise _map_error(resp.status_code, body)
189
+ return meta
vynco/_client.py ADDED
@@ -0,0 +1,267 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any, TypeVar
5
+
6
+ import httpx
7
+
8
+ from vynco._base_client import BaseClientConfig
9
+ from vynco._constants import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT
10
+ from vynco._response import Response, ResponseMeta
11
+ from vynco.resources.ai import Ai, AsyncAi
12
+ from vynco.resources.analytics import Analytics, AsyncAnalytics
13
+ from vynco.resources.api_keys import ApiKeys, AsyncApiKeys
14
+ from vynco.resources.auditors import AsyncAuditors, Auditors
15
+ from vynco.resources.billing import AsyncBilling, Billing
16
+ from vynco.resources.changes import AsyncChanges, Changes
17
+ from vynco.resources.companies import AsyncCompanies, Companies
18
+ from vynco.resources.credits import AsyncCredits, Credits
19
+ from vynco.resources.dashboard import AsyncDashboard, Dashboard
20
+ from vynco.resources.dossiers import AsyncDossiers, Dossiers
21
+ from vynco.resources.exports import AsyncExports, Exports
22
+ from vynco.resources.graph import AsyncGraph, Graph
23
+ from vynco.resources.health import AsyncHealth, Health
24
+ from vynco.resources.persons import AsyncPersons, Persons
25
+ from vynco.resources.screening import AsyncScreening, Screening
26
+ from vynco.resources.teams import AsyncTeams, Teams
27
+ from vynco.resources.watchlists import AsyncWatchlists, Watchlists
28
+ from vynco.resources.webhooks import AsyncWebhooks, Webhooks
29
+
30
+ T = TypeVar("T")
31
+
32
+
33
+ class AsyncClient(BaseClientConfig):
34
+ """Async client for the VynCo API.
35
+
36
+ Uses httpx.AsyncClient under the hood. Supports ``async with`` for
37
+ automatic cleanup.
38
+
39
+ Example::
40
+
41
+ async with vynco.AsyncClient("vc_live_xxx") as client:
42
+ result = await client.companies.list(query="Novartis")
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ api_key: str | None = None,
48
+ *,
49
+ base_url: str = DEFAULT_BASE_URL,
50
+ timeout: float = DEFAULT_TIMEOUT,
51
+ max_retries: int = DEFAULT_MAX_RETRIES,
52
+ ) -> None:
53
+ super().__init__(
54
+ api_key=api_key,
55
+ base_url=base_url,
56
+ timeout=timeout,
57
+ max_retries=max_retries,
58
+ )
59
+ self._http = httpx.AsyncClient(
60
+ headers=self._headers(),
61
+ timeout=self.timeout,
62
+ )
63
+ self.health = AsyncHealth(self)
64
+ self.companies = AsyncCompanies(self)
65
+ self.auditors = AsyncAuditors(self)
66
+ self.dashboard = AsyncDashboard(self)
67
+ self.screening = AsyncScreening(self)
68
+ self.watchlists = AsyncWatchlists(self)
69
+ self.webhooks = AsyncWebhooks(self)
70
+ self.exports = AsyncExports(self)
71
+ self.ai = AsyncAi(self)
72
+ self.api_keys = AsyncApiKeys(self)
73
+ self.credits = AsyncCredits(self)
74
+ self.billing = AsyncBilling(self)
75
+ self.teams = AsyncTeams(self)
76
+ self.changes = AsyncChanges(self)
77
+ self.persons = AsyncPersons(self)
78
+ self.analytics = AsyncAnalytics(self)
79
+ self.dossiers = AsyncDossiers(self)
80
+ self.graph = AsyncGraph(self)
81
+
82
+ async def _request(
83
+ self,
84
+ method: str,
85
+ path: str,
86
+ *,
87
+ params: dict[str, str] | None = None,
88
+ json: Any = None,
89
+ ) -> httpx.Response:
90
+ """Execute an HTTP request with retry logic."""
91
+ url = self._url(path)
92
+ last_exc: Exception | None = None
93
+
94
+ for attempt in range(self.max_retries + 1):
95
+ try:
96
+ resp = await self._http.request(
97
+ method,
98
+ url,
99
+ params=params,
100
+ json=json,
101
+ )
102
+ except httpx.HTTPError as e:
103
+ last_exc = e
104
+ if attempt < self.max_retries:
105
+ await asyncio.sleep(self._retry_delay(attempt))
106
+ continue
107
+ raise
108
+
109
+ if self._should_retry(resp.status_code) and attempt < self.max_retries:
110
+ delay = self._retry_delay(attempt, resp.headers)
111
+ await asyncio.sleep(delay)
112
+ continue
113
+
114
+ return resp
115
+
116
+ raise last_exc # type: ignore[misc]
117
+
118
+ async def _request_model(
119
+ self,
120
+ method: str,
121
+ path: str,
122
+ *,
123
+ params: dict[str, str] | None = None,
124
+ json: Any = None,
125
+ response_type: type[T],
126
+ ) -> Response[T]:
127
+ resp = await self._request(method, path, params=params, json=json)
128
+ return self._handle_response(resp, response_type)
129
+
130
+ async def _request_empty(
131
+ self,
132
+ method: str,
133
+ path: str,
134
+ *,
135
+ params: dict[str, str] | None = None,
136
+ json: Any = None,
137
+ ) -> ResponseMeta:
138
+ resp = await self._request(method, path, params=params, json=json)
139
+ return self._handle_empty_response(resp)
140
+
141
+ async def close(self) -> None:
142
+ await self._http.aclose()
143
+
144
+ async def __aenter__(self) -> AsyncClient:
145
+ return self
146
+
147
+ async def __aexit__(self, *args: Any) -> None:
148
+ await self.close()
149
+
150
+
151
+ class Client(BaseClientConfig):
152
+ """Sync client for the VynCo API.
153
+
154
+ Uses httpx.Client under the hood. Supports ``with`` for automatic cleanup.
155
+
156
+ Example::
157
+
158
+ with vynco.Client("vc_live_xxx") as client:
159
+ result = client.companies.list(query="Novartis")
160
+ """
161
+
162
+ def __init__(
163
+ self,
164
+ api_key: str | None = None,
165
+ *,
166
+ base_url: str = DEFAULT_BASE_URL,
167
+ timeout: float = DEFAULT_TIMEOUT,
168
+ max_retries: int = DEFAULT_MAX_RETRIES,
169
+ ) -> None:
170
+ super().__init__(
171
+ api_key=api_key,
172
+ base_url=base_url,
173
+ timeout=timeout,
174
+ max_retries=max_retries,
175
+ )
176
+ self._http = httpx.Client(
177
+ headers=self._headers(),
178
+ timeout=self.timeout,
179
+ )
180
+ self.health = Health(self)
181
+ self.companies = Companies(self)
182
+ self.auditors = Auditors(self)
183
+ self.dashboard = Dashboard(self)
184
+ self.screening = Screening(self)
185
+ self.watchlists = Watchlists(self)
186
+ self.webhooks = Webhooks(self)
187
+ self.exports = Exports(self)
188
+ self.ai = Ai(self)
189
+ self.api_keys = ApiKeys(self)
190
+ self.credits = Credits(self)
191
+ self.billing = Billing(self)
192
+ self.teams = Teams(self)
193
+ self.changes = Changes(self)
194
+ self.persons = Persons(self)
195
+ self.analytics = Analytics(self)
196
+ self.dossiers = Dossiers(self)
197
+ self.graph = Graph(self)
198
+
199
+ def _request(
200
+ self,
201
+ method: str,
202
+ path: str,
203
+ *,
204
+ params: dict[str, str] | None = None,
205
+ json: Any = None,
206
+ ) -> httpx.Response:
207
+ """Execute an HTTP request with retry logic."""
208
+ import time
209
+
210
+ url = self._url(path)
211
+ last_exc: Exception | None = None
212
+
213
+ for attempt in range(self.max_retries + 1):
214
+ try:
215
+ resp = self._http.request(
216
+ method,
217
+ url,
218
+ params=params,
219
+ json=json,
220
+ )
221
+ except httpx.HTTPError as e:
222
+ last_exc = e
223
+ if attempt < self.max_retries:
224
+ time.sleep(self._retry_delay(attempt))
225
+ continue
226
+ raise
227
+
228
+ if self._should_retry(resp.status_code) and attempt < self.max_retries:
229
+ delay = self._retry_delay(attempt, resp.headers)
230
+ time.sleep(delay)
231
+ continue
232
+
233
+ return resp
234
+
235
+ raise last_exc # type: ignore[misc]
236
+
237
+ def _request_model(
238
+ self,
239
+ method: str,
240
+ path: str,
241
+ *,
242
+ params: dict[str, str] | None = None,
243
+ json: Any = None,
244
+ response_type: type[T],
245
+ ) -> Response[T]:
246
+ resp = self._request(method, path, params=params, json=json)
247
+ return self._handle_response(resp, response_type)
248
+
249
+ def _request_empty(
250
+ self,
251
+ method: str,
252
+ path: str,
253
+ *,
254
+ params: dict[str, str] | None = None,
255
+ json: Any = None,
256
+ ) -> ResponseMeta:
257
+ resp = self._request(method, path, params=params, json=json)
258
+ return self._handle_empty_response(resp)
259
+
260
+ def close(self) -> None:
261
+ self._http.close()
262
+
263
+ def __enter__(self) -> Client:
264
+ return self
265
+
266
+ def __exit__(self, *args: Any) -> None:
267
+ self.close()
vynco/_constants.py ADDED
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ DEFAULT_BASE_URL = "https://api.vynco.ch"
4
+ DEFAULT_TIMEOUT = 30.0
5
+ DEFAULT_MAX_RETRIES = 2
6
+
7
+ __version__ = "2.0.0"
vynco/_errors.py ADDED
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class VyncoError(Exception):
5
+ """Base exception for all VynCo SDK errors."""
6
+
7
+ def __init__(self, detail: str = "", message: str = "", status: int = 0) -> None:
8
+ self.detail = detail or message
9
+ self.message = message or detail
10
+ self.status = status
11
+ super().__init__(self.detail)
12
+
13
+
14
+ class AuthenticationError(VyncoError):
15
+ """401 — Invalid or missing API key."""
16
+
17
+
18
+ class InsufficientCreditsError(VyncoError):
19
+ """402 — Not enough credits for this operation."""
20
+
21
+
22
+ class ForbiddenError(VyncoError):
23
+ """403 — Insufficient permissions."""
24
+
25
+
26
+ class NotFoundError(VyncoError):
27
+ """404 — Resource not found."""
28
+
29
+
30
+ class ValidationError(VyncoError):
31
+ """400/422 — Invalid request parameters."""
32
+
33
+
34
+ class RateLimitError(VyncoError):
35
+ """429 — Too many requests."""
36
+
37
+
38
+ class ConflictError(VyncoError):
39
+ """409 — Request conflicts with existing state."""
40
+
41
+
42
+ class ServerError(VyncoError):
43
+ """5xx — Server-side error."""
44
+
45
+
46
+ class ServiceUnavailableError(ServerError):
47
+ """503 — API temporarily unavailable."""
48
+
49
+
50
+ class ConfigError(VyncoError):
51
+ """Client misconfiguration."""
52
+
53
+
54
+ class DeserializationError(VyncoError):
55
+ """Failed to parse API response."""
56
+
57
+
58
+ STATUS_ERROR_MAP: dict[int, type[VyncoError]] = {
59
+ 400: ValidationError,
60
+ 401: AuthenticationError,
61
+ 402: InsufficientCreditsError,
62
+ 403: ForbiddenError,
63
+ 404: NotFoundError,
64
+ 409: ConflictError,
65
+ 422: ValidationError,
66
+ 429: RateLimitError,
67
+ 503: ServiceUnavailableError,
68
+ }