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.
@@ -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 }}
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ dist/
4
+ build/
5
+ .pytest_cache/
6
+ .venv/
@@ -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
@@ -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