Skip to content

Commit 0804069

Browse files
mivertowskiclaude
andcommitted
Align Python SDK v3.0.0 with Rust reference SDK
Brings the Python SDK to full parity with the prod Rust reference implementation: base URL, response headers, all types/resources, and file download support. Verified against the live API. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent abcc4db commit 0804069

25 files changed

+874
-69
lines changed

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [3.0.0] - 2026-04-08
9+
10+
### Changed
11+
12+
- **Breaking**: Base URL changed from `https://api.vynco.ch` to `https://vynco.ch/api` — matches the Rust reference SDK
13+
- **Breaking**: Response header names changed — `X-Rate-Limit-Limit``X-RateLimit-Limit` (matching Rust SDK)
14+
- **Breaking**: `DashboardResponse.data` model changed — `DataCompleteness` fields updated to `total_companies`, `enriched_companies`, `companies_with_industry`, `companies_with_geo`, `total_persons`, `total_changes`, `total_sogc_publications`
15+
- **Breaking**: `PipelineStatus` model changed — `name``id`, `records_processed``items_processed`, `duration_seconds` removed
16+
- **Breaking**: `AuditorTenureStats` model rewritten — fields now `total_tracked`, `current_auditors`, `tenures_over_10_years`, `tenures_over_7_years`, `avg_tenure_years`, `longest_tenure`
17+
- **Breaking**: `exports.download()` and `graph.export()` now return `ExportFile` dataclass (with `bytes`, `content_type`, `filename`, `meta`) instead of `Response[bytes]`
18+
- **Company** model expanded with 25+ new fields: `currency`, `purpose`, `founding_date`, `registration_date`, `deletion_date`, `legal_seat`, `municipality`, `data_source`, `enrichment_level`, `address_street`, `address_house_number`, `address_zip_code`, `address_city`, `address_canton`, `website`, `sub_industry`, `employee_count`, `auditor_name`, `latitude`, `longitude`, `geo_precision`, `noga_code`, `sanctions_hit`, `last_screened_at`, `is_finma_regulated`, `ehraid`, `chid`, `cantonal_excerpt_url`, `old_names`, `translations`
19+
- `companies.list()` now accepts `status`, `legal_form`, `capital_min`, `capital_max`, `auditor_category`, `sort_by`, `sort_desc` filter parameters
20+
21+
### Added
22+
23+
- **`ResponseMeta.rate_limit_remaining`** — remaining requests in rate limit window (`X-RateLimit-Remaining`)
24+
- **`ResponseMeta.rate_limit_reset`** — Unix timestamp when rate limit resets (`X-RateLimit-Reset`)
25+
- **`ExportFile`** dataclass — returned by file download endpoints (`exports.download()`, `graph.export()`, `companies.export_excel()`)
26+
- **`companies.get_full(uid)`** — full company details with persons, changes, relationships
27+
- **`companies.classification(uid)`** — industry classification
28+
- **`companies.structure(uid)`** — corporate structure (head offices, branches, M&A)
29+
- **`companies.acquisitions(uid)`** — M&A relationships
30+
- **`companies.notes(uid)`**, **`create_note()`**, **`update_note()`**, **`delete_note()`** — company notes CRUD
31+
- **`companies.tags(uid)`**, **`create_tag()`**, **`delete_tag()`**, **`all_tags()`** — company tags CRUD
32+
- **`companies.export_excel()`** — Excel/CSV export of companies
33+
- **`persons.search(q, page, page_size)`** — search persons by name
34+
- **`persons.get(id)`** — get person detail with all roles
35+
- **`teams.join(token)`** — join a team via invitation token
36+
- **`dossiers.generate(uid)`** — generate a dossier for a company
37+
- New typed models: `CompanyFullResponse`, `PersonEntry`, `ChangeEntry`, `RelationshipEntry`, `Classification`, `CorporateStructure`, `RelatedCompanyEntry`, `Acquisition`, `Note`, `Tag`, `TagSummary`, `PersonSearchResult`, `PersonDetail`, `PersonRoleDetail`, `JoinTeamResponse`, `LongestTenure`
38+
- Retry logic now respects `X-RateLimit-Reset` header in addition to `Retry-After`
39+
840
## [2.0.0] - 2026-03-31
941

1042
### Changed

README.md

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ print(f"Found {result.data.total} companies")
2929
company = client.companies.get("CHE-105.805.080")
3030
print(f"{company.data.name}: {company.data.legal_form}")
3131

