stackin-python-sdk 0.8.0__tar.gz → 0.10.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackin-python-sdk
3
- Version: 0.8.0
3
+ Version: 0.10.0
4
4
  Summary: Python SDK for issuing, consulting and cancelling electronic invoices.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -137,7 +137,7 @@ invoice = client.issue(
137
137
  document_type=DocumentType.NFSE,
138
138
  client_name="John Doe",
139
139
  tax_id="00000000000",
140
- items=[Product(description="Software development", amount=5000.00)],
140
+ items=[Product(description="Software development", unit_price=Decimal("5000.00"))],
141
141
  )
142
142
 
143
143
  status = client.consult("ACCESS_KEY...", document_type=DocumentType.NFSE)
@@ -194,13 +194,15 @@ burned, and undoing it means cancelling, which has a deadline.
194
194
  Pass `idempotency_key` to make the retry safe:
195
195
 
196
196
  ```python
197
+ from decimal import Decimal
198
+
197
199
  key = str(uuid.uuid4())
198
200
 
199
201
  result = invoice.issue(
200
202
  document_type=DocumentType.NFSE,
201
203
  client_name="Maria Silva",
202
204
  tax_id="12345678909",
203
- items=[Product(description="Consultoria", amount=1500.00)],
205
+ items=[Product(description="Consultoria", unit_price=Decimal("1500.00"))],
204
206
  idempotency_key=key,
205
207
  )
206
208
  ```
@@ -311,6 +313,37 @@ Realizada. Only the last one takes a reason, and it requires one — both rules
311
313
  checked locally, before the request goes out, because a round trip to be told a
312
314
  fixed rule is a round trip wasted.
313
315
 
316
+ ## Looking up a code, or who a CNPJ belongs to
317
+
318
+ Two more clients, for the tables an issuer reads while filling a document. Neither writes anything.
319
+
320
+ ```python
321
+ from stackin import FiscalReference, Taxpayer
322
+
323
+ ref = FiscalReference(api_key="...") # country="BR" by default
324
+
325
+ ref.ncm.get("84716052") # one code
326
+ ref.ncm.search("teclado", limit=5) # a page of matches
327
+ ref.cfop.get("5102")
328
+ ref.kinds() # what this country has
329
+ ref.kind("ibs_cbs_class").get("000001") # any kind, named or not
330
+ ref.search("teclado") # every kind at once
331
+
332
+ Taxpayer(api_key="...").get("00000000000191")
333
+ ```
334
+
335
+ `cfop`, `ncm`, `cest`, `cst`, `csosn`, `iss_service`, `icms_fuel` and `ibs_cbs_class` have accessors. **`kind(name)` reaches anything else**, including a classification published after this release — ask `kinds()` rather than trusting this list.
336
+
337
+ `metadata` is passed through as the API sends it and differs per kind: `utrib` on an NCM, `ncm_code` on a CEST, `tax_type` on a CST, `None` on an ISS service.
338
+
339
+ Three things worth knowing before you loop:
340
+
341
+ - **These share the invoice read allowance** — 600 calls a minute per key, the same bucket `consult()`, `history()`, `received()` and `pdf()` draw from. One `search()` page beats N `get()` calls.
342
+ - **Ordering is fixed** (kind, then code, ascending). Unlike `history()`, these take no `sort_by`/`order_by`.
343
+ - **A 404 from `Taxpayer` does not mean the company does not exist.** That registry reloads monthly, so a recently registered CNPJ is simply not in it yet. Do not build a validation rule on it.
344
+
345
+ `Taxpayer` has one method and keeps one: the registry holds the names and addresses of real people, so there is no search over it, by design.
346
+
314
347
  ## Errors
315
348
 
316
349
  - `stackin.APIError` — the API responded with a non-2xx status (`status_code`, `detail`) — a 401 here means `api_key` is missing, wrong, or was rotated.
