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.
- vynco/__init__.py +166 -0
- vynco/_base_client.py +189 -0
- vynco/_client.py +267 -0
- vynco/_constants.py +7 -0
- vynco/_errors.py +68 -0
- vynco/_response.py +34 -0
- vynco/py.typed +0 -0
- vynco/resources/__init__.py +1 -0
- vynco/resources/ai.py +93 -0
- vynco/resources/analytics.py +216 -0
- vynco/resources/api_keys.py +94 -0
- vynco/resources/auditors.py +95 -0
- vynco/resources/billing.py +59 -0
- vynco/resources/changes.py +98 -0
- vynco/resources/companies.py +276 -0
- vynco/resources/credits.py +84 -0
- vynco/resources/dashboard.py +39 -0
- vynco/resources/dossiers.py +100 -0
- vynco/resources/exports.py +117 -0
- vynco/resources/graph.py +88 -0
- vynco/resources/health.py +39 -0
- vynco/resources/persons.py +42 -0
- vynco/resources/screening.py +63 -0
- vynco/resources/teams.py +164 -0
- vynco/resources/watchlists.py +159 -0
- vynco/resources/webhooks.py +200 -0
- vynco/types/__init__.py +135 -0
- vynco/types/ai.py +46 -0
- vynco/types/analytics.py +86 -0
- vynco/types/api_keys.py +31 -0
- vynco/types/auditors.py +35 -0
- vynco/types/billing.py +9 -0
- vynco/types/changes.py +29 -0
- vynco/types/companies.py +148 -0
- vynco/types/credits.py +54 -0
- vynco/types/dashboard.py +36 -0
- vynco/types/dossiers.py +26 -0
- vynco/types/exports.py +24 -0
- vynco/types/graph.py +51 -0
- vynco/types/health.py +12 -0
- vynco/types/persons.py +17 -0
- vynco/types/screening.py +28 -0
- vynco/types/shared.py +27 -0
- vynco/types/teams.py +55 -0
- vynco/types/watchlists.py +35 -0
- vynco/types/webhooks.py +44 -0
- vynco-2.0.0.dist-info/METADATA +200 -0
- vynco-2.0.0.dist-info/RECORD +50 -0
- vynco-2.0.0.dist-info/WHEEL +4 -0
- 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
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
|
+
}
|