32+
# Full company details with persons, changes, relationships
33+
full = client.companies.get_full("CHE-105.805.080")
34+
print(f"Board: {len(full.data.persons)} persons")
35+
3236
# Sanctions screening
3337
screening = client.screening.screen(name="Suspicious Corp")
3438
print(f"Risk: {screening.data.risk_level} ({screening.data.hit_count} hits)")
@@ -55,12 +59,12 @@ async def main():
5559

5660
## API Coverage
5761

58-
18 resource modules covering 69 endpoints:
62+
18 resource modules covering 90+ endpoints:
5963

6064
| Resource | Methods |
6165
|----------|---------|
6266
| `client.health` | `check` |
63-
| `client.companies` | `list`, `get`, `count`, `events`, `statistics`, `compare`, `news`, `reports`, `relationships`, `hierarchy`, `fingerprint`, `nearby` |
67+
| `client.companies` | `list`, `get`, `get_full`, `count`, `events`, `statistics`, `compare`, `news`, `reports`, `relationships`, `hierarchy`, `classification`, `fingerprint`, `structure`, `acquisitions`, `nearby`, `notes`, `create_note`, `update_note`, `delete_note`, `tags`, `create_tag`, `delete_tag`, `all_tags`, `export_excel` |
6468
| `client.auditors` | `history`, `tenures` |
6569
| `client.dashboard` | `get` |
6670
| `client.screening` | `screen` |
@@ -71,11 +75,11 @@ async def main():
7175
| `client.api_keys` | `list`, `create`, `revoke` |
7276
| `client.credits` | `balance`, `usage`, `history` |
7377
| `client.billing` | `create_checkout`, `create_portal` |
74-
| `client.teams` | `me`, `create`, `members`, `invite_member`, `update_member_role`, `remove_member`, `billing_summary` |
78+
| `client.teams` | `me`, `create`, `members`, `invite_member`, `update_member_role`, `remove_member`, `billing_summary`, `join` |
7579
| `client.changes` | `list`, `by_company`, `statistics` |
76-
| `client.persons` | `board_members` |
80+
| `client.persons` | `board_members`, `search`, `get` |
7781
| `client.analytics` | `cantons`, `auditors`, `cluster`, `anomalies`, `rfm_segments`, `cohorts`, `candidates` |
78-
| `client.dossiers` | `create`, `list`, `get`, `delete` |
82+
| `client.dossiers` | `create`, `list`, `get`, `delete`, `generate` |
7983
| `client.graph` | `get`, `export`, `analyze` |
8084

8185
## Response Metadata
@@ -85,21 +89,23 @@ Every response includes header metadata for credit tracking and rate limiting:
8589
```python
8690
resp = client.companies.get("CHE-105.805.080")
8791

88-
print(f"Request ID: {resp.meta.request_id}") # X-Request-Id
89-
print(f"Credits used: {resp.meta.credits_used}") # X-Credits-Used
92+
print(f"Request ID: {resp.meta.request_id}") # X-Request-Id
93+
print(f"Credits used: {resp.meta.credits_used}") # X-Credits-Used
9094
print(f"Credits remaining: {resp.meta.credits_remaining}") # X-Credits-Remaining
91-
print(f"Rate limit: {resp.meta.rate_limit_limit}") # X-Rate-Limit-Limit
92-
print(f"Data source: {resp.meta.data_source}") # X-Data-Source
95+
print(f"Rate limit: {resp.meta.rate_limit_limit}") # X-RateLimit-Limit
96+
print(f"Rate remaining: {resp.meta.rate_limit_remaining}") # X-RateLimit-Remaining
97+
print(f"Rate reset: {resp.meta.rate_limit_reset}") # X-RateLimit-Reset
98+
print(f"Data source: {resp.meta.data_source}") # X-Data-Source
9399
```
94100

95101
## Configuration
96102

97103
```python
98104
client = vynco.Client(
99105
api_key="vc_live_xxx",
100-
base_url="https://api.vynco.ch", # default
101-
timeout=30.0, # seconds, default
102-
max_retries=2, # default, retries on 429/5xx
106+
base_url="https://vynco.ch/api", # default
107+
timeout=30.0, # seconds, default
108+
max_retries=2, # default, retries on 429/5xx
103109
)
104110
```
105111

