stackin-python-sdk 0.1.0__tar.gz → 0.1.2__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.
- {stackin_python_sdk-0.1.0 → stackin_python_sdk-0.1.2}/PKG-INFO +11 -17
- {stackin_python_sdk-0.1.0 → stackin_python_sdk-0.1.2}/README.md +5 -16
- {stackin_python_sdk-0.1.0 → stackin_python_sdk-0.1.2}/pyproject.toml +9 -2
- {stackin_python_sdk-0.1.0 → stackin_python_sdk-0.1.2}/stackin/__init__.py +1 -1
- {stackin_python_sdk-0.1.0 → stackin_python_sdk-0.1.2}/stackin/br/__init__.py +2 -0
- {stackin_python_sdk-0.1.0 → stackin_python_sdk-0.1.2}/stackin/br/product.py +41 -6
- {stackin_python_sdk-0.1.0 → stackin_python_sdk-0.1.2}/stackin/br/tax.py +32 -4
- {stackin_python_sdk-0.1.0 → stackin_python_sdk-0.1.2}/stackin/core/client.py +16 -8
- {stackin_python_sdk-0.1.0 → stackin_python_sdk-0.1.2}/stackin/core/exceptions.py +1 -2
- {stackin_python_sdk-0.1.0 → stackin_python_sdk-0.1.2}/stackin/core/types.py +1 -4
- {stackin_python_sdk-0.1.0 → stackin_python_sdk-0.1.2}/LICENSE +0 -0
- {stackin_python_sdk-0.1.0 → stackin_python_sdk-0.1.2}/stackin/core/__init__.py +0 -0
- {stackin_python_sdk-0.1.0 → stackin_python_sdk-0.1.2}/stackin/core/address.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackin-python-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Python SDK for issuing, consulting and cancelling electronic invoices.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -20,6 +20,11 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
20
20
|
Classifier: Natural Language :: Portuguese (Brazilian)
|
|
21
21
|
Requires-Dist: pydantic
|
|
22
22
|
Requires-Dist: requests
|
|
23
|
+
Project-URL: Changelog, https://github.com/stackin-io/stackin-python-sdk/releases
|
|
24
|
+
Project-URL: Documentation, https://docs.stackin.io/blog/python-sdk
|
|
25
|
+
Project-URL: Homepage, https://docs.stackin.io
|
|
26
|
+
Project-URL: Issues, https://github.com/stackin-io/stackin-python-sdk/issues
|
|
27
|
+
Project-URL: Repository, https://github.com/stackin-io/stackin-python-sdk
|
|
23
28
|
Description-Content-Type: text/markdown
|
|
24
29
|
|
|
25
30
|
<div align="center">
|
|
@@ -29,8 +34,11 @@ Description-Content-Type: text/markdown
|
|
|
29
34
|
**Integrate once. Issue everywhere.**
|
|
30
35
|
|
|
31
36
|
[](pyproject.toml)
|
|
37
|
+
[](https://pypi.org/project/stackin-python-sdk/)
|
|
32
38
|
[](https://github.com/stackin-io/stackin-python-sdk)
|
|
33
39
|
|
|
40
|
+
[API Reference](https://docs.stackin.io) · [Python SDK guide](https://docs.stackin.io/blog/python-sdk)
|
|
41
|
+
|
|
34
42
|
</div>
|
|
35
43
|
|
|
36
44
|
---
|
|
@@ -49,7 +57,7 @@ pip install stackin-python-sdk
|
|
|
49
57
|
|
|
50
58
|
## Usage
|
|
51
59
|
|
|
52
|
-
Get an `api_key` from the [stackin dashboard](https://app.stackin.io) — select the issuing company, then Settings → API key. One key per issuing company, shown once at creation. The API resolves the issuer (CNPJ, state, address, certificate, environment) entirely from it; nothing about the issuer is ever passed on a call.
|
|
60
|
+
Get an `api_key` from the [stackin dashboard](https://app.stackin.io) — select the issuing company, then Settings → API key (context `sdk`). One key per issuing company, shown once at creation. The API resolves the issuer (CNPJ, state, address, certificate, environment) entirely from it; nothing about the issuer is ever passed on a call.
|
|
53
61
|
|
|
54
62
|
```python
|
|
55
63
|
from stackin import Invoice, DocumentType, Address
|
|
@@ -96,19 +104,5 @@ Building the full fiscal document (issuer data, service code, tax groups, schema
|
|
|
96
104
|
|
|
97
105
|
## Examples
|
|
98
106
|
|
|
99
|
-
Runnable end-to-end scripts in [`examples/`](examples/)
|
|
100
|
-
|
|
101
|
-
## Commit Style
|
|
102
|
-
|
|
103
|
-
| Icon | Type | Description |
|
|
104
|
-
|------|-----------|--------------------------------------------|
|
|
105
|
-
| ⚙️ | FEATURE | New feature |
|
|
106
|
-
| 📝 | PEP8 | Formatting fixes following PEP8 |
|
|
107
|
-
| 📌 | ISSUE | Reference to issue |
|
|
108
|
-
| 🪲 | BUG | Bug fix |
|
|
109
|
-
| 📘 | DOCS | Documentation changes |
|
|
110
|
-
| 📦 | PyPI | PyPI releases |
|
|
111
|
-
| ❤️️ | TEST | Automated tests |
|
|
112
|
-
| ⬆️ | CI/CD | Changes in continuous integration/delivery |
|
|
113
|
-
| ⚠️ | SECURITY | Security improvements |
|
|
107
|
+
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.
|
|
114
108
|
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
**Integrate once. Issue everywhere.**
|
|
6
6
|
|
|
7
7
|
[](pyproject.toml)
|
|
8
|
+
[](https://pypi.org/project/stackin-python-sdk/)
|
|
8
9
|
[](https://github.com/stackin-io/stackin-python-sdk)
|
|
9
10
|
|
|
11
|
+
[API Reference](https://docs.stackin.io) · [Python SDK guide](https://docs.stackin.io/blog/python-sdk)
|
|
12
|
+
|
|
10
13
|
</div>
|
|
11
14
|
|
|
12
15
|
---
|
|
@@ -25,7 +28,7 @@ pip install stackin-python-sdk
|
|
|
25
28
|
|
|
26
29
|
## Usage
|
|
27
30
|
|
|
28
|
-
Get an `api_key` from the [stackin dashboard](https://app.stackin.io) — select the issuing company, then Settings → API key. One key per issuing company, shown once at creation. The API resolves the issuer (CNPJ, state, address, certificate, environment) entirely from it; nothing about the issuer is ever passed on a call.
|
|
31
|
+
Get an `api_key` from the [stackin dashboard](https://app.stackin.io) — select the issuing company, then Settings → API key (context `sdk`). One key per issuing company, shown once at creation. The API resolves the issuer (CNPJ, state, address, certificate, environment) entirely from it; nothing about the issuer is ever passed on a call.
|
|
29
32
|
|
|
30
33
|
```python
|
|
31
34
|
from stackin import Invoice, DocumentType, Address
|
|
@@ -72,18 +75,4 @@ Building the full fiscal document (issuer data, service code, tax groups, schema
|
|
|
72
75
|
|
|
73
76
|
## Examples
|
|
74
77
|
|
|
75
|
-
Runnable end-to-end scripts in [`examples/`](examples/)
|
|
76
|
-
|
|
77
|
-
## Commit Style
|
|
78
|
-
|
|
79
|
-
| Icon | Type | Description |
|
|
80
|
-
|------|-----------|--------------------------------------------|
|
|
81
|
-
| ⚙️ | FEATURE | New feature |
|
|
82
|
-
| 📝 | PEP8 | Formatting fixes following PEP8 |
|
|
83
|
-
| 📌 | ISSUE | Reference to issue |
|
|
84
|
-
| 🪲 | BUG | Bug fix |
|
|
85
|
-
| 📘 | DOCS | Documentation changes |
|
|
86
|
-
| 📦 | PyPI | PyPI releases |
|
|
87
|
-
| ❤️️ | TEST | Automated tests |
|
|
88
|
-
| ⬆️ | CI/CD | Changes in continuous integration/delivery |
|
|
89
|
-
| ⚠️ | SECURITY | Security improvements |
|
|
78
|
+
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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "stackin-python-sdk"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name = "Fernando Celmer", email = "email@fernandocelmer.com" },
|
|
6
6
|
]
|
|
@@ -36,9 +36,16 @@ dependencies = [
|
|
|
36
36
|
"pydantic",
|
|
37
37
|
]
|
|
38
38
|
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://docs.stackin.io"
|
|
41
|
+
Documentation = "https://docs.stackin.io/blog/python-sdk"
|
|
42
|
+
Repository = "https://github.com/stackin-io/stackin-python-sdk"
|
|
43
|
+
Issues = "https://github.com/stackin-io/stackin-python-sdk/issues"
|
|
44
|
+
Changelog = "https://github.com/stackin-io/stackin-python-sdk/releases"
|
|
45
|
+
|
|
39
46
|
[tool.poetry]
|
|
40
47
|
name = "stackin-python-sdk"
|
|
41
|
-
version = "0.1.
|
|
48
|
+
version = "0.1.2"
|
|
42
49
|
description = "Python SDK for issuing, consulting and cancelling electronic invoices."
|
|
43
50
|
authors = ["Fernando Celmer <email@fernandocelmer.com>"]
|
|
44
51
|
readme = "README.md"
|
|
@@ -10,6 +10,7 @@ from stackin.br.tax import (
|
|
|
10
10
|
Icms60,
|
|
11
11
|
IcmsSn101,
|
|
12
12
|
IcmsSn102,
|
|
13
|
+
IcmsSn900,
|
|
13
14
|
IcmsUfDest,
|
|
14
15
|
Ipi,
|
|
15
16
|
IpiNt,
|
|
@@ -29,6 +30,7 @@ __all__ = [
|
|
|
29
30
|
"Icms60",
|
|
30
31
|
"IcmsSn101",
|
|
31
32
|
"IcmsSn102",
|
|
33
|
+
"IcmsSn900",
|
|
32
34
|
"IcmsUfDest",
|
|
33
35
|
"Ipi",
|
|
34
36
|
"IpiTrib",
|
|
@@ -55,11 +55,13 @@ class Product(BaseModel):
|
|
|
55
55
|
ncm: str | None = Field(default=None, pattern=r"^\d{2}$|^\d{8}$")
|
|
56
56
|
cfop: str | None = Field(default=None, pattern=r"^[123567]\d{3}$")
|
|
57
57
|
cest: str | None = Field(default=None, pattern=r"^\d{7}$")
|
|
58
|
-
nve_codes:
|
|
59
|
-
|
|
60
|
-
)
|
|
58
|
+
nve_codes: (
|
|
59
|
+
list[Annotated[str, Field(pattern=r"^[A-Z]{2}\d{4}$")]] | None
|
|
60
|
+
) = Field(default=None)
|
|
61
61
|
ind_escala: str | None = Field(default=None)
|
|
62
|
-
manufacturer_cnpj: str | None = Field(
|
|
62
|
+
manufacturer_cnpj: str | None = Field(
|
|
63
|
+
default=None, pattern=r"^[0-9A-Z]{12}\d{2}$"
|
|
64
|
+
)
|
|
63
65
|
tax_benefit_code: str | None = Field(default=None, pattern=_CBENEF_PATTERN)
|
|
64
66
|
presumed_credits: list[PresumedCredit] | None = Field(default=None)
|
|
65
67
|
ex_tipi: str | None = Field(default=None, pattern=r"^\d{2,3}$")
|
|
@@ -71,11 +73,36 @@ class Product(BaseModel):
|
|
|
71
73
|
extra_groups: dict[str, Any] | None = Field(default=None)
|
|
72
74
|
tax: Tax | dict[str, Any] | None = Field(default=None)
|
|
73
75
|
|
|
76
|
+
service_code: str | None = Field(
|
|
77
|
+
default=None, description="LC 116/2003 item.subitem, nfse only."
|
|
78
|
+
)
|
|
79
|
+
service_discount: float | None = Field(
|
|
80
|
+
default=None, description="Unconditional discount, nfse only."
|
|
81
|
+
)
|
|
82
|
+
tax_retained: bool = Field(
|
|
83
|
+
default=False, description="ISSQN retained by the tomador, nfse only."
|
|
84
|
+
)
|
|
85
|
+
observations: str | None = Field(
|
|
86
|
+
default=None, max_length=2000, description="nfse only."
|
|
87
|
+
)
|
|
88
|
+
|
|
74
89
|
def to_dict(self) -> dict:
|
|
75
90
|
"""Returns the item as a plain dict, ready for the request body."""
|
|
91
|
+
_NFSE_FIELDS = {
|
|
92
|
+
"service_code",
|
|
93
|
+
"service_discount",
|
|
94
|
+
"tax_retained",
|
|
95
|
+
"observations",
|
|
96
|
+
}
|
|
76
97
|
data = self.model_dump(
|
|
77
98
|
exclude_none=True,
|
|
78
|
-
exclude={
|
|
99
|
+
exclude={
|
|
100
|
+
"description",
|
|
101
|
+
"amount",
|
|
102
|
+
"tax",
|
|
103
|
+
*_BR_FIELDS,
|
|
104
|
+
*_NFSE_FIELDS,
|
|
105
|
+
},
|
|
79
106
|
)
|
|
80
107
|
br = self.model_dump(exclude_none=True, include=_BR_FIELDS - {"tax"})
|
|
81
108
|
if isinstance(self.tax, Tax):
|
|
@@ -84,4 +111,12 @@ class Product(BaseModel):
|
|
|
84
111
|
br["tax"] = self.tax
|
|
85
112
|
if br:
|
|
86
113
|
data["br"] = br
|
|
87
|
-
return {
|
|
114
|
+
return {
|
|
115
|
+
"description": self.description,
|
|
116
|
+
"amount": self.amount,
|
|
117
|
+
"product": data,
|
|
118
|
+
"service_code": self.service_code,
|
|
119
|
+
"discount": self.service_discount,
|
|
120
|
+
"tax_retained": self.tax_retained,
|
|
121
|
+
"observations": self.observations,
|
|
122
|
+
}
|
|
@@ -44,7 +44,9 @@ class Icms60(BaseModel):
|
|
|
44
44
|
cst: str = Field(default="60", alias="CST")
|
|
45
45
|
v_bc_st_ret: str | None = Field(default=None, alias="vBCSTRet")
|
|
46
46
|
p_st: str | None = Field(default=None, alias="pST")
|
|
47
|
-
v_icms_substituto: str | None = Field(
|
|
47
|
+
v_icms_substituto: str | None = Field(
|
|
48
|
+
default=None, alias="vICMSSubstituto"
|
|
49
|
+
)
|
|
48
50
|
v_icms_st_ret: str | None = Field(default=None, alias="vICMSSTRet")
|
|
49
51
|
v_bc_fcp_st_ret: str | None = Field(default=None, alias="vBCFCPSTRet")
|
|
50
52
|
p_fcp_st_ret: str | None = Field(default=None, alias="pFCPSTRet")
|
|
@@ -75,6 +77,19 @@ class IcmsSn102(BaseModel):
|
|
|
75
77
|
csosn: str = Field(alias="CSOSN")
|
|
76
78
|
|
|
77
79
|
|
|
80
|
+
class IcmsSn900(BaseModel):
|
|
81
|
+
"""Simples Nacional ICMS, other cases (used with interstate partilha)."""
|
|
82
|
+
|
|
83
|
+
model_config = _CONFIG
|
|
84
|
+
|
|
85
|
+
orig: str | None = None
|
|
86
|
+
csosn: str = Field(default="900", alias="CSOSN")
|
|
87
|
+
mod_bc: str | None = Field(default=None, alias="modBC")
|
|
88
|
+
v_bc: str | None = Field(default=None, alias="vBC")
|
|
89
|
+
p_icms: str | None = Field(default=None, alias="pICMS")
|
|
90
|
+
v_icms: str | None = Field(default=None, alias="vICMS")
|
|
91
|
+
|
|
92
|
+
|
|
78
93
|
class IcmsUfDest(BaseModel):
|
|
79
94
|
"""Interstate ICMS share owed to the destination state."""
|
|
80
95
|
|
|
@@ -191,7 +206,15 @@ class CofinsOutr(BaseModel):
|
|
|
191
206
|
v_cofins: str = Field(alias="vCOFINS")
|
|
192
207
|
|
|
193
208
|
|
|
194
|
-
IcmsGroup =
|
|
209
|
+
IcmsGroup = (
|
|
210
|
+
Icms00
|
|
211
|
+
| Icms40
|
|
212
|
+
| Icms60
|
|
213
|
+
| IcmsSn101
|
|
214
|
+
| IcmsSn102
|
|
215
|
+
| IcmsSn900
|
|
216
|
+
| dict[str, Any]
|
|
217
|
+
)
|
|
195
218
|
PisGroup = PisAliq | PisNt | PisOutr | dict[str, Any]
|
|
196
219
|
CofinsGroup = CofinsAliq | CofinsNt | CofinsOutr | dict[str, Any]
|
|
197
220
|
|
|
@@ -201,6 +224,7 @@ _ICMS_TAGS = {
|
|
|
201
224
|
Icms60: "ICMS60",
|
|
202
225
|
IcmsSn101: "ICMSSN101",
|
|
203
226
|
IcmsSn102: "ICMSSN102",
|
|
227
|
+
IcmsSn900: "ICMSSN900",
|
|
204
228
|
}
|
|
205
229
|
_PIS_TAGS = {PisAliq: "PISAliq", PisNt: "PISNT", PisOutr: "PISOutr"}
|
|
206
230
|
_COFINS_TAGS = {
|
|
@@ -234,7 +258,9 @@ class Tax(BaseModel):
|
|
|
234
258
|
by_alias=True, exclude_none=True
|
|
235
259
|
)
|
|
236
260
|
if self.ipi is not None:
|
|
237
|
-
data["IPI"] =
|
|
261
|
+
data["IPI"] = (
|
|
262
|
+
self.ipi.to_dict() if isinstance(self.ipi, Ipi) else self.ipi
|
|
263
|
+
)
|
|
238
264
|
if self.pis is not None:
|
|
239
265
|
data["PIS"] = _wrap(self.pis, _PIS_TAGS)
|
|
240
266
|
if self.cofins is not None:
|
|
@@ -246,4 +272,6 @@ def _wrap(group: BaseModel | dict, tags: dict[type, str]) -> dict:
|
|
|
246
272
|
"""Nests a tax group under its variant name."""
|
|
247
273
|
if isinstance(group, dict):
|
|
248
274
|
return group
|
|
249
|
-
return {
|
|
275
|
+
return {
|
|
276
|
+
tags[type(group)]: group.model_dump(by_alias=True, exclude_none=True)
|
|
277
|
+
}
|
|
@@ -6,8 +6,8 @@ import os
|
|
|
6
6
|
|
|
7
7
|
import requests
|
|
8
8
|
|
|
9
|
-
from stackin.core.address import Address
|
|
10
9
|
from stackin.br.product import Product
|
|
10
|
+
from stackin.core.address import Address
|
|
11
11
|
from stackin.core.exceptions import APIError, ConnectionFailedError
|
|
12
12
|
from stackin.core.types import DocumentType, Environment
|
|
13
13
|
|
|
@@ -23,9 +23,7 @@ _ENVIRONMENT_URLS = {
|
|
|
23
23
|
def _resolve_base_url(
|
|
24
24
|
base_url: str | None, environment: Environment | str | None
|
|
25
25
|
) -> str:
|
|
26
|
-
"""Resolution order
|
|
27
|
-
then env var, then the environment's default — `base_url` always
|
|
28
|
-
wins over `environment` at each step."""
|
|
26
|
+
"""Resolution order: explicit param, then env var, then environment's default."""
|
|
29
27
|
if base_url:
|
|
30
28
|
return base_url
|
|
31
29
|
if url := os.environ.get("STACKIN_BASE_URL"):
|
|
@@ -61,6 +59,8 @@ class Invoice:
|
|
|
61
59
|
tax_id: str,
|
|
62
60
|
items: list[Product],
|
|
63
61
|
recipient_address: Address | None = None,
|
|
62
|
+
series: str | None = None,
|
|
63
|
+
number: str | None = None,
|
|
64
64
|
) -> dict:
|
|
65
65
|
"""Issues a fiscal document."""
|
|
66
66
|
if not items:
|
|
@@ -71,7 +71,9 @@ class Invoice:
|
|
|
71
71
|
if not item.ncm:
|
|
72
72
|
raise ValueError(f"items[{index}].ncm is required for NFE")
|
|
73
73
|
if not item.cfop:
|
|
74
|
-
raise ValueError(
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"items[{index}].cfop is required for NFE"
|
|
76
|
+
)
|
|
75
77
|
|
|
76
78
|
payload = {
|
|
77
79
|
"document_type": document_type.value,
|
|
@@ -79,8 +81,12 @@ class Invoice:
|
|
|
79
81
|
"tax_id": tax_id,
|
|
80
82
|
"items": [item.to_dict() for item in items],
|
|
81
83
|
}
|
|
82
|
-
if recipient_address
|
|
83
|
-
payload["
|
|
84
|
+
if recipient_address:
|
|
85
|
+
payload["recipient_address"] = recipient_address.to_dict()
|
|
86
|
+
if series:
|
|
87
|
+
payload["series"] = series
|
|
88
|
+
if number:
|
|
89
|
+
payload["number"] = number
|
|
84
90
|
|
|
85
91
|
return self._request("POST", "/invoices", json=payload)
|
|
86
92
|
|
|
@@ -108,7 +114,9 @@ class Invoice:
|
|
|
108
114
|
"reason": reason,
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
return self._request(
|
|
117
|
+
return self._request(
|
|
118
|
+
"POST", f"/invoices/{access_key}/cancel", json=payload
|
|
119
|
+
)
|
|
112
120
|
|
|
113
121
|
def _headers(self) -> dict:
|
|
114
122
|
if self.api_key:
|
|
@@ -13,10 +13,7 @@ class DocumentType(str, Enum):
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class Environment(str, Enum):
|
|
16
|
-
"""Which host to talk to — pass to `Invoice(environment=...)`
|
|
17
|
-
instead of a raw `base_url`. `TEST` and `PRODUCTION` resolve to the
|
|
18
|
-
same host: homologation vs. production invoicing is a per-company
|
|
19
|
-
setting on the platform side, not a different SDK host."""
|
|
16
|
+
"""Which host to talk to — pass to `Invoice(environment=...)` instead of a raw `base_url`."""
|
|
20
17
|
|
|
21
18
|
LOCAL = "local"
|
|
22
19
|
TEST = "test"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|