@@ -321,5 +354,5 @@ Building the full fiscal document (issuer data, service code, tax groups, schema
321
354
 
322
355
  ## Examples
323
356
 
324
- Runnable end-to-end scripts in [`examples/nfe/`](examples/nfe/) and [`examples/nfse/`](examples/nfse/) — one file per field/variant, from the bare minimum to every field filled. `examples/consult_invoice.py`, `examples/cancel_invoice.py`, and `examples/reissue_invoice.py` cover the operations that act on an already-issued document.
357
+ Runnable end-to-end scripts in [`examples/nfe/`](examples/nfe/) and [`examples/nfse/`](examples/nfse/) — one file per field/variant, from the bare minimum to every field filled. `examples/consult_invoice.py`, `examples/cancel_invoice.py`, and `examples/reissue_invoice.py` cover the operations that act on an already-issued document. `examples/lookup_fiscal_reference.py` and `examples/lookup_taxpayer.py` cover the two read-only clients.
325
358
 
@@ -108,7 +108,7 @@ invoice = client.issue(
108
108
  document_type=DocumentType.NFSE,
109
109
  client_name="John Doe",
110
110
  tax_id="00000000000",
111
- items=[Product(description="Software development", amount=5000.00)],
111
+ items=[Product(description="Software development", unit_price=Decimal("5000.00"))],
112
112
  )
113
113
 
114
114
  status = client.consult("ACCESS_KEY...", document_type=DocumentType.NFSE)
@@ -165,13 +165,15 @@ burned, and undoing it means cancelling, which has a deadline.
165
165
  Pass `idempotency_key` to make the retry safe:
166
166
 
167
167
  ```python
168
+ from decimal import Decimal
169
+
168
170
  key = str(uuid.uuid4())
169
171
 
170
172
  result = invoice.issue(
171
173
  document_type=DocumentType.NFSE,
172
174
  client_name="Maria Silva",
173
175
  tax_id="12345678909",
174
- items=[Product(description="Consultoria", amount=1500.00)],
176
+ items=[Product(description="Consultoria", unit_price=Decimal("1500.00"))],
175
177
  idempotency_key=key,
176
178
  )
177
179
  ```
@@ -282,6 +284,37 @@ Realizada. Only the last one takes a reason, and it requires one — both rules
282
284
  checked locally, before the request goes out, because a round trip to be told a
283
285
  fixed rule is a round trip wasted.
284
286
 
287
+ ## Looking up a code, or who a CNPJ belongs to
288
+
289
+ Two more clients, for the tables an issuer reads while filling a document. Neither writes anything.
290
+
291
+ ```python
292
+ from stackin import FiscalReference, Taxpayer
293
+
294
+ ref = FiscalReference(api_key="...") # country="BR" by default
295
+
296
+ ref.ncm.get("84716052") # one code
297
+ ref.ncm.search("teclado", limit=5) # a page of matches
298
+ ref.cfop.get("5102")
299
+ ref.kinds() # what this country has
300
+ ref.kind("ibs_cbs_class").get("000001") # any kind, named or not
301
+ ref.search("teclado") # every kind at once
302
+
303
+ Taxpayer(api_key="...").get("00000000000191")
304
+ ```
305
+
306
+ `cfop`, `ncm`, `cest`, `cst`, `csosn`, `iss_service`, `icms_fuel` and `ibs_cbs_class` have accessors. **`kind(name)` reaches anything else**, including a classification published after this release — ask `kinds()` rather than trusting this list.
307
+
308
+ `metadata` is passed through as the API sends it and differs per kind: `utrib` on an NCM, `ncm_code` on a CEST, `tax_type` on a CST, `None` on an ISS service.
309
+
310
+ Three things worth knowing before you loop:
311
+
312
+ - **These share the invoice read allowance** — 600 calls a minute per key, the same bucket `consult()`, `history()`, `received()` and `pdf()` draw from. One `search()` page beats N `get()` calls.
313
+ - **Ordering is fixed** (kind, then code, ascending). Unlike `history()`, these take no `sort_by`/`order_by`.
314
+ - **A 404 from `Taxpayer` does not mean the company does not exist.** That registry reloads monthly, so a recently registered CNPJ is simply not in it yet. Do not build a validation rule on it.
315
+
316
+ `Taxpayer` has one method and keeps one: the registry holds the names and addresses of real people, so there is no search over it, by design.
317
+
285
318
  ## Errors
286
319
 
287
320
  - `stackin.APIError` — the API responded with a non-2xx status (`status_code`, `detail`) — a 401 here means `api_key` is missing, wrong, or was rotated.
@@ -292,4 +325,4 @@ Building the full fiscal document (issuer data, service code, tax groups, schema
292
325
 
293
326
  ## Examples
294
327
 
295
- Runnable end-to-end scripts in [`examples/nfe/`](examples/nfe/) and [`examples/nfse/`](examples/nfse/) — one file per field/variant, from the bare minimum to every field filled. `examples/consult_invoice.py`, `examples/cancel_invoice.py`, and `examples/reissue_invoice.py` cover the operations that act on an already-issued document.
328
+ Runnable end-to-end scripts in [`examples/nfe/`](examples/nfe/) and [`examples/nfse/`](examples/nfse/) — one file per field/variant, from the bare minimum to every field filled. `examples/consult_invoice.py`, `examples/cancel_invoice.py`, and `examples/reissue_invoice.py` cover the operations that act on an already-issued document. `examples/lookup_fiscal_reference.py` and `examples/lookup_taxpayer.py` cover the two read-only clients.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stackin-python-sdk"
3
- version = "0.8.0"
3
+ version = "0.10.0"
4
4
  authors = [
5
5
  { name = "Fernando Celmer", email = "email@fernandocelmer.com" },
6
6
  ]
@@ -45,7 +45,7 @@ Changelog = "https://github.com/stackin-io/stackin-python-sdk/releases"
45
45
 
46
46
  [tool.poetry]
47
47
  name = "stackin-python-sdk"
48
- version = "0.8.0"
48
+ version = "0.10.0"
49
49
  description = "Python SDK for issuing, consulting and cancelling electronic invoices."
50
50
  authors = ["Fernando Celmer <email@fernandocelmer.com>"]
51
51
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  """Invoice __init__ module."""
2
2
 
3
- __version__ = "0.8.0"
3
+ __version__ = "0.10.0"
4
4
  __description__ = (
5
5
  "Python SDK for issuing, consulting and cancelling electronic invoices."
6
6
  )
@@ -8,10 +8,16 @@ __description__ = (
8
8
  from .core.address import Address
9
9
  from .core.client import Invoice
10
10
  from .core.exceptions import APIError, ConnectionFailedError, InvoiceError
11
+ from .core.reference import KINDS, FiscalReference, Kind
12
+ from .core.taxpayer import Taxpayer
11
13
  from .core.types import DocumentType, Environment, Manifestation
12
14
 
13
15
  __all__ = [
14
16
  "Invoice",
17
+ "FiscalReference",
18
+ "Kind",
19
+ "KINDS",
20
+ "Taxpayer",
15
21
  "DocumentType",
16
22
  "Environment",
17
23
  "Manifestation",
@@ -1,6 +1,6 @@
1
1
  """Brazil-specific document fields."""
2
2
 
3
- from stackin.br.product import PresumedCredit, Product
3
+ from stackin.br.product import IbsCbs, PresumedCredit, Product
4
4
  from stackin.br.tax import (
5
5
  CofinsAliq,
6
6
  CofinsNt,
@@ -42,6 +42,7 @@ from stackin.br.tax import (
42
42
 
43
43
  __all__ = [
44
44
  "Product",
45
+ "IbsCbs",
45
46
  "PresumedCredit",
46
47
  "Tax",
47
48
  "Icms00",
@@ -10,6 +10,17 @@ from pydantic import BaseModel, Field
10
10
  from stackin.br.tax import Tax
11
11
 
12
12
 
13
+ class IbsCbs(BaseModel):
14
+ """The Reforma Tributária group for one item."""
15
+
16
+ cst: str = Field(pattern=r"^\d{3}$")
17
+ classification: str = Field(pattern=r"^\d{6}$")
18
+ base: float | None = Field(default=None, gt=0)
19
+ rate_state: float = Field(ge=0)
20
+ rate_city: float = Field(ge=0)
21
+ rate_federal: float = Field(ge=0)
22
+
23
+
13
24
  class PresumedCredit(BaseModel):
14
25
  """A presumed tax credit applied to this item."""
15
26
 
@@ -33,6 +44,7 @@ _BR_FIELDS = {
33
44
  "import_content_control_number",
34
45
  "recopi_number",
35
46
  "extra_groups",
47
+ "ibs_cbs",
36
48
  "tax",
37
49
  }
38
50
 
@@ -80,6 +92,7 @@ class Product(BaseModel):
80
92
  ),
81
93
  )
82
94
  recopi_number: str | None = Field(default=None, pattern=r"^\d{20}$")
95
+ ibs_cbs: IbsCbs | None = Field(default=None)
83
96
  extra_groups: dict[str, Any] | None = Field(default=None)
84
97
  tax: Tax | dict[str, Any] | None = Field(default=None)
85
98
 
@@ -63,8 +63,13 @@ def _validate_nfe_address(address: Address | None) -> None:
63
63
  )
64
64
 
65
65
 
66
- class Invoice:
67
- """Client for issuing, consulting, and cancelling fiscal documents."""
66
+ class _Client:
67
+ """Where an api_key becomes an HTTP call, for every entry point.
68
+
69
+ Lives here rather than in a module of its own because the tests
70
+ patch `stackin.core.client.requests`, and so every request the SDK
71
+ makes has to be issued from this module.
72
+ """
68
73
 
69
74
  def __init__(
70
75
  self,
@@ -78,6 +83,77 @@ class Invoice:
78
83
  self.api_key = api_key or os.environ.get("STACKIN_API_KEY")
79
84
  self.timeout = timeout
80
85
 
86
+ def _headers(self, idempotency_key: str | None = None) -> dict:
87
+ headers = {}
88
+ if self.api_key:
89
+ headers["Authorization"] = f"Bearer {self.api_key}"
90
+ if idempotency_key:
91
+ headers["Idempotency-Key"] = idempotency_key
92
+ return headers
93
+
94
+ def _request(
95
+ self,
96
+ method: str,
97
+ path: str,
98
+ *,
99
+ json: dict | None = None,
100
+ params: dict | None = None,
101
+ idempotency_key: str | None = None,
102
+ ) -> Any:
103
+ response = self._send(
104
+ method,
105
+ path,
106
+ json=json,
107
+ params=params,
108
+ idempotency_key=idempotency_key,
109
+ )
110
+ try:
111
+ body = response.json() if response.content else {}
112
+ except ValueError:
113
+ body = {}
114
+ if isinstance(body, dict):
115
+ return body.get("result", body)
116
+ return body
117
+
118
+ def _send(
119
+ self,
120
+ method: str,
121
+ path: str,
122
+ *,
123
+ json: dict | None = None,
124
+ params: dict | None = None,
125
+ idempotency_key: str | None = None,
126
+ ) -> requests.Response:
127
+ url = f"{self.base_url}/api/v1{path}"
128
+
129
+ try:
130
+ response = requests.request(
131
+ method,
132
+ url,
133
+ json=json,
134
+ params=params,
135
+ headers=self._headers(idempotency_key),
136
+ timeout=self.timeout,
137
+ )
138
+ except requests.RequestException as error:
139
+ raise ConnectionFailedError(str(error)) from error
140
+
141
+ if not response.ok:
142
+ try:
143
+ body = response.json() if response.content else {}
144
+ except ValueError:
145
+ body = {}
146
+ raise APIError(
147
+ status_code=response.status_code,
148
+ detail=body.get("detail", response.text),
149
+ )
150
+
151
+ return response
152
+
153
+
154
+ class Invoice(_Client):
155
+ """Client for issuing, consulting, and cancelling fiscal documents."""
156
+
81
157
  def issue(
82
158
  self,
83
159
  *,
@@ -328,70 +404,3 @@ class Invoice:
328
404
  f"/received-invoices/{access_key}/manifestation",
329
405
  json=payload,
330
406
  )
331
-
332
- def _headers(self, idempotency_key: str | None = None) -> dict:
333
- headers = {}
334
- if self.api_key:
335
- headers["Authorization"] = f"Bearer {self.api_key}"
336
- if idempotency_key:
337
- headers["Idempotency-Key"] = idempotency_key
338
- return headers
339
-
340
- def _request(
341
- self,
342
- method: str,
343
- path: str,
344
- *,
345
- json: dict | None = None,
346
- params: dict | None = None,
347
- idempotency_key: str | None = None,
348
- ) -> Any:
349
- response = self._send(
350
- method,
351
- path,
352
- json=json,
353
- params=params,
354
- idempotency_key=idempotency_key,
355
- )
356
- try:
357
- body = response.json() if response.content else {}
358
- except ValueError:
359
- body = {}
360
- if isinstance(body, dict):
361
- return body.get("result", body)
362
- return body
363
-
364
- def _send(
365
- self,
366
- method: str,
367
- path: str,
368
- *,
369
- json: dict | None = None,
370
- params: dict | None = None,
371
- idempotency_key: str | None = None,
372
- ) -> requests.Response:
373
- url = f"{self.base_url}/api/v1{path}"
374
-
375
- try:
376
- response = requests.request(
377
- method,
378
- url,
379
- json=json,
380
- params=params,
381
- headers=self._headers(idempotency_key),
382
- timeout=self.timeout,
383
- )
384
- except requests.RequestException as error:
385
- raise ConnectionFailedError(str(error)) from error
386
-
387
- if not response.ok:
388
- try:
389
- body = response.json() if response.content else {}
390
- except ValueError:
391
- body = {}
392
- raise APIError(
393
- status_code=response.status_code,
394
- detail=body.get("detail", response.text),
395
- )
396
-
397
- return response
@@ -0,0 +1,173 @@
1
+ """The published classification tables — CFOP, NCM, CEST and the rest.
2
+
3
+ None of this is the company's own data and none of it is writable. The
4
+ eight named accessors are ergonomics; `kind()` is the contract, and it
5
+ reaches a classification published after this release without one.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, cast
11
+ from urllib.parse import quote
12
+
13
+ from stackin.core.client import _Client
14
+ from stackin.core.types import Environment
15
+
16
+
17
+ def _segment(value: str) -> str:
18
+ """One path segment, escaped, refusing what would leave it.
19
+
20
+ `safe=""` because a code the caller typed may hold a `/`. The dot
21
+ segments need refusing rather than escaping: RFC 3986 calls `.`
22
+ unreserved, so `quote` leaves it alone and the HTTP client then
23
+ collapses `..` into a different endpoint.
24
+ """
25
+ if value in ("", ".", ".."):
26
+ raise ValueError(f"{value!r} is not a usable path segment")
27
+ return quote(value, safe="")
28
+
29
+
30
+ KINDS = (
31
+ "cfop",
32
+ "ncm",
33
+ "cest",
34
+ "cst",
35
+ "csosn",
36
+ "iss_service",
37
+ "icms_fuel",
38
+ "ibs_cbs_class",
39
+ )
40
+
41
+
42
+ class Kind:
43
+ """One classification, bound to a country."""
44
+
45
+ def __init__(self, client: _Client, name: str, country: str) -> None:
46
+ self._client = client
47
+ self.name = name
48
+ self.country = country
49
+
50
+ def get(self, code: str, *, country: str | None = None) -> dict:
51
+ """One code. A code that does not exist raises APIError (404)."""
52
+ return cast(
53
+ dict,
54
+ self._client._request(
55
+ "GET",
56
+ f"/fiscal-references/{_segment(self.name)}"
57
+ f"/{_segment(code)}",
58
+ params={"country": country or self.country},
59
+ ),
60
+ )
61
+
62
+ def search(
63
+ self,
64
+ term: str | None = None,
65
+ *,
66
+ country: str | None = None,
67
+ limit: int | None = None,
68
+ offset: int | None = None,
69
+ ) -> dict:
70
+ """This classification, searched or simply paged through.
71
+
72
+ Rows come back ordered by kind then code; the API takes no
73
+ sort_by or order_by here, unlike Invoice.history().
74
+ """
75
+ return _search(
76
+ self._client,
77
+ country=country or self.country,
78
+ kind=self.name,
79
+ term=term,
80
+ limit=limit,
81
+ offset=offset,
82
+ )
83
+
84
+
85
+ def _search(
86
+ client: _Client,
87
+ *,
88
+ country: str,
89
+ kind: str | None,
90
+ term: str | None,
91
+ limit: int | None,
92
+ offset: int | None,
93
+ ) -> dict:
94
+ """Shared by Kind.search and FiscalReference.search."""
95
+ params: dict[str, Any] = {"country": country}
96
+ if kind is not None:
97
+ params["kind"] = kind
98
+ if term:
99
+ params["search"] = term
100
+ if limit is not None:
101
+ params["limit"] = limit
102
+ if offset is not None:
103
+ params["offset"] = offset
104
+
105
+ return cast(
106
+ dict,
107
+ client._request("GET", "/fiscal-references", params=params),
108
+ )
109
+
110
+
111
+ class FiscalReference(_Client):
112
+ """Client for the published fiscal classification tables."""
113
+
114
+ def __init__(
115
+ self,
116
+ base_url: str | None = None,
117
+ environment: Environment | str | None = None,
118
+ api_key: str | None = None,
119
+ timeout: int = 30,
120
+ country: str = "BR",
121
+ ) -> None:
122
+ super().__init__(base_url, environment, api_key, timeout)
123
+ self.country = country
124
+
125
+ self.cfop = Kind(self, "cfop", country)
126
+ self.ncm = Kind(self, "ncm", country)
127
+ self.cest = Kind(self, "cest", country)
128
+ self.cst = Kind(self, "cst", country)
129
+ self.csosn = Kind(self, "csosn", country)
130
+ self.iss_service = Kind(self, "iss_service", country)
131
+ self.icms_fuel = Kind(self, "icms_fuel", country)
132
+ self.ibs_cbs_class = Kind(self, "ibs_cbs_class", country)
133
+
134
+ def kinds(self, *, country: str | None = None) -> list[str]:
135
+ """Which classifications this country has data for.
136
+
137
+ The only honest answer to "what else is there" — a hard-coded
138
+ list in a README goes stale the next time the ETL grows one.
139
+ """
140
+ return cast(
141
+ list[str],
142
+ self._request(
143
+ "GET",
144
+ "/fiscal-references/kinds",
145
+ params={"country": country or self.country},
146
+ ),
147
+ )
148
+
149
+ def kind(self, name: str, *, country: str | None = None) -> Kind:
150
+ """Any classification by name, including one with no accessor."""
151
+ return Kind(self, name, country or self.country)
152
+
153
+ def search(
154
+ self,
155
+ term: str | None = None,
156
+ *,
157
+ country: str | None = None,
158
+ limit: int | None = None,
159
+ offset: int | None = None,
160
+ ) -> dict:
161
+ """Every classification at once, which no accessor can express.
162
+
163
+ The most expensive call the endpoint accepts: it is the whole
164
+ country's tables, not one of them.
165
+ """
166
+ return _search(
167
+ self,
168
+ country=country or self.country,
169
+ kind=None,
170
+ term=term,
171
+ limit=limit,
172
+ offset=offset,
173
+ )
@@ -0,0 +1,49 @@
1
+ """The taxpayer registry, looked up one tax id at a time.
2
+
3
+ One method, and it stays one method. The registry names and addresses
4
+ real people and companies: an exact lookup answers the question an
5
+ issuer has — who is this CNPJ I am about to invoice — and answers
6
+ nothing else. A prefix or wildcard search over the same table is a bulk
7
+ export wearing the costume of a query. Do not add one, not for parity
8
+ with FiscalReference, not "just by name", not "just within one state".
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import cast
14
+
15
+ from stackin.core.client import _Client
16
+ from stackin.core.reference import _segment
17
+ from stackin.core.types import Environment
18
+
19
+
20
+ class Taxpayer(_Client):
21
+ """Client for the taxpayer registry."""
22
+
23
+ def __init__(
24
+ self,
25
+ base_url: str | None = None,
26
+ environment: Environment | str | None = None,
27
+ api_key: str | None = None,
28
+ timeout: int = 30,
29
+ country: str = "BR",
30
+ ) -> None:
31
+ super().__init__(base_url, environment, api_key, timeout)
32
+ self.country = country
33
+
34
+ def get(self, tax_id: str, *, country: str | None = None) -> dict:
35
+ """One taxpayer, by exact tax id.
36
+
37
+ A 404 raises APIError and does not mean the taxpayer does not
38
+ exist: the registry reloads monthly, so a company registered in
39
+ the last few weeks is simply not in it yet. Never build a
40
+ validation rule on top of it.
41
+ """
42
+ return cast(
43
+ dict,
44
+ self._request(
45
+ "GET",
46
+ f"/taxpayers/{_segment(tax_id)}",
47
+ params={"country": country or self.country},
48
+ ),
49
+ )