@@ -114,7 +120,7 @@ client = vynco.Client() # reads from VYNCO_API_KEY
114120
```
115121

116122
The client automatically retries on HTTP 429 (rate limited) and 5xx (server error) with
117-
exponential backoff (500ms x 2^attempt). It respects the `Retry-After` header when present.
123+
exponential backoff (500ms x 2^attempt). It respects the `Retry-After` and `X-RateLimit-Reset` headers when present.
118124

119125
## Error Handling
120126

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "vynco"
3-
version = "2.0.0"
3+
version = "3.0.0"
44
description = "Python SDK for the VynCo Swiss Corporate Intelligence API"
55
readme = "README.md"
66
license = "Apache-2.0"

src/vynco/__init__.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
ValidationError,
1919
VyncoError,
2020
)
21-
from vynco._response import Response, ResponseMeta
21+
from vynco._response import ExportFile, Response, ResponseMeta
2222
from vynco.types import (
23+
Acquisition,
2324
AddCompaniesResponse,
2425
AiSearchResponse,
2526
AnomalyResponse,
@@ -32,15 +33,19 @@
3233
BillingSummary,
3334
BoardMember,
3435
CantonDistribution,
36+
ChangeEntry,
3537
ChangeStatistics,
38+
Classification,
3639
ClusterResponse,
3740
CohortResponse,
3841
Company,
3942
CompanyChange,
4043
CompanyCount,
44+
CompanyFullResponse,
4145
CompanyReport,
4246
CompanyStatistics,
4347
CompareResponse,
48+
CorporateStructure,
4449
CreateWebhookResponse,
4550
CreditBalance,
4651
CreditHistory,
@@ -60,17 +65,27 @@
6065
HealthResponse,
6166
HierarchyResponse,
6267
Invitation,
68+
JoinTeamResponse,
6369
NearbyCompany,
6470
NetworkAnalysisResponse,
6571
NewsItem,
72+
Note,
6673
PaginatedResponse,
74+
PersonDetail,
75+
PersonEntry,
76+
PersonRoleDetail,
77+
PersonSearchResult,
78+
RelatedCompanyEntry,
6779
Relationship,
80+
RelationshipEntry,
6881
RfmSegmentsResponse,
6982
RiskFactor,
7083
RiskScoreResponse,
7184
ScreeningHit,
7285
ScreeningResponse,
7386
SessionUrl,
87+
Tag,
88+
TagSummary,
7489
Team,
7590
TeamMember,
7691
TestDeliveryResponse,
@@ -103,6 +118,7 @@
103118
# Response
104119
"Response",
105120
"ResponseMeta",
121+
"ExportFile",
106122
# Types
107123
"PaginatedResponse",
108124
"HealthResponse",
@@ -117,6 +133,17 @@
117133
"HierarchyResponse",
118134
"Fingerprint",
119135
"NearbyCompany",
136+
"Classification",
137+
"CorporateStructure",
138+
"RelatedCompanyEntry",
139+
"Acquisition",
140+
"Note",
141+
"Tag",
142+
"TagSummary",
143+
"CompanyFullResponse",
144+
"PersonEntry",
145+
"ChangeEntry",
146+
"RelationshipEntry",
120147
"AuditorHistoryResponse",
121148
"AuditorTenure",
122149
"AuditorMarketShare",
@@ -148,9 +175,13 @@
148175
"TeamMember",
149176
"Invitation",
150177
"BillingSummary",
178+
"JoinTeamResponse",
151179
"CompanyChange",
152180
"ChangeStatistics",
153181
"BoardMember",
182+
"PersonSearchResult",
183+
"PersonDetail",
184+
"PersonRoleDetail",
154185
"CantonDistribution",
155186
"ClusterResponse",
156187
"AnomalyResponse",

src/vynco/_base_client.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
ServerError,
1515
VyncoError,
1616
)
17-
from vynco._response import Response, ResponseMeta
17+
from vynco._response import ExportFile, Response, ResponseMeta
1818

1919
T = TypeVar("T")
2020

@@ -41,7 +41,9 @@ def _parse_meta(headers: httpx.Headers) -> ResponseMeta:
4141
request_id=headers.get("X-Request-Id"),
4242
credits_used=_parse_int(headers.get("X-Credits-Used")),
4343
credits_remaining=_parse_int(headers.get("X-Credits-Remaining")),
44-
rate_limit_limit=_parse_int(headers.get("X-Rate-Limit-Limit")),
44+
rate_limit_limit=_parse_int(headers.get("X-RateLimit-Limit")),
45+
rate_limit_remaining=_parse_int(headers.get("X-RateLimit-Remaining")),
46+
rate_limit_reset=_parse_int(headers.get("X-RateLimit-Reset")),
4547
data_source=headers.get("X-Data-Source"),
4648
)
4749

@@ -142,10 +144,10 @@ def _url(self, path: str) -> str:
142144
def _retry_delay(self, attempt: int, headers: httpx.Headers | None = None) -> float:
143145
"""Compute retry delay with exponential backoff, respecting Retry-After."""
144146
if headers:
145-
retry_after = headers.get("Retry-After")
147+
retry_after = headers.get("Retry-After") or headers.get("X-RateLimit-Reset")
146148
if retry_after:
147149
try:
148-
return float(retry_after)
150+
return min(float(retry_after), 60.0)
149151
except ValueError:
150152
pass
151153
return 0.5 * float(2**attempt)
@@ -187,3 +189,25 @@ def _handle_empty_response(self, resp: httpx.Response) -> ResponseMeta:
187189
body = {"detail": resp.text, "status": resp.status_code}
188190
raise _map_error(resp.status_code, body)
189191
return meta
192+
193+
def _handle_bytes_response(self, resp: httpx.Response) -> ExportFile:
194+
meta = _parse_meta(resp.headers)
195+
if not resp.is_success:
196+
try:
197+
body = resp.json()
198+
except Exception:
199+
body = {"detail": resp.text, "status": resp.status_code}
200+
raise _map_error(resp.status_code, body)
201+
202+
content_type = resp.headers.get("content-type", "application/octet-stream")
203+
filename = ""
204+
disposition = resp.headers.get("content-disposition", "")
205+
if "filename=" in disposition:
206+
filename = disposition.split("filename=")[1].strip('"')
207+
208+
return ExportFile(
209+
meta=meta,
210+
bytes=resp.content,
211+
content_type=content_type,
212+
filename=filename,
213+
)

src/vynco/_client.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from vynco._base_client import BaseClientConfig
99
from vynco._constants import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT
10-
from vynco._response import Response, ResponseMeta
10+
from vynco._response import ExportFile, Response, ResponseMeta
1111
from vynco.resources.ai import Ai, AsyncAi
1212
from vynco.resources.analytics import Analytics, AsyncAnalytics
1313
from vynco.resources.api_keys import ApiKeys, AsyncApiKeys
@@ -138,6 +138,17 @@ async def _request_empty(
138138
resp = await self._request(method, path, params=params, json=json)
139139
return self._handle_empty_response(resp)
140140

141+
async def _request_bytes(
142+
self,
143+
method: str,
144+
path: str,
145+
*,
146+
params: dict[str, str] | None = None,
147+
json: Any = None,
148+
) -> ExportFile:
149+
resp = await self._request(method, path, params=params, json=json)
150+
return self._handle_bytes_response(resp)
151+
141152
async def close(self) -> None:
142153
await self._http.aclose()
143154

@@ -257,6 +268,17 @@ def _request_empty(
257268
resp = self._request(method, path, params=params, json=json)
258269
return self._handle_empty_response(resp)
259270

271+
def _request_bytes(
272+
self,
273+
method: str,
274+
path: str,
275+
*,
276+
params: dict[str, str] | None = None,
277+
json: Any = None,
278+
) -> ExportFile:
279+
resp = self._request(method, path, params=params, json=json)
280+
return self._handle_bytes_response(resp)
281+
260282
def close(self) -> None:
261283
self._http.close()
262284

src/vynco/_constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

3-
DEFAULT_BASE_URL = "https://api.vynco.ch"
3+
DEFAULT_BASE_URL = "https://vynco.ch/api"
44
DEFAULT_TIMEOUT = 30.0
55
DEFAULT_MAX_RETRIES = 2
66

7-
__version__ = "2.0.0"
7+
__version__ = "3.0.0"

src/vynco/_response.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,16 @@ class ResponseMeta:
2020
"""Remaining credit balance after this request (X-Credits-Remaining)."""
2121

2222
rate_limit_limit: int | None = None
23-
"""Maximum requests per minute for the current tier (X-Rate-Limit-Limit)."""
23+
"""Maximum requests per minute for the current tier (X-RateLimit-Limit)."""
24+
25+
rate_limit_remaining: int | None = None
26+
"""Remaining requests in the current rate limit window (X-RateLimit-Remaining)."""
27+
28+
rate_limit_reset: int | None = None
29+
"""Unix timestamp when the rate limit window resets (X-RateLimit-Reset)."""
2430

2531
data_source: str | None = None
26-
"""Data source for OGD compliance (X-Data-Source): "Zefix" or "LINDAS"."""
32+
"""Data source attribution (X-Data-Source)."""
2733

2834

2935
@dataclass
@@ -32,3 +38,13 @@ class Response(Generic[T]):
3238

3339
data: T
3440
meta: ResponseMeta = field(default_factory=ResponseMeta)
41+
42+
43+
@dataclass
44+
class ExportFile:
45+
"""Downloaded export file with raw bytes and metadata."""
46+
47+
meta: ResponseMeta
48+
bytes: bytes
49+
content_type: str
50+
filename: str

0 commit comments

Comments
 (0)