vericorp 1.0.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.
- vericorp-1.0.0/.github/workflows/publish.yml +20 -0
- vericorp-1.0.0/.gitignore +6 -0
- vericorp-1.0.0/PKG-INFO +94 -0
- vericorp-1.0.0/README.md +70 -0
- vericorp-1.0.0/pyproject.toml +39 -0
- vericorp-1.0.0/src/vericorp/__init__.py +37 -0
- vericorp-1.0.0/src/vericorp/async_client.py +116 -0
- vericorp-1.0.0/src/vericorp/client.py +117 -0
- vericorp-1.0.0/src/vericorp/errors.py +37 -0
- vericorp-1.0.0/src/vericorp/models.py +109 -0
- vericorp-1.0.0/src/vericorp/py.typed +0 -0
- vericorp-1.0.0/tests/__init__.py +0 -0
- vericorp-1.0.0/tests/test_async_client.py +67 -0
- vericorp-1.0.0/tests/test_client.py +125 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.12"
|
|
15
|
+
- run: pip install build twine
|
|
16
|
+
- run: python -m build
|
|
17
|
+
- run: twine upload dist/*
|
|
18
|
+
env:
|
|
19
|
+
TWINE_USERNAME: __token__
|
|
20
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
vericorp-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vericorp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for the VeriCorp API — European company verification
|
|
5
|
+
Project-URL: Homepage, https://github.com/vericorptest-collab/vericorp-python
|
|
6
|
+
Project-URL: Documentation, https://rapidapi.com/vericorp/api/vericorp-api
|
|
7
|
+
Author: VeriCorp
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: api,company,europe,tax-id,validation,vat,vies
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: httpx>=0.24
|
|
22
|
+
Requires-Dist: pydantic>=2.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# VeriCorp Python SDK
|
|
26
|
+
|
|
27
|
+
Python SDK for the [VeriCorp API](https://rapidapi.com/vericorp/api/vericorp-api) — European company verification.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install vericorp
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from vericorp import VeriCorp
|
|
39
|
+
|
|
40
|
+
client = VeriCorp("your-rapidapi-key")
|
|
41
|
+
|
|
42
|
+
# Look up a company
|
|
43
|
+
company = client.lookup("PT502011378")
|
|
44
|
+
print(company.name) # UNIVERSIDADE DO MINHO
|
|
45
|
+
print(company.address) # Address(street='LG DO PACO', city='BRAGA', ...)
|
|
46
|
+
|
|
47
|
+
# Validate a VAT number
|
|
48
|
+
result = client.validate("DE811871080")
|
|
49
|
+
print(result.vat_valid) # True
|
|
50
|
+
|
|
51
|
+
# List supported countries
|
|
52
|
+
countries = client.countries()
|
|
53
|
+
print(countries.total) # 29
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Async
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from vericorp import AsyncVeriCorp
|
|
60
|
+
|
|
61
|
+
async with AsyncVeriCorp("your-rapidapi-key") as client:
|
|
62
|
+
company = await client.lookup("DK10150817")
|
|
63
|
+
print(company.name)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Methods
|
|
67
|
+
|
|
68
|
+
| Method | Description |
|
|
69
|
+
|--------|-------------|
|
|
70
|
+
| `lookup(tax_id)` | Look up company by tax ID |
|
|
71
|
+
| `lookup_gb(company_number)` | Look up UK company by number |
|
|
72
|
+
| `validate(tax_id)` | Validate a VAT number |
|
|
73
|
+
| `batch(tax_ids)` | Batch lookup (max 10) |
|
|
74
|
+
| `countries()` | List supported countries |
|
|
75
|
+
| `health()` | API health check |
|
|
76
|
+
|
|
77
|
+
## Error Handling
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from vericorp.errors import InvalidTaxIdError, NotFoundError, RateLimitError
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
company = client.lookup("INVALID")
|
|
84
|
+
except InvalidTaxIdError:
|
|
85
|
+
print("Bad tax ID format")
|
|
86
|
+
except NotFoundError:
|
|
87
|
+
print("Company not found")
|
|
88
|
+
except RateLimitError as e:
|
|
89
|
+
print(f"Rate limited, retry after {e.retry_after}s")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
vericorp-1.0.0/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# VeriCorp Python SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for the [VeriCorp API](https://rapidapi.com/vericorp/api/vericorp-api) — European company verification.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install vericorp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from vericorp import VeriCorp
|
|
15
|
+
|
|
16
|
+
client = VeriCorp("your-rapidapi-key")
|
|
17
|
+
|
|
18
|
+
# Look up a company
|
|
19
|
+
company = client.lookup("PT502011378")
|
|
20
|
+
print(company.name) # UNIVERSIDADE DO MINHO
|
|
21
|
+
print(company.address) # Address(street='LG DO PACO', city='BRAGA', ...)
|
|
22
|
+
|
|
23
|
+
# Validate a VAT number
|
|
24
|
+
result = client.validate("DE811871080")
|
|
25
|
+
print(result.vat_valid) # True
|
|
26
|
+
|
|
27
|
+
# List supported countries
|
|
28
|
+
countries = client.countries()
|
|
29
|
+
print(countries.total) # 29
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Async
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from vericorp import AsyncVeriCorp
|
|
36
|
+
|
|
37
|
+
async with AsyncVeriCorp("your-rapidapi-key") as client:
|
|
38
|
+
company = await client.lookup("DK10150817")
|
|
39
|
+
print(company.name)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Methods
|
|
43
|
+
|
|
44
|
+
| Method | Description |
|
|
45
|
+
|--------|-------------|
|
|
46
|
+
| `lookup(tax_id)` | Look up company by tax ID |
|
|
47
|
+
| `lookup_gb(company_number)` | Look up UK company by number |
|
|
48
|
+
| `validate(tax_id)` | Validate a VAT number |
|
|
49
|
+
| `batch(tax_ids)` | Batch lookup (max 10) |
|
|
50
|
+
| `countries()` | List supported countries |
|
|
51
|
+
| `health()` | API health check |
|
|
52
|
+
|
|
53
|
+
## Error Handling
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from vericorp.errors import InvalidTaxIdError, NotFoundError, RateLimitError
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
company = client.lookup("INVALID")
|
|
60
|
+
except InvalidTaxIdError:
|
|
61
|
+
print("Bad tax ID format")
|
|
62
|
+
except NotFoundError:
|
|
63
|
+
print("Company not found")
|
|
64
|
+
except RateLimitError as e:
|
|
65
|
+
print(f"Rate limited, retry after {e.retry_after}s")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vericorp"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python SDK for the VeriCorp API — European company verification"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "VeriCorp" }]
|
|
13
|
+
keywords = ["vat", "validation", "company", "europe", "api", "tax-id", "vies"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"httpx>=0.24",
|
|
28
|
+
"pydantic>=2.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/vericorptest-collab/vericorp-python"
|
|
33
|
+
Documentation = "https://rapidapi.com/vericorp/api/vericorp-api"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/vericorp"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .client import VeriCorp
|
|
2
|
+
from .async_client import AsyncVeriCorp
|
|
3
|
+
from .models import (
|
|
4
|
+
Company,
|
|
5
|
+
Address,
|
|
6
|
+
Contacts,
|
|
7
|
+
Director,
|
|
8
|
+
PSC,
|
|
9
|
+
ValidationResult,
|
|
10
|
+
CountriesResponse,
|
|
11
|
+
CountryInfo,
|
|
12
|
+
HealthResponse,
|
|
13
|
+
BatchResponse,
|
|
14
|
+
)
|
|
15
|
+
from .errors import VeriCorpError, RateLimitError, InvalidTaxIdError, NotFoundError, TimeoutError
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"VeriCorp",
|
|
19
|
+
"AsyncVeriCorp",
|
|
20
|
+
"Company",
|
|
21
|
+
"Address",
|
|
22
|
+
"Contacts",
|
|
23
|
+
"Director",
|
|
24
|
+
"PSC",
|
|
25
|
+
"ValidationResult",
|
|
26
|
+
"CountriesResponse",
|
|
27
|
+
"CountryInfo",
|
|
28
|
+
"HealthResponse",
|
|
29
|
+
"BatchResponse",
|
|
30
|
+
"VeriCorpError",
|
|
31
|
+
"RateLimitError",
|
|
32
|
+
"InvalidTaxIdError",
|
|
33
|
+
"NotFoundError",
|
|
34
|
+
"TimeoutError",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import asyncio
|
|
3
|
+
import httpx
|
|
4
|
+
from .models import Company, ValidationResult, CountriesResponse, HealthResponse, BatchResponse
|
|
5
|
+
from .errors import VeriCorpError, RateLimitError, InvalidTaxIdError, NotFoundError, TimeoutError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AsyncVeriCorp:
|
|
9
|
+
"""Async client for the VeriCorp API."""
|
|
10
|
+
|
|
11
|
+
BASE_URL = "https://vericorp-api.p.rapidapi.com"
|
|
12
|
+
|
|
13
|
+
def __init__(self, api_key: str, *, timeout: float = 10.0, max_retries: int = 2):
|
|
14
|
+
self._api_key = api_key
|
|
15
|
+
self._timeout = timeout
|
|
16
|
+
self._max_retries = max_retries
|
|
17
|
+
self._client = httpx.AsyncClient(
|
|
18
|
+
base_url=self.BASE_URL,
|
|
19
|
+
headers={
|
|
20
|
+
"X-RapidAPI-Key": api_key,
|
|
21
|
+
"X-RapidAPI-Host": "vericorp-api.p.rapidapi.com",
|
|
22
|
+
},
|
|
23
|
+
timeout=timeout,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
async def _request(self, method: str, path: str, **kwargs) -> httpx.Response:
|
|
27
|
+
last_error: Exception | None = None
|
|
28
|
+
for attempt in range(self._max_retries + 1):
|
|
29
|
+
try:
|
|
30
|
+
response = await self._client.request(method, path, **kwargs)
|
|
31
|
+
|
|
32
|
+
if response.status_code == 429:
|
|
33
|
+
retry_after = response.headers.get("Retry-After")
|
|
34
|
+
if attempt < self._max_retries:
|
|
35
|
+
wait = int(retry_after) if retry_after else (2 ** attempt)
|
|
36
|
+
await asyncio.sleep(wait)
|
|
37
|
+
continue
|
|
38
|
+
raise RateLimitError(int(retry_after) if retry_after else None)
|
|
39
|
+
|
|
40
|
+
if response.status_code == 503 and attempt < self._max_retries:
|
|
41
|
+
await asyncio.sleep(2 ** attempt)
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
return response
|
|
45
|
+
except httpx.TimeoutException:
|
|
46
|
+
last_error = TimeoutError()
|
|
47
|
+
if attempt < self._max_retries:
|
|
48
|
+
continue
|
|
49
|
+
raise last_error
|
|
50
|
+
except (RateLimitError, TimeoutError):
|
|
51
|
+
raise
|
|
52
|
+
except httpx.HTTPError as e:
|
|
53
|
+
last_error = VeriCorpError(str(e))
|
|
54
|
+
if attempt < self._max_retries:
|
|
55
|
+
continue
|
|
56
|
+
raise last_error
|
|
57
|
+
|
|
58
|
+
raise last_error or VeriCorpError("Request failed")
|
|
59
|
+
|
|
60
|
+
def _handle_error(self, response: httpx.Response, tax_id: str | None = None) -> None:
|
|
61
|
+
if response.status_code == 400:
|
|
62
|
+
raise InvalidTaxIdError(tax_id or "unknown")
|
|
63
|
+
if response.status_code == 404:
|
|
64
|
+
raise NotFoundError(tax_id or "unknown")
|
|
65
|
+
if response.status_code == 429:
|
|
66
|
+
retry_after = response.headers.get("Retry-After")
|
|
67
|
+
raise RateLimitError(int(retry_after) if retry_after else None)
|
|
68
|
+
if response.status_code >= 400:
|
|
69
|
+
raise VeriCorpError(f"API error: {response.status_code}", response.status_code)
|
|
70
|
+
|
|
71
|
+
async def lookup(self, tax_id: str, *, force_refresh: bool = False) -> Company:
|
|
72
|
+
params = {"force_refresh": "true"} if force_refresh else {}
|
|
73
|
+
response = await self._request("GET", f"/v1/company/{tax_id}", params=params)
|
|
74
|
+
if not response.is_success:
|
|
75
|
+
self._handle_error(response, tax_id)
|
|
76
|
+
return Company.model_validate(response.json())
|
|
77
|
+
|
|
78
|
+
async def lookup_gb(self, company_number: str, *, force_refresh: bool = False) -> Company:
|
|
79
|
+
params = {"force_refresh": "true"} if force_refresh else {}
|
|
80
|
+
response = await self._request("GET", f"/v1/company/gb/{company_number}", params=params)
|
|
81
|
+
if not response.is_success:
|
|
82
|
+
self._handle_error(response, company_number)
|
|
83
|
+
return Company.model_validate(response.json())
|
|
84
|
+
|
|
85
|
+
async def validate(self, tax_id: str) -> ValidationResult:
|
|
86
|
+
response = await self._request("GET", f"/v1/validate/{tax_id}")
|
|
87
|
+
if not response.is_success:
|
|
88
|
+
self._handle_error(response, tax_id)
|
|
89
|
+
return ValidationResult.model_validate(response.json())
|
|
90
|
+
|
|
91
|
+
async def batch(self, tax_ids: list[str]) -> BatchResponse:
|
|
92
|
+
response = await self._request("POST", "/v1/batch", json={"tax_ids": tax_ids})
|
|
93
|
+
if not response.is_success:
|
|
94
|
+
self._handle_error(response)
|
|
95
|
+
return BatchResponse.model_validate(response.json())
|
|
96
|
+
|
|
97
|
+
async def countries(self) -> CountriesResponse:
|
|
98
|
+
response = await self._request("GET", "/v1/countries")
|
|
99
|
+
if not response.is_success:
|
|
100
|
+
self._handle_error(response)
|
|
101
|
+
return CountriesResponse.model_validate(response.json())
|
|
102
|
+
|
|
103
|
+
async def health(self) -> HealthResponse:
|
|
104
|
+
response = await self._request("GET", "/v1/health")
|
|
105
|
+
if not response.is_success:
|
|
106
|
+
self._handle_error(response)
|
|
107
|
+
return HealthResponse.model_validate(response.json())
|
|
108
|
+
|
|
109
|
+
async def close(self) -> None:
|
|
110
|
+
await self._client.aclose()
|
|
111
|
+
|
|
112
|
+
async def __aenter__(self):
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
async def __aexit__(self, *args):
|
|
116
|
+
await self.close()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import httpx
|
|
3
|
+
from .models import Company, ValidationResult, CountriesResponse, HealthResponse, BatchResponse
|
|
4
|
+
from .errors import VeriCorpError, RateLimitError, InvalidTaxIdError, NotFoundError, TimeoutError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VeriCorp:
|
|
8
|
+
"""Synchronous client for the VeriCorp API."""
|
|
9
|
+
|
|
10
|
+
BASE_URL = "https://vericorp-api.p.rapidapi.com"
|
|
11
|
+
|
|
12
|
+
def __init__(self, api_key: str, *, timeout: float = 10.0, max_retries: int = 2):
|
|
13
|
+
self._api_key = api_key
|
|
14
|
+
self._timeout = timeout
|
|
15
|
+
self._max_retries = max_retries
|
|
16
|
+
self._client = httpx.Client(
|
|
17
|
+
base_url=self.BASE_URL,
|
|
18
|
+
headers={
|
|
19
|
+
"X-RapidAPI-Key": api_key,
|
|
20
|
+
"X-RapidAPI-Host": "vericorp-api.p.rapidapi.com",
|
|
21
|
+
},
|
|
22
|
+
timeout=timeout,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def _request(self, method: str, path: str, **kwargs) -> httpx.Response:
|
|
26
|
+
last_error: Exception | None = None
|
|
27
|
+
for attempt in range(self._max_retries + 1):
|
|
28
|
+
try:
|
|
29
|
+
response = self._client.request(method, path, **kwargs)
|
|
30
|
+
|
|
31
|
+
if response.status_code == 429:
|
|
32
|
+
retry_after = response.headers.get("Retry-After")
|
|
33
|
+
if attempt < self._max_retries:
|
|
34
|
+
import time
|
|
35
|
+
wait = int(retry_after) if retry_after else (2 ** attempt)
|
|
36
|
+
time.sleep(wait)
|
|
37
|
+
continue
|
|
38
|
+
raise RateLimitError(int(retry_after) if retry_after else None)
|
|
39
|
+
|
|
40
|
+
if response.status_code == 503 and attempt < self._max_retries:
|
|
41
|
+
import time
|
|
42
|
+
time.sleep(2 ** attempt)
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
return response
|
|
46
|
+
except httpx.TimeoutException:
|
|
47
|
+
last_error = TimeoutError()
|
|
48
|
+
if attempt < self._max_retries:
|
|
49
|
+
continue
|
|
50
|
+
raise last_error
|
|
51
|
+
except (RateLimitError, TimeoutError):
|
|
52
|
+
raise
|
|
53
|
+
except httpx.HTTPError as e:
|
|
54
|
+
last_error = VeriCorpError(str(e))
|
|
55
|
+
if attempt < self._max_retries:
|
|
56
|
+
continue
|
|
57
|
+
raise last_error
|
|
58
|
+
|
|
59
|
+
raise last_error or VeriCorpError("Request failed")
|
|
60
|
+
|
|
61
|
+
def _handle_error(self, response: httpx.Response, tax_id: str | None = None) -> None:
|
|
62
|
+
if response.status_code == 400:
|
|
63
|
+
raise InvalidTaxIdError(tax_id or "unknown")
|
|
64
|
+
if response.status_code == 404:
|
|
65
|
+
raise NotFoundError(tax_id or "unknown")
|
|
66
|
+
if response.status_code == 429:
|
|
67
|
+
retry_after = response.headers.get("Retry-After")
|
|
68
|
+
raise RateLimitError(int(retry_after) if retry_after else None)
|
|
69
|
+
if response.status_code >= 400:
|
|
70
|
+
raise VeriCorpError(f"API error: {response.status_code}", response.status_code)
|
|
71
|
+
|
|
72
|
+
def lookup(self, tax_id: str, *, force_refresh: bool = False) -> Company:
|
|
73
|
+
params = {"force_refresh": "true"} if force_refresh else {}
|
|
74
|
+
response = self._request("GET", f"/v1/company/{tax_id}", params=params)
|
|
75
|
+
if not response.is_success:
|
|
76
|
+
self._handle_error(response, tax_id)
|
|
77
|
+
return Company.model_validate(response.json())
|
|
78
|
+
|
|
79
|
+
def lookup_gb(self, company_number: str, *, force_refresh: bool = False) -> Company:
|
|
80
|
+
params = {"force_refresh": "true"} if force_refresh else {}
|
|
81
|
+
response = self._request("GET", f"/v1/company/gb/{company_number}", params=params)
|
|
82
|
+
if not response.is_success:
|
|
83
|
+
self._handle_error(response, company_number)
|
|
84
|
+
return Company.model_validate(response.json())
|
|
85
|
+
|
|
86
|
+
def validate(self, tax_id: str) -> ValidationResult:
|
|
87
|
+
response = self._request("GET", f"/v1/validate/{tax_id}")
|
|
88
|
+
if not response.is_success:
|
|
89
|
+
self._handle_error(response, tax_id)
|
|
90
|
+
return ValidationResult.model_validate(response.json())
|
|
91
|
+
|
|
92
|
+
def batch(self, tax_ids: list[str]) -> BatchResponse:
|
|
93
|
+
response = self._request("POST", "/v1/batch", json={"tax_ids": tax_ids})
|
|
94
|
+
if not response.is_success:
|
|
95
|
+
self._handle_error(response)
|
|
96
|
+
return BatchResponse.model_validate(response.json())
|
|
97
|
+
|
|
98
|
+
def countries(self) -> CountriesResponse:
|
|
99
|
+
response = self._request("GET", "/v1/countries")
|
|
100
|
+
if not response.is_success:
|
|
101
|
+
self._handle_error(response)
|
|
102
|
+
return CountriesResponse.model_validate(response.json())
|
|
103
|
+
|
|
104
|
+
def health(self) -> HealthResponse:
|
|
105
|
+
response = self._request("GET", "/v1/health")
|
|
106
|
+
if not response.is_success:
|
|
107
|
+
self._handle_error(response)
|
|
108
|
+
return HealthResponse.model_validate(response.json())
|
|
109
|
+
|
|
110
|
+
def close(self) -> None:
|
|
111
|
+
self._client.close()
|
|
112
|
+
|
|
113
|
+
def __enter__(self):
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def __exit__(self, *args):
|
|
117
|
+
self.close()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
class VeriCorpError(Exception):
|
|
2
|
+
"""Base error for VeriCorp API."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, message: str, status_code: int | None = None):
|
|
5
|
+
super().__init__(message)
|
|
6
|
+
self.status_code = status_code
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RateLimitError(VeriCorpError):
|
|
10
|
+
"""Raised when API rate limit is exceeded (429)."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, retry_after: int | None = None):
|
|
13
|
+
super().__init__("Rate limit exceeded", 429)
|
|
14
|
+
self.retry_after = retry_after
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InvalidTaxIdError(VeriCorpError):
|
|
18
|
+
"""Raised when the tax ID format is invalid (400)."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, tax_id: str):
|
|
21
|
+
super().__init__(f"Invalid tax ID: {tax_id}", 400)
|
|
22
|
+
self.tax_id = tax_id
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NotFoundError(VeriCorpError):
|
|
26
|
+
"""Raised when a company is not found (404)."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, tax_id: str):
|
|
29
|
+
super().__init__(f"Company not found: {tax_id}", 404)
|
|
30
|
+
self.tax_id = tax_id
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TimeoutError(VeriCorpError):
|
|
34
|
+
"""Raised when a request times out."""
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
super().__init__("Request timed out")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Address(BaseModel):
|
|
7
|
+
street: Optional[str] = None
|
|
8
|
+
city: Optional[str] = None
|
|
9
|
+
postal_code: Optional[str] = None
|
|
10
|
+
region: Optional[str] = None
|
|
11
|
+
country: Optional[str] = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Contacts(BaseModel):
|
|
15
|
+
phone: Optional[str] = None
|
|
16
|
+
fax: Optional[str] = None
|
|
17
|
+
email: Optional[str] = None
|
|
18
|
+
website: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Director(BaseModel):
|
|
22
|
+
name: Optional[str] = None
|
|
23
|
+
role: Optional[str] = None
|
|
24
|
+
appointed_on: Optional[str] = None
|
|
25
|
+
resigned_on: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PSC(BaseModel):
|
|
29
|
+
name: Optional[str] = None
|
|
30
|
+
natures_of_control: Optional[list[str]] = None
|
|
31
|
+
notified_on: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Company(BaseModel):
|
|
35
|
+
tax_id: str
|
|
36
|
+
country: str
|
|
37
|
+
name: str
|
|
38
|
+
address: Optional[Address] = None
|
|
39
|
+
legal_form: Optional[str] = None
|
|
40
|
+
status: Optional[str] = None
|
|
41
|
+
incorporation_date: Optional[str] = None
|
|
42
|
+
activity_code: Optional[str] = None
|
|
43
|
+
activity_description: Optional[str] = None
|
|
44
|
+
capital: Optional[float] = None
|
|
45
|
+
capital_currency: Optional[str] = None
|
|
46
|
+
contacts: Optional[Contacts] = None
|
|
47
|
+
directors: Optional[list[Director]] = None
|
|
48
|
+
psc: Optional[list[PSC]] = None
|
|
49
|
+
vat_valid: Optional[bool] = None
|
|
50
|
+
vat_consultation_number: Optional[str] = None
|
|
51
|
+
source: str
|
|
52
|
+
data_quality: str
|
|
53
|
+
cached: bool
|
|
54
|
+
stale: Optional[bool] = None
|
|
55
|
+
data_age_days: Optional[int] = None
|
|
56
|
+
fetched_at: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ValidationResult(BaseModel):
|
|
60
|
+
tax_id: str
|
|
61
|
+
country: str
|
|
62
|
+
format_valid: bool
|
|
63
|
+
vat_valid: Optional[bool] = None
|
|
64
|
+
company_name: Optional[str] = None
|
|
65
|
+
consultation_number: Optional[str] = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class CountryInfo(BaseModel):
|
|
69
|
+
code: str
|
|
70
|
+
name: str
|
|
71
|
+
enriched: bool
|
|
72
|
+
source: Optional[str] = None
|
|
73
|
+
vat_validation: bool
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class CountriesResponse(BaseModel):
|
|
77
|
+
total: int
|
|
78
|
+
enriched: int
|
|
79
|
+
vat_validation: int
|
|
80
|
+
countries: list[CountryInfo]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SourceHealth(BaseModel):
|
|
84
|
+
status: str
|
|
85
|
+
latency_ms: Optional[int] = None
|
|
86
|
+
budget_remaining: Optional[int] = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CacheHealth(BaseModel):
|
|
90
|
+
status: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class HealthResponse(BaseModel):
|
|
94
|
+
status: str
|
|
95
|
+
timestamp: str
|
|
96
|
+
sources: Optional[dict[str, SourceHealth]] = None
|
|
97
|
+
cache: Optional[CacheHealth] = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class BatchError(BaseModel):
|
|
101
|
+
tax_id: str
|
|
102
|
+
error: str
|
|
103
|
+
code: str
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class BatchResponse(BaseModel):
|
|
107
|
+
total: int
|
|
108
|
+
successful: int
|
|
109
|
+
results: list[Company | BatchError]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import httpx
|
|
3
|
+
from unittest.mock import patch, MagicMock, AsyncMock
|
|
4
|
+
from vericorp import AsyncVeriCorp, Company, ValidationResult
|
|
5
|
+
from vericorp.errors import InvalidTaxIdError, RateLimitError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
COMPANY_JSON = {
|
|
9
|
+
"tax_id": "PT502011378",
|
|
10
|
+
"country": "PT",
|
|
11
|
+
"name": "UNIVERSIDADE DO MINHO",
|
|
12
|
+
"address": {"street": "LG DO PACO", "city": "BRAGA", "postal_code": "4700-320", "country": "PT"},
|
|
13
|
+
"source": "nifpt",
|
|
14
|
+
"data_quality": "full",
|
|
15
|
+
"cached": True,
|
|
16
|
+
"fetched_at": "2026-01-01T00:00:00Z",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
VALIDATION_JSON = {
|
|
20
|
+
"tax_id": "PT502011378",
|
|
21
|
+
"country": "PT",
|
|
22
|
+
"format_valid": True,
|
|
23
|
+
"vat_valid": True,
|
|
24
|
+
"company_name": "UNIVERSIDADE DO MINHO",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def mock_response(status_code: int = 200, json_data: dict | None = None, headers: dict | None = None) -> MagicMock:
|
|
29
|
+
response = MagicMock(spec=httpx.Response)
|
|
30
|
+
response.status_code = status_code
|
|
31
|
+
response.is_success = 200 <= status_code < 300
|
|
32
|
+
response.json.return_value = json_data or {}
|
|
33
|
+
response.headers = headers or {}
|
|
34
|
+
return response
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.asyncio
|
|
38
|
+
class TestAsyncVeriCorp:
|
|
39
|
+
async def test_lookup(self):
|
|
40
|
+
client = AsyncVeriCorp("test-key")
|
|
41
|
+
with patch.object(client._client, "request", new_callable=AsyncMock, return_value=mock_response(200, COMPANY_JSON)):
|
|
42
|
+
result = await client.lookup("PT502011378")
|
|
43
|
+
assert isinstance(result, Company)
|
|
44
|
+
assert result.name == "UNIVERSIDADE DO MINHO"
|
|
45
|
+
|
|
46
|
+
async def test_validate(self):
|
|
47
|
+
client = AsyncVeriCorp("test-key")
|
|
48
|
+
with patch.object(client._client, "request", new_callable=AsyncMock, return_value=mock_response(200, VALIDATION_JSON)):
|
|
49
|
+
result = await client.validate("PT502011378")
|
|
50
|
+
assert isinstance(result, ValidationResult)
|
|
51
|
+
assert result.vat_valid is True
|
|
52
|
+
|
|
53
|
+
async def test_invalid_tax_id(self):
|
|
54
|
+
client = AsyncVeriCorp("test-key")
|
|
55
|
+
with patch.object(client._client, "request", new_callable=AsyncMock, return_value=mock_response(400)):
|
|
56
|
+
with pytest.raises(InvalidTaxIdError):
|
|
57
|
+
await client.lookup("INVALID")
|
|
58
|
+
|
|
59
|
+
async def test_rate_limit(self):
|
|
60
|
+
client = AsyncVeriCorp("test-key", max_retries=0)
|
|
61
|
+
with patch.object(client._client, "request", new_callable=AsyncMock, return_value=mock_response(429, headers={"Retry-After": "10"})):
|
|
62
|
+
with pytest.raises(RateLimitError):
|
|
63
|
+
await client.lookup("PT502011378")
|
|
64
|
+
|
|
65
|
+
async def test_async_context_manager(self):
|
|
66
|
+
async with AsyncVeriCorp("test-key") as client:
|
|
67
|
+
assert client._client is not None
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import httpx
|
|
3
|
+
from unittest.mock import patch, MagicMock
|
|
4
|
+
from vericorp import VeriCorp, Company, ValidationResult, CountriesResponse, HealthResponse, BatchResponse
|
|
5
|
+
from vericorp.errors import InvalidTaxIdError, NotFoundError, RateLimitError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
COMPANY_JSON = {
|
|
9
|
+
"tax_id": "PT502011378",
|
|
10
|
+
"country": "PT",
|
|
11
|
+
"name": "UNIVERSIDADE DO MINHO",
|
|
12
|
+
"address": {"street": "LG DO PACO", "city": "BRAGA", "postal_code": "4700-320", "country": "PT"},
|
|
13
|
+
"source": "nifpt",
|
|
14
|
+
"data_quality": "full",
|
|
15
|
+
"cached": True,
|
|
16
|
+
"fetched_at": "2026-01-01T00:00:00Z",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
VALIDATION_JSON = {
|
|
20
|
+
"tax_id": "PT502011378",
|
|
21
|
+
"country": "PT",
|
|
22
|
+
"format_valid": True,
|
|
23
|
+
"vat_valid": True,
|
|
24
|
+
"company_name": "UNIVERSIDADE DO MINHO",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
COUNTRIES_JSON = {
|
|
28
|
+
"total": 29,
|
|
29
|
+
"enriched": 8,
|
|
30
|
+
"vat_validation": 28,
|
|
31
|
+
"countries": [{"code": "PT", "name": "Portugal", "enriched": True, "source": "nifpt", "vat_validation": True}],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
HEALTH_JSON = {
|
|
35
|
+
"status": "healthy",
|
|
36
|
+
"timestamp": "2026-01-01T00:00:00Z",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
BATCH_JSON = {
|
|
40
|
+
"total": 1,
|
|
41
|
+
"successful": 1,
|
|
42
|
+
"results": [COMPANY_JSON],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def mock_response(status_code: int = 200, json_data: dict | None = None, headers: dict | None = None) -> httpx.Response:
|
|
47
|
+
response = MagicMock(spec=httpx.Response)
|
|
48
|
+
response.status_code = status_code
|
|
49
|
+
response.is_success = 200 <= status_code < 300
|
|
50
|
+
response.json.return_value = json_data or {}
|
|
51
|
+
response.headers = headers or {}
|
|
52
|
+
return response
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestVeriCorp:
|
|
56
|
+
def test_lookup(self):
|
|
57
|
+
client = VeriCorp("test-key")
|
|
58
|
+
with patch.object(client._client, "request", return_value=mock_response(200, COMPANY_JSON)):
|
|
59
|
+
result = client.lookup("PT502011378")
|
|
60
|
+
assert isinstance(result, Company)
|
|
61
|
+
assert result.name == "UNIVERSIDADE DO MINHO"
|
|
62
|
+
assert result.country == "PT"
|
|
63
|
+
|
|
64
|
+
def test_lookup_gb(self):
|
|
65
|
+
client = VeriCorp("test-key")
|
|
66
|
+
gb_json = {**COMPANY_JSON, "tax_id": "GB00445790", "country": "GB", "source": "companies_house"}
|
|
67
|
+
with patch.object(client._client, "request", return_value=mock_response(200, gb_json)):
|
|
68
|
+
result = client.lookup_gb("00445790")
|
|
69
|
+
assert result.source == "companies_house"
|
|
70
|
+
|
|
71
|
+
def test_validate(self):
|
|
72
|
+
client = VeriCorp("test-key")
|
|
73
|
+
with patch.object(client._client, "request", return_value=mock_response(200, VALIDATION_JSON)):
|
|
74
|
+
result = client.validate("PT502011378")
|
|
75
|
+
assert isinstance(result, ValidationResult)
|
|
76
|
+
assert result.format_valid is True
|
|
77
|
+
assert result.vat_valid is True
|
|
78
|
+
|
|
79
|
+
def test_batch(self):
|
|
80
|
+
client = VeriCorp("test-key")
|
|
81
|
+
with patch.object(client._client, "request", return_value=mock_response(200, BATCH_JSON)):
|
|
82
|
+
result = client.batch(["PT502011378"])
|
|
83
|
+
assert isinstance(result, BatchResponse)
|
|
84
|
+
assert result.total == 1
|
|
85
|
+
|
|
86
|
+
def test_countries(self):
|
|
87
|
+
client = VeriCorp("test-key")
|
|
88
|
+
with patch.object(client._client, "request", return_value=mock_response(200, COUNTRIES_JSON)):
|
|
89
|
+
result = client.countries()
|
|
90
|
+
assert isinstance(result, CountriesResponse)
|
|
91
|
+
assert result.total == 29
|
|
92
|
+
|
|
93
|
+
def test_health(self):
|
|
94
|
+
client = VeriCorp("test-key")
|
|
95
|
+
with patch.object(client._client, "request", return_value=mock_response(200, HEALTH_JSON)):
|
|
96
|
+
result = client.health()
|
|
97
|
+
assert isinstance(result, HealthResponse)
|
|
98
|
+
assert result.status == "healthy"
|
|
99
|
+
|
|
100
|
+
def test_invalid_tax_id(self):
|
|
101
|
+
client = VeriCorp("test-key")
|
|
102
|
+
with patch.object(client._client, "request", return_value=mock_response(400)):
|
|
103
|
+
with pytest.raises(InvalidTaxIdError):
|
|
104
|
+
client.lookup("INVALID")
|
|
105
|
+
|
|
106
|
+
def test_not_found(self):
|
|
107
|
+
client = VeriCorp("test-key")
|
|
108
|
+
with patch.object(client._client, "request", return_value=mock_response(404)):
|
|
109
|
+
with pytest.raises(NotFoundError):
|
|
110
|
+
client.lookup("PT000000000")
|
|
111
|
+
|
|
112
|
+
def test_rate_limit(self):
|
|
113
|
+
client = VeriCorp("test-key", max_retries=0)
|
|
114
|
+
with patch.object(client._client, "request", return_value=mock_response(429, headers={"Retry-After": "30"})):
|
|
115
|
+
with pytest.raises(RateLimitError) as exc_info:
|
|
116
|
+
client.lookup("PT502011378")
|
|
117
|
+
assert exc_info.value.retry_after == 30
|
|
118
|
+
|
|
119
|
+
def test_context_manager(self):
|
|
120
|
+
with VeriCorp("test-key") as client:
|
|
121
|
+
assert client._client is not None
|
|
122
|
+
|
|
123
|
+
def test_custom_timeout(self):
|
|
124
|
+
client = VeriCorp("test-key", timeout=5.0)
|
|
125
|
+
assert client._timeout == 5.0
|