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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackin-python-sdk
3
- Version: 0.1.0
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
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue?style=flat-square)](pyproject.toml)
37
+ [![PyPI](https://img.shields.io/pypi/v/stackin-python-sdk?style=flat-square)](https://pypi.org/project/stackin-python-sdk/)
32
38
  [![License](https://img.shields.io/badge/license-MIT-informational?style=flat-square)](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/) `simple_issue_nfe.py` and `simple_issue_nfse.py`, each with a catalog of realistic line items covering every optional field.
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
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue?style=flat-square)](pyproject.toml)
8
+ [![PyPI](https://img.shields.io/pypi/v/stackin-python-sdk?style=flat-square)](https://pypi.org/project/stackin-python-sdk/)
8
9
  [![License](https://img.shields.io/badge/license-MIT-informational?style=flat-square)](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/) `simple_issue_nfe.py` and `simple_issue_nfse.py`, each with a catalog of realistic line items covering every optional field.
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.0"
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.0"
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"
@@ -1,6 +1,6 @@
1
1
  """Invoice __init__ module."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
4
4
  __description__ = (
5
5
  "Python SDK for issuing, consulting and cancelling electronic invoices."
6
6
  )
@@ -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: list[Annotated[str, Field(pattern=r"^[A-Z]{2}\d{4}$")]] | None = Field(
59
- default=None
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(default=None, pattern=r"^[0-9A-Z]{12}\d{2}$")
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={"description", "amount", "tax", *_BR_FIELDS},
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 {"description": self.description, "amount": self.amount, "product": data}
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(default=None, alias="vICMSSubstituto")
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 = Icms00 | Icms40 | Icms60 | IcmsSn101 | IcmsSn102 | dict[str, Any]
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"] = self.ipi.to_dict() if isinstance(self.ipi, Ipi) else self.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 {tags[type(group)]: group.model_dump(by_alias=True, exclude_none=True)}
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, same shape as the AWS CLI: explicit param,
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(f"items[{index}].cfop is required for NFE")
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 and recipient_address.state:
83
- payload["recipient_state"] = recipient_address.state
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("POST", f"/invoices/{access_key}/cancel", json=payload)
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:
@@ -15,5 +15,4 @@ class APIError(InvoiceError):
15
15
 
16
16
 
17
17
  class ConnectionFailedError(InvoiceError):
18
- """Raised when the invoice API can't be reached at all
19
- (network/DNS/timeout)."""
18
+ """Raised when the invoice API can't be reached at all (network/DNS/timeout)."""
@@ -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"