feat: extract pure fiscal core from sowai-auto (parity-preserving copy)
Copies the 6 pure fiscal modules (domains, chave_acesso, tpag, presets, resolver, xml_builder) from sowai-auto's backend/app/modules/fiscal/ into this standalone lib, with only the two mechanical changes required to drop the app.* dependency: import prefix rename, and TaxRule (ORM) -> TaxRuleLike (structural Protocol) in the resolver. No other line changes -- byte-level parity with the auto is the point. Ports the pure test suites (domains, chave_acesso, resolver with a local FakeTaxRule satisfying TaxRuleLike, xml_builder) plus a new test_presets_data covering the PRESETS dict directly (the auto's equivalent test exercises the apply-preset HTTP/DB flow, which isn't part of the extracted pure core). 55 tests pass locally via `uv run pytest`, no Postgres/k8s required.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.pytest_cache/
|
||||
dist/
|
||||
*.egg-info/
|
||||
@@ -0,0 +1,45 @@
|
||||
# sowai-fiscal
|
||||
|
||||
Núcleo fiscal puro da SowAI: motor de regras tributário (imposto-como-dado),
|
||||
chave de acesso de NF-e (módulo 11) e builder do XML NF-e 55 (leiaute NT
|
||||
2025.002 v1.40, bindings `nfelib` 2.5.x). Extraído do produto `sowai-auto`
|
||||
(F1 do plano `sowai-fiscal-svc`) como lib versionada — zero I/O, zero
|
||||
SQLAlchemy, zero FastAPI, zero acoplamento a nenhum produto: a interface com
|
||||
o ORM do consumidor é o `Protocol` estrutural `sowai_fiscal.types.TaxRuleLike`.
|
||||
|
||||
## Módulos
|
||||
|
||||
- `domains` — catálogo `TaxDomain`/`DOMAIN_FIELDS`/`MATCHER_WEIGHTS` +
|
||||
transposição de CFOP intra→interestadual.
|
||||
- `chave_acesso` — DV módulo 11, sorteio de `cNF`, montagem da chave de 44
|
||||
dígitos.
|
||||
- `tpag` — tabela de códigos `tPag` (MOC, grupo YA02).
|
||||
- `presets` — presets fiscais seed-editáveis (perfis reais extraídos do
|
||||
iCode da Thiago Auto Center).
|
||||
- `resolver` — `resolve_fiscal`: `FiscalOperation` × regras (`TaxRuleLike`)
|
||||
→ `FiscalResult`. Puro, determinístico, fail-closed.
|
||||
- `xml_builder` — `build_nfe`: `DadosEmissao` (já resolvido) → objeto `Nfe`
|
||||
(bindings `nfelib`).
|
||||
- `types` — `TaxRuleLike`, o `Protocol` que desacopla o resolver do ORM do
|
||||
produto.
|
||||
- `golden_helpers` — `dados_from_json`/`serialize_infnfe`, usados pelo
|
||||
corpus golden (`goldens/`) e pelo teste de paridade do produto consumidor.
|
||||
|
||||
## Corpus golden
|
||||
|
||||
`src/sowai_fiscal/goldens/caso_<nome>.{input.json,expected.xml}` — pares
|
||||
determinísticos (`dh_emi`/`cnf`/`chave_acesso` pinados no input) que travam
|
||||
o leiaute XML pré-assinatura gerado a partir de um `DadosEmissao`. São dados
|
||||
de pacote (lidos via `importlib.resources`), para que o produto consumidor
|
||||
compare byte a byte contra os MESMOS arquivos da lib instalada — nenhuma
|
||||
cópia que possa divergir. Regenerar com `uv run python scripts/gen_goldens.py`
|
||||
apenas quando uma NT mudar o leiaute DE PROPÓSITO.
|
||||
|
||||
## Testes
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
100% local — sem Postgres, sem k8s.
|
||||
@@ -0,0 +1,21 @@
|
||||
[project]
|
||||
name = "sowai-fiscal"
|
||||
version = "0.1.0"
|
||||
description = "Núcleo fiscal SowAI: motor de regras (imposto-como-dado), chave de acesso, builder de XML NF-e 4.00/NT 2025.002 v1.40"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"pydantic>=2.7",
|
||||
"nfelib>=2.5.2",
|
||||
"lxml>=5.2",
|
||||
"xsdata>=24.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["pytest>=8", "pytest-asyncio>=0.24"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/sowai_fiscal"]
|
||||
@@ -0,0 +1,2 @@
|
||||
"""sowai_fiscal — núcleo fiscal puro da SowAI (motor de regras, chave de
|
||||
acesso, builder de XML NF-e). Ver README.md para o mapa dos módulos."""
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Chave de acesso da NF-e (funções PURAS -- zero I/O, zero sessão).
|
||||
|
||||
Estrutura (44 dígitos): cUF(2) + AAMM(4) + CNPJ(14) + mod(2) + série(3) +
|
||||
nNF(9) + tpEmis(1) + cNF(8) + DV(1).
|
||||
|
||||
cNF é sorteado UMA vez por documento e PERSISTE (`FiscalDocument.
|
||||
codigo_numerico`, Task 4): retry de transmissão usa a MESMA chave --
|
||||
re-sortear cNF é o que fabrica a Rejeição 539 (duplicidade com chave
|
||||
diferente). Regra NT 2019.001: cNF != nNF.
|
||||
"""
|
||||
import secrets
|
||||
|
||||
_PESOS = (2, 3, 4, 5, 6, 7, 8, 9)
|
||||
|
||||
|
||||
def dv_modulo11(chave43: str) -> str:
|
||||
"""Dígito verificador módulo 11 da chave de acesso.
|
||||
|
||||
Pesos 2..9 aplicados da direita para a esquerda (repetindo em ciclos de
|
||||
8); soma dos produtos; resto = soma % 11; DV = 0 se resto in (0, 1),
|
||||
senão DV = 11 - resto. Validado contra chaves reais -- ver
|
||||
`tests/modules/fiscal/test_chave_acesso.py`.
|
||||
"""
|
||||
if len(chave43) != 43 or not chave43.isdigit():
|
||||
raise ValueError("chave sem DV deve ter 43 dígitos numéricos")
|
||||
soma = 0
|
||||
for i, digito in enumerate(reversed(chave43)):
|
||||
soma += int(digito) * _PESOS[i % 8]
|
||||
resto = soma % 11
|
||||
return "0" if resto in (0, 1) else str(11 - resto)
|
||||
|
||||
|
||||
def gerar_cnf(numero_nnf: int) -> str:
|
||||
"""Sorteia o código numérico (cNF) de 8 dígitos, criptograficamente
|
||||
aleatório, garantindo cNF != nNF (regra NT 2019.001 -- SEFAZ rejeita
|
||||
quando os dois coincidem).
|
||||
|
||||
F3a (review 2026-07-16): também rejeita `cNF == "00000000"`. All-zeros
|
||||
é um valor técnicamente válido pelo formato (8 dígitos), mas é o chute
|
||||
óbvio de qualquer gerador ingênuo/mockado -- aceitá-lo aqui abriria
|
||||
espaço para uma chave previsível se algum outro ponto do código algum
|
||||
dia trocasse `secrets.randbelow` por algo mais fraco sem essa rede de
|
||||
segurança. Nunca colide com `numero_nnf` de propósito (nNF sempre >= 1,
|
||||
nunca 0 -- `montar_chave_acesso` já exige `numero >= 1`), então esta
|
||||
checagem nunca é a mesma da de `cNF != nNF` acima."""
|
||||
while True:
|
||||
cnf = f"{secrets.randbelow(100_000_000):08d}"
|
||||
if int(cnf) != numero_nnf and int(cnf) != 0:
|
||||
return cnf
|
||||
|
||||
|
||||
def montar_chave_acesso(
|
||||
*,
|
||||
uf_ibge: str,
|
||||
aamm: str,
|
||||
cnpj: str,
|
||||
modelo: str,
|
||||
serie: int,
|
||||
numero: int,
|
||||
tp_emis: str,
|
||||
cnf: str,
|
||||
) -> str:
|
||||
"""Monta a chave de acesso de 44 dígitos a partir dos componentes já
|
||||
formatados/validados pelo chamador (o orquestrador de emissão, Task 6).
|
||||
Pura: não consulta banco, não decide nenhum dos valores -- só concatena
|
||||
e calcula o DV.
|
||||
|
||||
F1 (review 2026-07-16): valida CADA componente individualmente (tamanho
|
||||
exato + só dígitos), não apenas o tamanho AGREGADO de 43 dígitos do
|
||||
`chave43` final. Só checar o total é insuficiente -- dois componentes
|
||||
errados que se COMPENSAM em dígitos (ex.: um `cnpj` de 13 dígitos + um
|
||||
`cnf` de 9, em vez dos 14+8 corretos) somam exatamente 43 e passavam
|
||||
batendo pela checagem antiga, produzindo uma chave de 44 dígitos
|
||||
SINTATICAMENTE válida (DV correto) mas que aponta para o CNPJ/emitente
|
||||
ERRADO -- um bug silencioso que só se manifestaria como uma rejeição
|
||||
(ou pior, uma autorização indevida) na SEFAZ, não em teste algum. Cada
|
||||
checagem abaixo isola o componente específico que a violaria."""
|
||||
if len(uf_ibge) != 2 or not uf_ibge.isdigit():
|
||||
raise ValueError(f"uf_ibge deve ter 2 dígitos numéricos: {uf_ibge!r}")
|
||||
if len(aamm) != 4 or not aamm.isdigit():
|
||||
raise ValueError(f"aamm deve ter 4 dígitos numéricos: {aamm!r}")
|
||||
if len(cnpj) != 14 or not cnpj.isdigit():
|
||||
raise ValueError(f"cnpj deve ter 14 dígitos numéricos: {cnpj!r}")
|
||||
if len(modelo) != 2 or not modelo.isdigit():
|
||||
raise ValueError(f"modelo deve ter 2 dígitos numéricos: {modelo!r}")
|
||||
if len(tp_emis) != 1 or not tp_emis.isdigit():
|
||||
raise ValueError(f"tp_emis deve ter 1 dígito numérico: {tp_emis!r}")
|
||||
if len(cnf) != 8 or not cnf.isdigit():
|
||||
raise ValueError(f"cnf deve ter 8 dígitos numéricos: {cnf!r}")
|
||||
if not (0 <= serie <= 999):
|
||||
raise ValueError(f"serie fora do intervalo 0-999: {serie}")
|
||||
if not (1 <= numero <= 999_999_999):
|
||||
raise ValueError(f"numero fora do intervalo 1-999999999: {numero}")
|
||||
|
||||
chave43 = f"{uf_ibge}{aamm}{cnpj}{modelo}{serie:03d}{numero:09d}{tp_emis}{cnf}"
|
||||
# Invariante garantido pelas checagens de componente acima -- mantido
|
||||
# como cinto-e-suspensório (nunca deve disparar; se disparar, é sinal
|
||||
# de que uma checagem de componente ficou desalinhada com o format
|
||||
# string abaixo dela).
|
||||
if len(chave43) != 43:
|
||||
raise ValueError(f"componentes somam {len(chave43)} dígitos, esperado 43")
|
||||
return chave43 + dv_modulo11(chave43)
|
||||
@@ -0,0 +1,140 @@
|
||||
"""Catálogo de domínios tributários do motor fiscal (Bloco B).
|
||||
|
||||
`tax_domain` é `String(20)` no banco DE PROPÓSITO — validado por ESTE enum
|
||||
Python na borda Pydantic. Adicionar um tributo (IBS/CBS/qualquer futuro) é
|
||||
um membro novo aqui + uma entrada em DOMAIN_FIELDS: sem ALTER TYPE, sem
|
||||
migration, sem o gotcha enum-NAME-vs-value que já quebrou produção neste
|
||||
projeto (ver project_enum_migration_gotcha). Este é o "imposto-como-dado"
|
||||
literal da Opção 1 do spec.
|
||||
"""
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class TaxDomain(str, enum.Enum):
|
||||
ICMS = "icms"
|
||||
ICMSST = "icmsst"
|
||||
IPI = "ipi"
|
||||
PIS = "pis"
|
||||
COFINS = "cofins"
|
||||
DIFAL = "difal"
|
||||
FCP = "fcp"
|
||||
IBS = "ibs"
|
||||
CBS = "cbs"
|
||||
ISS = "iss"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DomainFieldSpec:
|
||||
"""O que o form da UI mostra/exige para uma regra deste domínio.
|
||||
|
||||
Nomes de campo = colunas de resultado da TaxRule. O frontend NÃO
|
||||
hard-coda isto — consome via GET /fiscal/tax-domains.
|
||||
|
||||
`campos_um_de`: grupos "exatamente um de" (review do frontend,
|
||||
2026-07-15) — para ICMS/ICMS-ST a situação tributária é exatamente um
|
||||
de {cst, csosn} (qual, depende do CRT da filial: Simples → CSOSN,
|
||||
Normal → CST). Sem isto no catálogo, o form dinâmico não tem como
|
||||
exigir situação tributária e o usuário come um 422 que o form não
|
||||
previu. O validator das regras consome ESTES grupos — uma fonte só.
|
||||
"""
|
||||
|
||||
label: str
|
||||
campos_aplicaveis: tuple[str, ...]
|
||||
campos_obrigatorios: tuple[str, ...]
|
||||
campos_um_de: tuple[tuple[str, ...], ...] = ()
|
||||
|
||||
|
||||
_SITUACAO = ("cst", "csosn") # exatamente um dos dois, conforme o CRT
|
||||
_VALORES = ("base_calc_percent", "aliquota")
|
||||
|
||||
DOMAIN_FIELDS: dict[str, DomainFieldSpec] = {
|
||||
TaxDomain.ICMS.value: DomainFieldSpec(
|
||||
label="ICMS",
|
||||
campos_aplicaveis=(*_SITUACAO, "cfop", *_VALORES, "codigo_beneficio"),
|
||||
campos_obrigatorios=("cfop",),
|
||||
campos_um_de=(_SITUACAO,),
|
||||
),
|
||||
TaxDomain.ICMSST.value: DomainFieldSpec(
|
||||
label="ICMS-ST",
|
||||
campos_aplicaveis=(*_SITUACAO, "cfop", *_VALORES, "mva", "aliquota_st", "codigo_beneficio"),
|
||||
campos_obrigatorios=("cfop",),
|
||||
campos_um_de=(_SITUACAO,),
|
||||
),
|
||||
TaxDomain.IPI.value: DomainFieldSpec(
|
||||
label="IPI",
|
||||
campos_aplicaveis=("cst", *_VALORES, "codigo_beneficio"),
|
||||
campos_obrigatorios=("cst",),
|
||||
),
|
||||
TaxDomain.PIS.value: DomainFieldSpec(
|
||||
label="PIS", campos_aplicaveis=("cst", *_VALORES), campos_obrigatorios=("cst",),
|
||||
),
|
||||
TaxDomain.COFINS.value: DomainFieldSpec(
|
||||
label="COFINS", campos_aplicaveis=("cst", *_VALORES), campos_obrigatorios=("cst",),
|
||||
),
|
||||
TaxDomain.DIFAL.value: DomainFieldSpec(
|
||||
label="DIFAL", campos_aplicaveis=_VALORES, campos_obrigatorios=("aliquota",),
|
||||
),
|
||||
TaxDomain.FCP.value: DomainFieldSpec(
|
||||
label="FCP", campos_aplicaveis=("fcp_percent",), campos_obrigatorios=("fcp_percent",),
|
||||
),
|
||||
# Reforma tributária: domínios ACEITOS hoje (regra pode ser cadastrada),
|
||||
# cálculo entra no roadmap R1 do spec quando SEFAZ publicar o leiaute.
|
||||
TaxDomain.IBS.value: DomainFieldSpec(
|
||||
label="IBS", campos_aplicaveis=_VALORES, campos_obrigatorios=("aliquota",),
|
||||
),
|
||||
TaxDomain.CBS.value: DomainFieldSpec(
|
||||
label="CBS", campos_aplicaveis=_VALORES, campos_obrigatorios=("aliquota",),
|
||||
),
|
||||
TaxDomain.ISS.value: DomainFieldSpec(
|
||||
label="ISS", campos_aplicaveis=_VALORES, campos_obrigatorios=("aliquota",),
|
||||
),
|
||||
}
|
||||
|
||||
# Especificidade ponderada (spec, seção resolvedor): potências de 2 em ordem
|
||||
# ESTRITA — cada matcher supera a soma de todos os mais fracos (64 > 63),
|
||||
# então nenhuma combinação de matchers genéricos vence um mais seletivo.
|
||||
# Caveat registrado no spec: ncm > cest; se um preset futuro criar regra-NCM
|
||||
# que sombreie regra-CEST-de-ST, revisitar estes dois.
|
||||
#
|
||||
# M5 (auditoria Fable Bloco B) — pegadinha dos prefixos NCM ANINHADOS: o
|
||||
# peso de `ncm_prefix` é fixo (64) independente do COMPRIMENTO do prefixo —
|
||||
# uma regra com `ncm_prefix="87"` e outra com `ncm_prefix="8708"` casando a
|
||||
# MESMA peça (NCM 87089990) EMPATAM em peso (ambas ganham 64), mesmo a
|
||||
# segunda sendo estritamente mais específica. Isto é AMBIGUIDADE DELIBERADA
|
||||
# (vira `AmbiguousRuleError`, fail-closed, nunca escolha silenciosa do
|
||||
# prefixo mais longo) — o resolvedor NÃO faz "longest-prefix-wins" como um
|
||||
# roteador de IP faria. A UI de cadastro de regras deve orientar o tenant a
|
||||
# usar prefixos NCM DISJUNTOS (não aninhados) dentro do mesmo domínio/
|
||||
# situação, ou aceitar o 409 de ambiguidade como sinal de conflito a
|
||||
# resolver manualmente.
|
||||
MATCHER_WEIGHTS: dict[str, int] = {
|
||||
"ncm_prefix": 64,
|
||||
"cest": 32,
|
||||
"consumidor_final": 16,
|
||||
"indicador_ie": 8,
|
||||
"uf_destino_tipo": 4,
|
||||
"tipo_operacao": 2,
|
||||
"crt": 1,
|
||||
}
|
||||
|
||||
# Transposição CFOP intra→interestadual. A tabela explícita existe porque a
|
||||
# regra geral (trocar 5→6 / 1→2) está ERRADA para ST: 5405 → 6403, não 6405.
|
||||
# CFOPs de ST/exceção DEVEM estar aqui; o fallback genérico cobre o resto.
|
||||
TRANSPOSICAO_CFOP: dict[str, str] = {
|
||||
"5102": "6102",
|
||||
"5405": "6403",
|
||||
"1202": "2202",
|
||||
}
|
||||
|
||||
_GENERIC_FIRST_DIGIT = {"5": "6", "1": "2"}
|
||||
|
||||
|
||||
def transpose_cfop(cfop: str) -> str:
|
||||
"""CFOP-base intra-estadual → interestadual (requisito herdado #3 da 1b)."""
|
||||
if cfop in TRANSPOSICAO_CFOP:
|
||||
return TRANSPOSICAO_CFOP[cfop]
|
||||
first = cfop[0]
|
||||
if first in _GENERIC_FIRST_DIGIT:
|
||||
return _GENERIC_FIRST_DIGIT[first] + cfop[1:]
|
||||
return cfop
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Presets fiscais seed-editáveis (decisão batida #3/#4 do spec): as regras
|
||||
são COPIADAS como TaxRule reais do tenant, que edita/apaga à vontade.
|
||||
|
||||
Valores REAIS extraídos do iCode da Thiago
|
||||
(thoughts/2026-07-13-icode-config-fiscal-extraida.md) — perfis "Padrão"
|
||||
(cód 110) e "Óleos/ST" (cód 108). NÃO inventar valores aqui.
|
||||
|
||||
PIS/COFINS do Óleos: CST 04 (monofásico — revenda a alíquota zero). O
|
||||
`~49/52` da extração tinha um til de incerteza; óleo lubrificante revendido
|
||||
é monofásico POR LEGISLAÇÃO (não é regra do cliente), logo CST 04.
|
||||
⚠️ Confirmar contra a aba PIS/COFINS do perfil 108 no iCode antes do
|
||||
primeiro tenant novo usar este preset em emissão real (tarefa registrada).
|
||||
|
||||
NENHUM preset carrega `origem`: origem é sempre do Part (review do
|
||||
frontend, 2026-07-14).
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PresetRule:
|
||||
tax_domain: str
|
||||
crt: str | None = None
|
||||
tipo_operacao: str | None = None
|
||||
cst: str | None = None
|
||||
csosn: str | None = None
|
||||
cfop: str | None = None
|
||||
aliquota: Decimal | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FiscalPreset:
|
||||
key: str
|
||||
name: str
|
||||
description: str
|
||||
# M2 (auditoria Fable Bloco B): CRT-alvo do preset ("1" = Simples
|
||||
# Nacional para os dois presets extraídos do iCode) -- exposto no
|
||||
# catálogo (`GET /fiscal/presets`) pra UI avisar quando o CRT da filial
|
||||
# não bate com o preset que o tenant está aplicando.
|
||||
regime_alvo: str = "1"
|
||||
rules: tuple[PresetRule, ...] = field(default_factory=tuple)
|
||||
|
||||
|
||||
PRESETS: dict[str, FiscalPreset] = {
|
||||
"autopecas_simples_padrao": FiscalPreset(
|
||||
key="autopecas_simples_padrao",
|
||||
name="Autopeças — Simples Nacional (Padrão)",
|
||||
description=(
|
||||
"Venda de peças no Simples: CSOSN 102, CFOP 5102 (interno; o motor "
|
||||
"transpõe para 6102 fora do estado), PIS/COFINS CST 08 alíquota 0, "
|
||||
"IPI não destacado. Devolução (entrada): CSOSN 102, CFOP 1202/2202."
|
||||
),
|
||||
rules=(
|
||||
# ICMS escopado a `tipo_operacao="venda"` (review 2026-07-15,
|
||||
# IMPORTANTE 1): sem escopo, esta regra também casava devolução
|
||||
# (tipo_operacao=NULL casa qualquer) e devolvia CFOP de SAÍDA
|
||||
# (5102/6102) para uma operação de ENTRADA — fail-open num motor
|
||||
# fail-closed. A regra de devolução abaixo é a única que cobre
|
||||
# tipo_operacao="devolucao" para este preset.
|
||||
PresetRule(
|
||||
tax_domain="icms", crt="1", tipo_operacao="venda",
|
||||
csosn="102", cfop="5102", aliquota=Decimal("0"),
|
||||
),
|
||||
PresetRule(
|
||||
tax_domain="icms", crt="1", tipo_operacao="devolucao",
|
||||
csosn="102", cfop="1202", aliquota=Decimal("0"),
|
||||
),
|
||||
# PIS/COFINS ficam sem escopo de tipo_operacao: os mesmos CSTs
|
||||
# valem para venda e devolução.
|
||||
PresetRule(tax_domain="pis", cst="08", aliquota=Decimal("0")),
|
||||
PresetRule(tax_domain="cofins", cst="08", aliquota=Decimal("0")),
|
||||
),
|
||||
),
|
||||
"autopecas_simples_st": FiscalPreset(
|
||||
key="autopecas_simples_st",
|
||||
name="Autopeças — Simples Nacional (Óleos/ST)",
|
||||
description=(
|
||||
"Peça/óleo com ICMS-ST retido pelo fornecedor: CSOSN 500, CFOP 5405 "
|
||||
"(interno; 6403 fora do estado), PIS/COFINS CST 04 (monofásico). "
|
||||
"Devolução (entrada): CSOSN 500, CFOP 1202/2202."
|
||||
),
|
||||
rules=(
|
||||
# Mesmo escopo de tipo_operacao que o preset Padrão — ver
|
||||
# comentário acima.
|
||||
PresetRule(
|
||||
tax_domain="icmsst", crt="1", tipo_operacao="venda",
|
||||
csosn="500", cfop="5405",
|
||||
),
|
||||
PresetRule(
|
||||
tax_domain="icmsst", crt="1", tipo_operacao="devolucao",
|
||||
csosn="500", cfop="1202",
|
||||
),
|
||||
PresetRule(tax_domain="pis", cst="04", aliquota=Decimal("0")),
|
||||
PresetRule(tax_domain="cofins", cst="04", aliquota=Decimal("0")),
|
||||
),
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
"""Resolvedor fiscal puro do Bloco B: FiscalOperation × TaxRule → FiscalResult.
|
||||
|
||||
Função pura, determinística, ZERO I/O — recebe as regras já carregadas.
|
||||
Quem toca o banco é fiscal/service.py. Fail-closed: sem regra para um
|
||||
domínio OBRIGATÓRIO (icms/icmsst com CFOP + situação tributária), levanta
|
||||
FiscalConfigError — nunca emite imposto adivinhado (decisão batida #2 do
|
||||
spec). `origem` é ecoada do ITEM (Part.icms_origem) e jamais de regra.
|
||||
"""
|
||||
import uuid
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from sowai_fiscal.domains import MATCHER_WEIGHTS, TaxDomain, transpose_cfop
|
||||
from sowai_fiscal.types import TaxRuleLike
|
||||
|
||||
_CENT = Decimal("0.01")
|
||||
|
||||
|
||||
class FiscalItem(BaseModel):
|
||||
tax_profile_id: uuid.UUID
|
||||
ncm: str
|
||||
cest: str | None
|
||||
origem: str | None
|
||||
# M1 (auditoria Fable Bloco B): input completo que o spec lista (costura
|
||||
# do R4/ISS) -- nenhuma regra o consome hoje, ecoado do
|
||||
# `Part.tipo_fiscal` pelo simulate.
|
||||
tipo_fiscal: str | None = None
|
||||
quantity: Decimal
|
||||
unit_price: Decimal
|
||||
|
||||
|
||||
class FiscalOperation(BaseModel):
|
||||
crt: str
|
||||
uf_origem: str
|
||||
uf_destino: str
|
||||
indicador_ie: str
|
||||
consumidor_final: bool
|
||||
tipo_operacao: str
|
||||
item: FiscalItem
|
||||
|
||||
@property
|
||||
def uf_destino_tipo(self) -> str:
|
||||
return "interna" if self.uf_origem == self.uf_destino else "interestadual"
|
||||
|
||||
|
||||
class TributoLinha(BaseModel):
|
||||
tax_domain: str
|
||||
cst: str | None
|
||||
csosn: str | None
|
||||
base_calc: Decimal
|
||||
base_calc_percent: Decimal
|
||||
aliquota: Decimal | None
|
||||
valor: Decimal
|
||||
mva: Decimal | None
|
||||
aliquota_st: Decimal | None
|
||||
fcp_percent: Decimal | None
|
||||
codigo_beneficio: str | None
|
||||
rule_id: uuid.UUID
|
||||
|
||||
|
||||
class FiscalResult(BaseModel):
|
||||
cfop: str
|
||||
cst: str | None
|
||||
csosn: str | None
|
||||
origem: str | None
|
||||
consumidor_final: bool
|
||||
indicador_ie: str
|
||||
tributos: list[TributoLinha]
|
||||
|
||||
|
||||
class FiscalConfigError(Exception):
|
||||
"""Fail-closed: configuração fiscal faltante — a emissão DEVE bloquear."""
|
||||
|
||||
def __init__(self, missing: list[str]):
|
||||
self.missing = missing
|
||||
super().__init__(
|
||||
"Configuração fiscal ausente para: " + ", ".join(missing)
|
||||
)
|
||||
|
||||
|
||||
class AmbiguousRuleError(Exception):
|
||||
"""Duas regras casam com o MESMO peso — erro de configuração, nunca
|
||||
escolha silenciosa (decisão do spec, review do frontend)."""
|
||||
|
||||
def __init__(self, tax_domain: str, rule_ids: list[uuid.UUID]):
|
||||
self.tax_domain = tax_domain
|
||||
self.rule_ids = rule_ids
|
||||
super().__init__(
|
||||
f"Regras ambíguas para o domínio {tax_domain!r}: "
|
||||
+ ", ".join(str(r) for r in rule_ids)
|
||||
)
|
||||
|
||||
|
||||
def _matches(rule: TaxRuleLike, op: FiscalOperation) -> int | None:
|
||||
"""Peso da regra para a operação, ou None se algum matcher não casa."""
|
||||
weight = 0
|
||||
checks: list[tuple[str, bool]] = [
|
||||
("crt", rule.crt is None or rule.crt == op.crt),
|
||||
("uf_destino_tipo", rule.uf_destino_tipo is None or rule.uf_destino_tipo == op.uf_destino_tipo),
|
||||
("consumidor_final", rule.consumidor_final is None or rule.consumidor_final == op.consumidor_final),
|
||||
("indicador_ie", rule.indicador_ie is None or rule.indicador_ie == op.indicador_ie),
|
||||
("tipo_operacao", rule.tipo_operacao is None or rule.tipo_operacao == op.tipo_operacao),
|
||||
("ncm_prefix", rule.ncm_prefix is None or op.item.ncm.startswith(rule.ncm_prefix)),
|
||||
("cest", rule.cest is None or rule.cest == op.item.cest),
|
||||
]
|
||||
for campo, ok in checks:
|
||||
if not ok:
|
||||
return None
|
||||
if getattr(rule, campo) is not None:
|
||||
weight += MATCHER_WEIGHTS[campo]
|
||||
return weight
|
||||
|
||||
|
||||
def _pick(rules: list[TaxRuleLike], domain: str, op: FiscalOperation) -> TaxRuleLike | None:
|
||||
candidates: list[tuple[int, TaxRuleLike]] = []
|
||||
for rule in rules:
|
||||
if rule.tax_domain != domain or rule.deleted_at is not None:
|
||||
continue
|
||||
weight = _matches(rule, op)
|
||||
if weight is not None:
|
||||
candidates.append((weight, rule))
|
||||
if not candidates:
|
||||
return None
|
||||
top = max(w for w, _ in candidates)
|
||||
winners = [r for w, r in candidates if w == top]
|
||||
if len(winners) > 1:
|
||||
raise AmbiguousRuleError(domain, [r.id for r in winners])
|
||||
return winners[0]
|
||||
|
||||
|
||||
def _linha(rule: TaxRuleLike, base: Decimal) -> TributoLinha:
|
||||
pct = rule.base_calc_percent if rule.base_calc_percent is not None else Decimal("100")
|
||||
base_efetiva = (base * pct / Decimal("100")).quantize(_CENT, rounding=ROUND_HALF_UP)
|
||||
aliquota = rule.aliquota
|
||||
if rule.tax_domain == TaxDomain.FCP.value:
|
||||
# I1 (auditoria Fable Bloco B): o domínio `fcp` NÃO tem `aliquota`
|
||||
# no catálogo (DOMAIN_FIELDS['fcp'] só declara `fcp_percent`) — o
|
||||
# ramo genérico abaixo (aliquota is None → 0.00) fazia toda linha
|
||||
# FCP sair com valor zerado, um preview que mente. Domínio-explícito
|
||||
# de propósito: NÃO um fallback genérico "aliquota None usa
|
||||
# fcp_percent", que surpreenderia outros domínios (ex.: PIS/COFINS
|
||||
# CST 08/04, onde aliquota None É zero de verdade).
|
||||
taxa = rule.fcp_percent if rule.fcp_percent is not None else Decimal("0")
|
||||
valor = (base_efetiva * taxa / Decimal("100")).quantize(_CENT, rounding=ROUND_HALF_UP)
|
||||
else:
|
||||
valor = (
|
||||
(base_efetiva * aliquota / Decimal("100")).quantize(_CENT, rounding=ROUND_HALF_UP)
|
||||
if aliquota is not None
|
||||
else Decimal("0.00")
|
||||
)
|
||||
return TributoLinha(
|
||||
tax_domain=rule.tax_domain,
|
||||
cst=rule.cst, csosn=rule.csosn,
|
||||
base_calc=base_efetiva, base_calc_percent=pct,
|
||||
aliquota=aliquota, valor=valor,
|
||||
mva=rule.mva, aliquota_st=rule.aliquota_st,
|
||||
fcp_percent=rule.fcp_percent, codigo_beneficio=rule.codigo_beneficio,
|
||||
rule_id=rule.id,
|
||||
)
|
||||
|
||||
|
||||
def _linha_st(rule: TaxRuleLike, base: Decimal, valor_icms_proprio: Decimal) -> TributoLinha:
|
||||
"""ICMS-ST: base_st = base × (1 + MVA%); valor = base_st × aliq_st − ICMS próprio.
|
||||
Para CSOSN 500 (ST retida) mva/aliquota_st são nulos → valor 0, correto."""
|
||||
linha = _linha(rule, base)
|
||||
if rule.mva is not None and rule.aliquota_st is not None:
|
||||
base_st = (base * (Decimal("100") + rule.mva) / Decimal("100")).quantize(
|
||||
_CENT, rounding=ROUND_HALF_UP
|
||||
)
|
||||
bruto = (base_st * rule.aliquota_st / Decimal("100")).quantize(
|
||||
_CENT, rounding=ROUND_HALF_UP
|
||||
)
|
||||
linha = linha.model_copy(
|
||||
update={"base_calc": base_st, "valor": max(Decimal("0.00"), bruto - valor_icms_proprio)}
|
||||
)
|
||||
return linha
|
||||
|
||||
|
||||
def resolve_fiscal(rules: list[TaxRuleLike], op: FiscalOperation) -> FiscalResult:
|
||||
base = (op.item.quantity * op.item.unit_price).quantize(_CENT, rounding=ROUND_HALF_UP)
|
||||
|
||||
# 1. Âncora: ICMS ou ICMS-ST — deve existir, com CFOP + situação.
|
||||
icms_rule = _pick(rules, TaxDomain.ICMS.value, op)
|
||||
icmsst_rule = _pick(rules, TaxDomain.ICMSST.value, op)
|
||||
anchor = icmsst_rule or icms_rule # ST é a situação mais específica quando ambos casam
|
||||
missing: list[str] = []
|
||||
if anchor is None:
|
||||
missing.append("icms (ou icmsst): nenhuma regra casa com a operação")
|
||||
else:
|
||||
if anchor.cfop is None:
|
||||
missing.append(f"cfop na regra {anchor.id} ({anchor.tax_domain})")
|
||||
if anchor.cst is None and anchor.csosn is None:
|
||||
missing.append(f"cst/csosn na regra {anchor.id} ({anchor.tax_domain})")
|
||||
if missing:
|
||||
raise FiscalConfigError(missing)
|
||||
|
||||
# 2. CFOP com transposição (requisito herdado #3).
|
||||
cfop = anchor.cfop
|
||||
if op.uf_destino_tipo == "interestadual":
|
||||
cfop = transpose_cfop(cfop)
|
||||
|
||||
# 3. Tributos: uma linha por domínio que tiver regra casando.
|
||||
tributos: list[TributoLinha] = []
|
||||
icms_linha = _linha(icms_rule, base) if icms_rule is not None else None
|
||||
if icms_linha is not None:
|
||||
tributos.append(icms_linha)
|
||||
if icmsst_rule is not None:
|
||||
valor_proprio = icms_linha.valor if icms_linha is not None else Decimal("0.00")
|
||||
tributos.append(_linha_st(icmsst_rule, base, valor_proprio))
|
||||
for domain in (
|
||||
TaxDomain.IPI, TaxDomain.PIS, TaxDomain.COFINS,
|
||||
TaxDomain.DIFAL, TaxDomain.FCP, TaxDomain.IBS, TaxDomain.CBS, TaxDomain.ISS,
|
||||
):
|
||||
rule = _pick(rules, domain.value, op)
|
||||
if rule is not None:
|
||||
tributos.append(_linha(rule, base))
|
||||
|
||||
return FiscalResult(
|
||||
cfop=cfop,
|
||||
cst=anchor.cst,
|
||||
csosn=anchor.csosn,
|
||||
origem=op.item.origem, # SEMPRE do item (Part) — nunca de regra
|
||||
consumidor_final=op.consumidor_final,
|
||||
indicador_ie=op.indicador_ie,
|
||||
tributos=tributos,
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Códigos tPag da NF-e (tabela do MOC, grupo YA02). String no banco,
|
||||
validação aqui -- um código novo (ex.: futuro meio de pagamento) é uma linha
|
||||
neste dict, sem migration."""
|
||||
|
||||
TPAG_CODES: dict[str, str] = {
|
||||
"01": "Dinheiro",
|
||||
"02": "Cheque",
|
||||
"03": "Cartão de Crédito",
|
||||
"04": "Cartão de Débito",
|
||||
"05": "Crédito Loja",
|
||||
"10": "Vale Alimentação",
|
||||
"11": "Vale Refeição",
|
||||
"12": "Vale Presente",
|
||||
"13": "Vale Combustível",
|
||||
"14": "Duplicata Mercantil",
|
||||
"15": "Boleto Bancário",
|
||||
"16": "Depósito Bancário",
|
||||
"17": "Pagamento Instantâneo (PIX)",
|
||||
"18": "Transferência bancária, Carteira Digital",
|
||||
"19": "Programa de fidelidade, Cashback, Crédito Virtual",
|
||||
"90": "Sem pagamento",
|
||||
"99": "Outros",
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Interface estrutural da regra fiscal — o desacoplamento que permite a lib
|
||||
existir. O produto passa seus objetos (no auto, o ORM TaxRule) e eles
|
||||
satisfazem o Protocol por estrutura, sem herdar nada daqui."""
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class TaxRuleLike(Protocol):
|
||||
id: uuid.UUID
|
||||
tax_domain: str
|
||||
crt: str | None
|
||||
uf_destino_tipo: str | None
|
||||
consumidor_final: bool | None
|
||||
indicador_ie: str | None
|
||||
tipo_operacao: str | None
|
||||
ncm_prefix: str | None
|
||||
cest: str | None
|
||||
cst: str | None
|
||||
csosn: str | None
|
||||
cfop: str | None
|
||||
base_calc_percent: Decimal | None
|
||||
aliquota: Decimal | None
|
||||
mva: Decimal | None
|
||||
aliquota_st: Decimal | None
|
||||
fcp_percent: Decimal | None
|
||||
codigo_beneficio: str | None
|
||||
deleted_at: datetime | None
|
||||
@@ -0,0 +1,563 @@
|
||||
"""Montagem PURA do XML da NF-e 55 (leiaute NT 2025.002 v1.40) -- 1b.1
|
||||
Task 5. Recebe um `DadosEmissao` já COMPLETO (o orquestrador de emissão,
|
||||
`emissao.py`/Task 6, é quem coleta/valida os dados de Branch/Person/Part/
|
||||
FiscalResult e monta este dataclass) e devolve um objeto `Nfe` (bindings
|
||||
xsdata do pacote `nfelib`, os MESMOS usados por `estoque/nfe_import.py`
|
||||
para PARSE -- aqui construímos os mesmos dataclasses em vez de lê-los).
|
||||
|
||||
ZERO I/O, ZERO sessão de banco, ZERO decisão de negócio: este módulo não
|
||||
sabe o que é uma Branch ou uma Sale, só sabe montar o XML a partir dos
|
||||
campos já resolvidos. Fail-closed (409 `fiscal_config_missing` nomeando
|
||||
o campo) é responsabilidade do ORQUESTRADOR -- este builder assume que
|
||||
`DadosEmissao` está completo e, se não estiver (ex.: `None` onde um campo
|
||||
obrigatório do leiaute era esperado), deixa o próprio xsdata/lxml estourar
|
||||
uma exceção genérica (AttributeError/TypeError), não um 409 estruturado.
|
||||
|
||||
Nomes dos campos vêm do binding REAL instalado (nfelib 2.5.2, inspecionado
|
||||
diretamente no venv -- `nfelib.nfe.bindings.v4_0.leiaute_nfe_v4_00.Tnfe.
|
||||
InfNfe` e seus subgrupos -- não chutados): `nfelib.nfe.bindings.v4_0.
|
||||
nfe_v4_00.Nfe` é a raiz `<NFe>`; `.infNFe` é o `Tnfe.InfNfe`.
|
||||
|
||||
Mapeamento de situação tributária ICMS (CST/CSOSN -> grupo do binding):
|
||||
DELIBERADAMENTE parcial -- cobre exatamente as situações alcançáveis pelos
|
||||
presets hoje cadastrados (`fiscal.presets`, CSOSN 102/103/300/400 e 500) e
|
||||
mais alguns CSTs de regime normal (00/40/41/50/60/90) de cobertura óbvia.
|
||||
Uma situação fora desta tabela levanta `UnsupportedIcmsSituationError` --
|
||||
erro claro, não um XML silenciosamente errado -- e a tabela deve crescer
|
||||
quando um preset/regra novo precisar de outra situação.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
from nfelib.nfe.bindings.v4_0.leiaute_nfe_v4_00 import Tendereco, TenderEmi, Tnfe
|
||||
from nfelib.nfe.bindings.v4_0.nfe_v4_00 import Nfe
|
||||
|
||||
from sowai_fiscal.resolver import FiscalResult, TributoLinha
|
||||
|
||||
_InfNfe = Tnfe.InfNfe
|
||||
_Det = _InfNfe.Det
|
||||
_Icms = _Det.Imposto.Icms
|
||||
_Pis = _Det.Imposto.Pis
|
||||
_Cofins = _Det.Imposto.Cofins
|
||||
|
||||
_CENT = Decimal("0.01")
|
||||
|
||||
# Texto oficial (NT NF-e -- ambiente de homologação): substitui o xNome do
|
||||
# destinatário em toda nota emitida em homologação (tpAmb=2), independente
|
||||
# do nome real informado.
|
||||
TEXTO_HOMOLOGACAO = "NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL"
|
||||
|
||||
_SEM_GTIN = "SEM GTIN"
|
||||
|
||||
|
||||
class UnsupportedIcmsSituationError(ValueError):
|
||||
"""CST/CSOSN fora da tabela de mapeamento deste builder (ver docstring
|
||||
do módulo) -- NÃO é um 409 de dado faltante (o motor resolveu uma
|
||||
situação válida), é uma lacuna de COBERTURA deste builder. Deve ser
|
||||
tratado como bug/gap a fechar quando aparecer, não silenciado."""
|
||||
|
||||
def __init__(self, cst: str | None, csosn: str | None):
|
||||
self.cst = cst
|
||||
self.csosn = csosn
|
||||
super().__init__(
|
||||
f"Situação tributária ICMS não suportada pelo builder: cst={cst!r} csosn={csosn!r}"
|
||||
)
|
||||
|
||||
|
||||
# --- dataclasses de input (montados pelo orquestrador, Task 6) -------------
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EmitenteData:
|
||||
cnpj: str
|
||||
razao_social: str
|
||||
nome_fantasia: str | None
|
||||
ie: str
|
||||
crt: str # "1"|"2"|"3" (CRT.value)
|
||||
address_street: str
|
||||
address_number: str
|
||||
address_complement: str | None
|
||||
address_district: str
|
||||
address_city: str
|
||||
address_state: str
|
||||
address_zip: str
|
||||
address_city_ibge_code: str
|
||||
fone: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DestinatarioData:
|
||||
"""`None` no orquestrador == consumidor final não identificado (o
|
||||
grupo `<dest>` inteiro fica ausente -- opcional no schema)."""
|
||||
|
||||
nome: str
|
||||
cnpj: str | None
|
||||
cpf: str | None
|
||||
indicador_ie: str # "1"|"2"|"9" (DestIndIedest)
|
||||
ie: str | None = None
|
||||
address_street: str | None = None
|
||||
address_number: str | None = None
|
||||
address_complement: str | None = None
|
||||
address_district: str | None = None
|
||||
address_city: str | None = None
|
||||
address_state: str | None = None
|
||||
address_zip: str | None = None
|
||||
address_city_ibge_code: str | None = None
|
||||
email: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ItemData:
|
||||
codigo: str # cProd
|
||||
descricao: str # xProd
|
||||
ncm: str
|
||||
cfop: str # já transposto pelo resolvedor (resolve_fiscal)
|
||||
unidade_comercial: str # uCom
|
||||
unidade_tributavel: str # uTrib
|
||||
quantidade: Decimal
|
||||
valor_unitario: Decimal
|
||||
fiscal_result: FiscalResult
|
||||
gtin: str | None = None # None -> "SEM GTIN"
|
||||
cest: str | None = None
|
||||
peso_liquido_kg: Decimal | None = None
|
||||
peso_bruto_kg: Decimal | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PagamentoData:
|
||||
tpag: str
|
||||
valor: Decimal
|
||||
indpag: str = "0" # 0=à vista, 1=a prazo
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DadosEmissao:
|
||||
emitente: EmitenteData
|
||||
itens: list[ItemData]
|
||||
pagamento: PagamentoData
|
||||
ambiente: str # "homologacao"|"producao"
|
||||
chave_acesso: str # 44 dígitos, já montada (chave_acesso.montar_chave_acesso)
|
||||
numero: int
|
||||
serie: int
|
||||
cnf: str
|
||||
dh_emi: str # ISO 8601 com timezone, ex. "2026-07-16T10:00:00-03:00"
|
||||
uf_destino_tipo: str # "interna"|"interestadual" (mesmo campo do resolvedor)
|
||||
destinatario: DestinatarioData | None = None
|
||||
nat_op: str = "Venda"
|
||||
tp_emis: str = "1"
|
||||
ind_final: str = "1" # 1 = consumidor final (venda de balcão, o caso comum da 1b.1)
|
||||
ind_pres: str = "1" # 1 = operação presencial
|
||||
fin_nfe: str = "1" # 1 = NF-e normal
|
||||
ver_proc: str = "sowai-auto/1b.1"
|
||||
|
||||
|
||||
# --- mapeamento CST/CSOSN -> grupo ICMS do binding --------------------------
|
||||
|
||||
|
||||
def _pct(linha: TributoLinha | None) -> str:
|
||||
if linha is None or linha.aliquota is None:
|
||||
return "0.0000"
|
||||
return str(linha.aliquota.quantize(Decimal("0.0001")))
|
||||
|
||||
|
||||
def _val(linha: TributoLinha | None) -> str:
|
||||
if linha is None:
|
||||
return "0.00"
|
||||
return str(linha.valor.quantize(_CENT, rounding=ROUND_HALF_UP))
|
||||
|
||||
|
||||
def _base(linha: TributoLinha | None) -> str:
|
||||
if linha is None:
|
||||
return "0.00"
|
||||
return str(linha.base_calc.quantize(_CENT, rounding=ROUND_HALF_UP))
|
||||
|
||||
|
||||
_CSOSN_SEM_PERMISSAO_CREDITO = {"102", "103", "300", "400"}
|
||||
|
||||
|
||||
def _build_icms(fiscal_result: FiscalResult, icms_linha, icmsst_linha) -> _Icms:
|
||||
csosn = fiscal_result.csosn
|
||||
cst = fiscal_result.cst
|
||||
# I3 (review Opus, 2026-07-16): SEM fallback `or "0"` -- origem ausente
|
||||
# aqui é erro de PROGRAMAÇÃO (a validação de completude do orquestrador,
|
||||
# `emissao.emitir_documento`, barra peça sem `icms_origem` com 409 antes
|
||||
# de chegar neste builder), nunca um default silencioso: emitir "0"
|
||||
# (nacional) para uma peça importada é imposto errado na SEFAZ.
|
||||
if fiscal_result.origem is None:
|
||||
raise ValueError(
|
||||
"fiscal_result.origem é None -- input incompleto para o builder; "
|
||||
"peça sem icms_origem deveria ter sido barrada na validação de "
|
||||
"completude da emissão (409 fiscal_config_missing)"
|
||||
)
|
||||
orig = fiscal_result.origem
|
||||
|
||||
if csosn is not None:
|
||||
if csosn in _CSOSN_SEM_PERMISSAO_CREDITO:
|
||||
return _Icms(ICMSSN102=_Icms.Icmssn102(orig=orig, CSOSN=csosn))
|
||||
if csosn == "500":
|
||||
return _Icms(
|
||||
ICMSSN500=_Icms.Icmssn500(
|
||||
orig=orig,
|
||||
CSOSN=csosn,
|
||||
vBCSTRet=_base(icmsst_linha),
|
||||
pST=_pct(icmsst_linha),
|
||||
vICMSSTRet=_val(icmsst_linha),
|
||||
)
|
||||
)
|
||||
if csosn == "900":
|
||||
return _Icms(
|
||||
ICMSSN900=_Icms.Icmssn900(
|
||||
orig=orig,
|
||||
CSOSN=csosn,
|
||||
modBC="0",
|
||||
vBC=_base(icms_linha),
|
||||
pICMS=_pct(icms_linha),
|
||||
vICMS=_val(icms_linha),
|
||||
)
|
||||
)
|
||||
raise UnsupportedIcmsSituationError(cst, csosn)
|
||||
|
||||
if cst is not None:
|
||||
if cst == "00":
|
||||
return _Icms(
|
||||
ICMS00=_Icms.Icms00(
|
||||
orig=orig, CST=cst, modBC="0",
|
||||
vBC=_base(icms_linha), pICMS=_pct(icms_linha), vICMS=_val(icms_linha),
|
||||
)
|
||||
)
|
||||
if cst in ("40", "41", "50"):
|
||||
return _Icms(ICMS40=_Icms.Icms40(orig=orig, CST=cst))
|
||||
if cst == "60":
|
||||
return _Icms(
|
||||
ICMS60=_Icms.Icms60(
|
||||
orig=orig, CST=cst,
|
||||
vBCSTRet=_base(icmsst_linha), pST=_pct(icmsst_linha),
|
||||
vICMSSTRet=_val(icmsst_linha),
|
||||
)
|
||||
)
|
||||
if cst == "90":
|
||||
return _Icms(
|
||||
ICMS90=_Icms.Icms90(
|
||||
orig=orig, CST=cst, modBC="0",
|
||||
vBC=_base(icms_linha), pICMS=_pct(icms_linha), vICMS=_val(icms_linha),
|
||||
vBCST=_base(icmsst_linha), pICMSST=_pct(icmsst_linha), vICMSST=_val(icmsst_linha),
|
||||
)
|
||||
)
|
||||
raise UnsupportedIcmsSituationError(cst, csosn)
|
||||
|
||||
raise UnsupportedIcmsSituationError(cst, csosn)
|
||||
|
||||
|
||||
# CST 04-09 -> "NT" (não tributado/isento/suspenso -- só CST, sem base/aliq);
|
||||
# 01/02 -> Aliq (percentual); 03 -> Qtde; qualquer outro -> Outr ("99"),
|
||||
# mesma tabela para PIS e COFINS (os dois têm as mesmas 4 famílias de grupo).
|
||||
_PIS_COFINS_NT_CSTS = {"04", "05", "06", "07", "08", "09"}
|
||||
|
||||
|
||||
def _build_pis(linha: TributoLinha | None) -> _Pis:
|
||||
if linha is None or linha.cst is None:
|
||||
return _Pis(PISNT=_Pis.Pisnt(CST="08"))
|
||||
cst = linha.cst
|
||||
if cst in _PIS_COFINS_NT_CSTS:
|
||||
return _Pis(PISNT=_Pis.Pisnt(CST=cst))
|
||||
if cst in ("01", "02"):
|
||||
return _Pis(
|
||||
PISAliq=_Pis.Pisaliq(CST=cst, vBC=_base(linha), pPIS=_pct(linha), vPIS=_val(linha))
|
||||
)
|
||||
return _Pis(PISOutr=_Pis.Pisoutr(CST="99", vBC=_base(linha), pPIS=_pct(linha), vPIS=_val(linha)))
|
||||
|
||||
|
||||
def _build_cofins(linha: TributoLinha | None) -> _Cofins:
|
||||
if linha is None or linha.cst is None:
|
||||
return _Cofins(COFINSNT=_Cofins.Cofinsnt(CST="08"))
|
||||
cst = linha.cst
|
||||
if cst in _PIS_COFINS_NT_CSTS:
|
||||
return _Cofins(COFINSNT=_Cofins.Cofinsnt(CST=cst))
|
||||
if cst in ("01", "02"):
|
||||
return _Cofins(
|
||||
COFINSAliq=_Cofins.Cofinsaliq(
|
||||
CST=cst, vBC=_base(linha), pCOFINS=_pct(linha), vCOFINS=_val(linha)
|
||||
)
|
||||
)
|
||||
return _Cofins(
|
||||
COFINSOutr=_Cofins.Cofinsoutr(
|
||||
CST="99", vBC=_base(linha), pCOFINS=_pct(linha), vCOFINS=_val(linha)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _linha_por_dominio(fiscal_result: FiscalResult, dominio: str) -> TributoLinha | None:
|
||||
for linha in fiscal_result.tributos:
|
||||
if linha.tax_domain == dominio:
|
||||
return linha
|
||||
return None
|
||||
|
||||
|
||||
# M4 (auditoria Fable, 2026-07-16): SEM fonte real -- ver o docstring de
|
||||
# `_build_ibscbs`. Módulo-level (não literais inline) de propósito, para o
|
||||
# nome do valor já denunciar no call-site que é fabricado.
|
||||
_PLACEHOLDER_CST_SEM_FONTE_REAL = "000"
|
||||
_PLACEHOLDER_CCLASSTRIB_SEM_FONTE_REAL = "000001"
|
||||
|
||||
|
||||
def _build_ibscbs(fiscal_result: FiscalResult):
|
||||
"""Grupo UB (IBS/CBS, RTC v1.40) -- só presente quando o `FiscalResult`
|
||||
tiver linhas `ibs`/`cbs` (CRT 1 em 2026 nunca tem -- R1 do motor exige
|
||||
isso a partir de 04/01/2027, ver spec). Best-effort ESTRUTURAL: os
|
||||
valores de `vBC`/`pIBSUF`/`vIBSUF`/`pCBS`/`vCBS` vêm das linhas
|
||||
resolvidas (REAIS -- `aliquota`/`valor`/`base_calc` do `TributoLinha`,
|
||||
configurados pelo tenant na regra), mas `CST`/`cClassTrib` abaixo são
|
||||
FABRICADOS ("000"/"000001") -- não existe fonte real para nenhum dos
|
||||
dois hoje: `DOMAIN_FIELDS['ibs'|'cbs']` nem aceita `cst`/
|
||||
`codigo_beneficio` no cadastro da regra, só `aliquota`/
|
||||
`base_calc_percent` (ver `fiscal.domains`). O grupo RTC completo
|
||||
(gIBSUF/gIBSMun/gCBS com suas dezenas de sub-campos) também ainda não
|
||||
tem um mapeamento tão maduro quanto o ICMS legado.
|
||||
|
||||
M4 (auditoria Fable, 2026-07-16): esta função é PROVADAMENTE
|
||||
inalcançável a partir da emissão real -- `fiscal.emissao.
|
||||
emitir_documento` barra com 409 `fiscal_config_missing` ANTES de
|
||||
montar o XML sempre que algum item resolve uma linha ibs/cbs
|
||||
(exatamente o caso em que este `if` abaixo deixaria de devolver
|
||||
`None`). Só existe aqui para o builder PURO continuar com o suporte
|
||||
ESTRUTURAL testável isoladamente (`test_grupo_ibscbs_presente_com_
|
||||
linhas_ibs_cbs`) -- endurecer (CST/cClassTrib reais) junto com o R1 do
|
||||
motor, quando o catálogo de domínios ganhar esses campos."""
|
||||
ibs_linha = _linha_por_dominio(fiscal_result, "ibs")
|
||||
cbs_linha = _linha_por_dominio(fiscal_result, "cbs")
|
||||
if ibs_linha is None and cbs_linha is None:
|
||||
return None
|
||||
|
||||
from nfelib.nfe.bindings.v4_0.dfe_tipos_basicos_v1_00 import Tcibs, TtribNfe
|
||||
|
||||
base = ibs_linha or cbs_linha
|
||||
g_ibs_uf = (
|
||||
Tcibs.GIbsuf(pIBSUF=_pct(ibs_linha), vIBSUF=_val(ibs_linha)) if ibs_linha else None
|
||||
)
|
||||
g_cbs = Tcibs.GCbs(pCBS=_pct(cbs_linha), vCBS=_val(cbs_linha)) if cbs_linha else None
|
||||
|
||||
gibscbs = Tcibs(vBC=_base(base), gIBSUF=g_ibs_uf, gCBS=g_cbs)
|
||||
# M4: nomeados _PLACEHOLDER_* (não `CST`/`cClassTrib` soltos) de
|
||||
# propósito -- deixa explícito, no próprio call-site, que estes DOIS
|
||||
# valores não vêm de lugar nenhum real (ver o docstring acima).
|
||||
return TtribNfe(
|
||||
CST=_PLACEHOLDER_CST_SEM_FONTE_REAL,
|
||||
cClassTrib=_PLACEHOLDER_CCLASSTRIB_SEM_FONTE_REAL,
|
||||
gIBSCBS=gibscbs,
|
||||
)
|
||||
|
||||
|
||||
def _build_det(item: ItemData, n_item: int) -> _Det:
|
||||
fr = item.fiscal_result
|
||||
icms_linha = _linha_por_dominio(fr, "icms")
|
||||
icmsst_linha = _linha_por_dominio(fr, "icmsst")
|
||||
pis_linha = _linha_por_dominio(fr, "pis")
|
||||
cofins_linha = _linha_por_dominio(fr, "cofins")
|
||||
|
||||
v_prod = (item.quantidade * item.valor_unitario).quantize(_CENT, rounding=ROUND_HALF_UP)
|
||||
|
||||
prod = _Det.Prod(
|
||||
cProd=item.codigo,
|
||||
cEAN=item.gtin or _SEM_GTIN,
|
||||
xProd=item.descricao,
|
||||
NCM=item.ncm,
|
||||
CEST=item.cest,
|
||||
CFOP=item.cfop,
|
||||
uCom=item.unidade_comercial,
|
||||
qCom=str(item.quantidade),
|
||||
vUnCom=str(item.valor_unitario),
|
||||
vProd=str(v_prod),
|
||||
cEANTrib=item.gtin or _SEM_GTIN,
|
||||
uTrib=item.unidade_tributavel,
|
||||
qTrib=str(item.quantidade),
|
||||
vUnTrib=str(item.valor_unitario),
|
||||
indTot="1",
|
||||
)
|
||||
imposto = _Det.Imposto(
|
||||
ICMS=_build_icms(fr, icms_linha, icmsst_linha),
|
||||
PIS=_build_pis(pis_linha),
|
||||
COFINS=_build_cofins(cofins_linha),
|
||||
IBSCBS=_build_ibscbs(fr),
|
||||
)
|
||||
return _Det(prod=prod, imposto=imposto, nItem=str(n_item))
|
||||
|
||||
|
||||
def soma_itens_quantizados(itens: list[ItemData]) -> Decimal:
|
||||
"""Fonte ÚNICA do somatório de produtos (I1, review Opus 2026-07-16):
|
||||
soma dos `vProd` POR ITEM já quantizados (2 casas, ROUND_HALF_UP) --
|
||||
exatamente o valor que aparece em cada `<det><prod><vProd>` e, portanto,
|
||||
o único somatório que fecha com `vProd`/`vNF` do `<ICMSTot>`. O `vPag`
|
||||
do orquestrador (`emissao.py`) TEM que derivar daqui também: qualquer
|
||||
outro caminho (ex.: somar `qtd*preço` cru e quantizar só o agregado)
|
||||
diverge em 1 centavo quando um item cai em fração de centavo, e a SEFAZ
|
||||
rejeita com "Valor do Pagamento difere do total"."""
|
||||
total = sum(
|
||||
(item.quantidade * item.valor_unitario).quantize(_CENT, rounding=ROUND_HALF_UP)
|
||||
for item in itens
|
||||
)
|
||||
return Decimal(total).quantize(_CENT, rounding=ROUND_HALF_UP)
|
||||
|
||||
|
||||
def _sum_valor(itens: list[ItemData], dominio: str) -> Decimal:
|
||||
total = Decimal("0.00")
|
||||
for item in itens:
|
||||
linha = _linha_por_dominio(item.fiscal_result, dominio)
|
||||
if linha is not None:
|
||||
total += linha.valor
|
||||
return total.quantize(_CENT, rounding=ROUND_HALF_UP)
|
||||
|
||||
|
||||
def _sum_base(itens: list[ItemData], dominio: str) -> Decimal:
|
||||
total = Decimal("0.00")
|
||||
for item in itens:
|
||||
linha = _linha_por_dominio(item.fiscal_result, dominio)
|
||||
if linha is not None:
|
||||
total += linha.base_calc
|
||||
return total.quantize(_CENT, rounding=ROUND_HALF_UP)
|
||||
|
||||
|
||||
def _build_total(itens: list[ItemData]) -> _InfNfe.Total:
|
||||
v_prod = soma_itens_quantizados(itens)
|
||||
v_icms = _sum_valor(itens, "icms")
|
||||
v_bc = _sum_base(itens, "icms")
|
||||
v_st = _sum_valor(itens, "icmsst")
|
||||
v_bcst = _sum_base(itens, "icmsst")
|
||||
v_pis = _sum_valor(itens, "pis")
|
||||
v_cofins = _sum_valor(itens, "cofins")
|
||||
zero = "0.00"
|
||||
|
||||
icms_tot = _InfNfe.Total.Icmstot(
|
||||
vBC=str(v_bc), vICMS=str(v_icms), vICMSDeson=zero, vFCP=zero,
|
||||
vBCST=str(v_bcst), vST=str(v_st), vFCPST=zero, vFCPSTRet=zero,
|
||||
vProd=str(v_prod), vFrete=zero, vSeg=zero, vDesc=zero,
|
||||
vII=zero, vIPI=zero, vIPIDevol=zero, vPIS=str(v_pis), vCOFINS=str(v_cofins),
|
||||
vOutro=zero, vNF=str(v_prod),
|
||||
)
|
||||
return _InfNfe.Total(ICMSTot=icms_tot)
|
||||
|
||||
|
||||
def _build_ide(dados: DadosEmissao) -> _InfNfe.Ide:
|
||||
id_dest = "1" if dados.uf_destino_tipo == "interna" else "2"
|
||||
return _InfNfe.Ide(
|
||||
cUF=dados.chave_acesso[0:2],
|
||||
cNF=dados.cnf,
|
||||
natOp=dados.nat_op,
|
||||
mod="55",
|
||||
serie=str(dados.serie),
|
||||
nNF=str(dados.numero),
|
||||
dhEmi=dados.dh_emi,
|
||||
tpNF="1",
|
||||
idDest=id_dest,
|
||||
cMunFG=dados.emitente.address_city_ibge_code,
|
||||
tpImp="1",
|
||||
tpEmis=dados.tp_emis,
|
||||
cDV=dados.chave_acesso[-1],
|
||||
tpAmb="2" if dados.ambiente == "homologacao" else "1",
|
||||
finNFe=dados.fin_nfe,
|
||||
indFinal=dados.ind_final,
|
||||
indPres=dados.ind_pres,
|
||||
procEmi="0",
|
||||
verProc=dados.ver_proc,
|
||||
)
|
||||
|
||||
|
||||
def _build_emit(emitente: EmitenteData) -> _InfNfe.Emit:
|
||||
ender = TenderEmi(
|
||||
xLgr=emitente.address_street,
|
||||
nro=emitente.address_number,
|
||||
xCpl=emitente.address_complement,
|
||||
xBairro=emitente.address_district,
|
||||
cMun=emitente.address_city_ibge_code,
|
||||
xMun=emitente.address_city,
|
||||
UF=emitente.address_state,
|
||||
CEP=emitente.address_zip,
|
||||
cPais="1058",
|
||||
xPais="Brasil",
|
||||
fone=emitente.fone,
|
||||
)
|
||||
return _InfNfe.Emit(
|
||||
CNPJ=emitente.cnpj,
|
||||
xNome=emitente.razao_social,
|
||||
xFant=emitente.nome_fantasia,
|
||||
enderEmit=ender,
|
||||
IE=emitente.ie,
|
||||
CRT=emitente.crt,
|
||||
)
|
||||
|
||||
|
||||
def _build_dest(dados: DadosEmissao) -> _InfNfe.Dest | None:
|
||||
dest = dados.destinatario
|
||||
if dest is None:
|
||||
return None
|
||||
|
||||
x_nome = TEXTO_HOMOLOGACAO if dados.ambiente == "homologacao" else dest.nome
|
||||
|
||||
ender = None
|
||||
if dest.address_street is not None:
|
||||
ender = Tendereco(
|
||||
xLgr=dest.address_street,
|
||||
nro=dest.address_number,
|
||||
xCpl=dest.address_complement,
|
||||
xBairro=dest.address_district,
|
||||
cMun=dest.address_city_ibge_code,
|
||||
xMun=dest.address_city,
|
||||
UF=dest.address_state,
|
||||
CEP=dest.address_zip,
|
||||
cPais="1058",
|
||||
xPais="Brasil",
|
||||
)
|
||||
|
||||
return _InfNfe.Dest(
|
||||
CNPJ=dest.cnpj,
|
||||
CPF=dest.cpf,
|
||||
xNome=x_nome,
|
||||
enderDest=ender,
|
||||
indIEDest=dest.indicador_ie,
|
||||
IE=dest.ie,
|
||||
email=dest.email,
|
||||
)
|
||||
|
||||
|
||||
def _build_transp() -> _InfNfe.Transp:
|
||||
# 1b.1: venda de balcão/PDV -- sem transportador/volume rastreado ainda
|
||||
# (Sale/SaleItem não carregam esses dados). modFrete=9 "Sem Ocorrência
|
||||
# de Transporte" é o valor correto para essa realidade, não um chute.
|
||||
return _InfNfe.Transp(modFrete="9")
|
||||
|
||||
|
||||
def _build_pag(pagamento: PagamentoData) -> _InfNfe.Pag:
|
||||
det_pag = _InfNfe.Pag.DetPag(
|
||||
indPag=pagamento.indpag,
|
||||
tPag=pagamento.tpag,
|
||||
vPag=str(pagamento.valor.quantize(_CENT, rounding=ROUND_HALF_UP)),
|
||||
)
|
||||
return _InfNfe.Pag(detPag=[det_pag])
|
||||
|
||||
|
||||
def build_nfe(dados: DadosEmissao) -> Nfe:
|
||||
"""Monta o objeto `Nfe` (nfelib) completo a partir de `DadosEmissao` --
|
||||
PURA: nenhuma chamada de rede/banco, nenhuma decisão de negócio (tudo
|
||||
que precisava de contexto -- CFOP, CST/CSOSN, ambiente, chave -- já
|
||||
veio resolvido no input pelo orquestrador). O `Id` do `infNFe` é
|
||||
`"NFe" + chave_acesso` (44 dígitos), exatamente o formato que
|
||||
`erpbrasil.assinatura` referencia na assinatura (Task 6)."""
|
||||
ide = _build_ide(dados)
|
||||
emit = _build_emit(dados.emitente)
|
||||
dest = _build_dest(dados)
|
||||
dets = [_build_det(item, n) for n, item in enumerate(dados.itens, start=1)]
|
||||
total = _build_total(dados.itens)
|
||||
transp = _build_transp()
|
||||
pag = _build_pag(dados.pagamento)
|
||||
|
||||
inf_nfe = _InfNfe(
|
||||
ide=ide,
|
||||
emit=emit,
|
||||
dest=dest,
|
||||
det=dets,
|
||||
total=total,
|
||||
transp=transp,
|
||||
pag=pag,
|
||||
versao="4.00",
|
||||
Id="NFe" + dados.chave_acesso,
|
||||
)
|
||||
return Nfe(infNFe=inf_nfe)
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Chave de acesso NF-e: cUF(2)+AAMM(4)+CNPJ(14)+mod(2)+série(3)+nNF(9)+
|
||||
tpEmis(1)+cNF(8)+DV(1) = 44 dígitos. DV = módulo 11 (pesos 2..9, da direita
|
||||
para a esquerda; resto 0 ou 1 -> DV 0).
|
||||
|
||||
`dv_modulo11` é validado contra chaves REAIS (não inventadas) -- mas NÃO as
|
||||
de `tests/modules/estoque/test_nfe_import.py` como o plano original
|
||||
cogitava: aquele fixture é hand-written (docstring do próprio arquivo o diz)
|
||||
e tem só UMA chave de 44 dígitos, cujo último dígito NÃO bate com o DV
|
||||
módulo 11 (conferido por script antes de escrever este teste -- não é uma
|
||||
chave real, é um ID de exemplo forjado só para exercitar o parser). Fonte
|
||||
usada em vez disso: os XMLs de amostra que o próprio pacote `nfelib`
|
||||
(dependência já pinada em pyproject.toml, versão instalada no pod
|
||||
confirmada em 2.5.2 na Task 1 Step 0) empacota em
|
||||
`nfelib/nfe/samples/v4_0/leiauteNFe/*.xml` -- os NOMES desses arquivos SÃO
|
||||
as chaves de acesso de NF-e reais/realistas de exemplo, com DV
|
||||
correto -- confirmado por decomposição manual antes de escrever este teste:
|
||||
`35180834128745000152550010000476711079516696` (mod=55, DV correto=6) e
|
||||
`35200159594315000157550010000000012062777161` (mod=55, DV correto=1,
|
||||
também o `Id` do `infNFe` dentro de
|
||||
`NFe35200159594315000157550010000000012062777161.xml`)."""
|
||||
import pytest
|
||||
|
||||
from sowai_fiscal.chave_acesso import dv_modulo11, gerar_cnf, montar_chave_acesso
|
||||
|
||||
|
||||
def test_dv_modulo11_contra_chave_real_1_amostra_nfelib():
|
||||
chave44 = "35180834128745000152550010000476711079516696"
|
||||
assert dv_modulo11(chave44[:43]) == chave44[43]
|
||||
|
||||
|
||||
def test_dv_modulo11_contra_chave_real_2_amostra_nfelib():
|
||||
chave44 = "35200159594315000157550010000000012062777161"
|
||||
assert dv_modulo11(chave44[:43]) == chave44[43]
|
||||
|
||||
|
||||
def test_dv_modulo11_rejeita_chave_com_tamanho_errado():
|
||||
with pytest.raises(ValueError):
|
||||
dv_modulo11("123")
|
||||
|
||||
|
||||
def test_montar_chave_tem_44_digitos_e_dv_valido():
|
||||
chave = montar_chave_acesso(
|
||||
uf_ibge="41", aamm="2607", cnpj="12345678000190",
|
||||
modelo="55", serie=1, numero=1014, tp_emis="1", cnf="87654321",
|
||||
)
|
||||
assert len(chave) == 44 and chave.isdigit()
|
||||
assert chave[-1] == dv_modulo11(chave[:43])
|
||||
assert chave[22:25] == "001" and chave[25:34] == "000001014"
|
||||
|
||||
|
||||
def test_gerar_cnf_8_digitos_e_diferente_do_nnf():
|
||||
for _ in range(50):
|
||||
cnf = gerar_cnf(numero_nnf=1014)
|
||||
assert len(cnf) == 8 and cnf.isdigit()
|
||||
assert int(cnf) != 1014 # regra NT2019.001: cNF != nNF
|
||||
|
||||
|
||||
def test_gerar_cnf_nunca_all_zeros():
|
||||
# F3a (review 2026-07-16): all-zeros é 8 dígitos válidos no FORMATO mas
|
||||
# é o chute óbvio de um gerador fraco -- nunca deve sair do sorteio,
|
||||
# mesmo por acaso estatístico (1 em 10^8 por chamada; 200 chamadas aqui
|
||||
# deixa a chance de falso-negativo desprezível).
|
||||
for _ in range(200):
|
||||
assert gerar_cnf(numero_nnf=1014) != "00000000"
|
||||
|
||||
|
||||
def test_montar_chave_acesso_rejeita_componentes_que_se_compensam_no_total():
|
||||
# F1 (review 2026-07-16, Important): o bug real -- checar SÓ o tamanho
|
||||
# agregado (43 dígitos) deixava passar um CNPJ de 13 dígitos (errado,
|
||||
# deveria ter 14) somado a um cNF de 9 dígitos (errado, deveria ter 8):
|
||||
# 13 + 9 = 22, o mesmo total de dígitos que o par correto 14 + 8 = 22 --
|
||||
# a chave "compensada" saía com 43 dígitos e um DV módulo-11 sintaticamente
|
||||
# válido, mas apontando para o CNPJ ERRADO (um dígito do cNF vazando para
|
||||
# dentro do que deveria ser o CNPJ). A validação por COMPONENTE (não só o
|
||||
# total) tem que capturar isto.
|
||||
with pytest.raises(ValueError, match="cnpj"):
|
||||
montar_chave_acesso(
|
||||
uf_ibge="41", aamm="2607", cnpj="1234567800019", # 13 dígitos, não 14
|
||||
modelo="55", serie=1, numero=1014, tp_emis="1",
|
||||
cnf="876543210", # 9 dígitos, não 8 -- compensa o total em 43
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"campo,valor",
|
||||
[
|
||||
("uf_ibge", "4"),
|
||||
("uf_ibge", "411"),
|
||||
("aamm", "260"),
|
||||
("cnpj", "1234567800019"),
|
||||
("cnpj", "123456780001900"),
|
||||
("modelo", "5"),
|
||||
("tp_emis", "12"),
|
||||
("cnf", "8765432"),
|
||||
("cnf", "876543210"),
|
||||
],
|
||||
)
|
||||
def test_montar_chave_acesso_rejeita_cada_componente_fora_do_tamanho(campo, valor):
|
||||
kwargs = dict(
|
||||
uf_ibge="41", aamm="2607", cnpj="12345678000190",
|
||||
modelo="55", serie=1, numero=1014, tp_emis="1", cnf="87654321",
|
||||
)
|
||||
kwargs[campo] = valor
|
||||
with pytest.raises(ValueError):
|
||||
montar_chave_acesso(**kwargs)
|
||||
|
||||
|
||||
def test_montar_chave_acesso_rejeita_componente_nao_numerico():
|
||||
with pytest.raises(ValueError, match="cnpj"):
|
||||
montar_chave_acesso(
|
||||
uf_ibge="41", aamm="2607", cnpj="1234567800019A",
|
||||
modelo="55", serie=1, numero=1014, tp_emis="1", cnf="87654321",
|
||||
)
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Bloco B (spec 2026-07-14): catálogo de domínios tributários.
|
||||
|
||||
`tax_domain` é String no banco de propósito (imposto-como-dado): um domínio
|
||||
novo (IBS/CBS/futuro) é um membro novo NO ENUM PYTHON, sem ALTER TYPE, sem
|
||||
migration, sem o gotcha enum-NAME-vs-value. Estes testes travam o contrato
|
||||
que o frontend consome via GET /fiscal/tax-domains e a tabela de pesos/
|
||||
transposição que o resolvedor usa.
|
||||
"""
|
||||
from sowai_fiscal.domains import (
|
||||
DOMAIN_FIELDS,
|
||||
MATCHER_WEIGHTS,
|
||||
TaxDomain,
|
||||
transpose_cfop,
|
||||
)
|
||||
|
||||
|
||||
def test_tax_domain_has_the_reform_domains_without_any_migration():
|
||||
# A prova literal do "imposto-como-dado": IBS/CBS já são domínios aceitos
|
||||
# hoje, e não existe NENHUM tipo enum Postgres para tax_domain.
|
||||
assert TaxDomain.IBS.value == "ibs"
|
||||
assert TaxDomain.CBS.value == "cbs"
|
||||
assert {d.value for d in TaxDomain} >= {
|
||||
"icms", "icmsst", "ipi", "pis", "cofins", "difal", "fcp", "ibs", "cbs", "iss",
|
||||
}
|
||||
|
||||
|
||||
def test_domain_fields_catalog_covers_every_domain():
|
||||
# O frontend monta o form por domínio a partir deste catálogo — cada
|
||||
# domínio precisa declarar campos aplicáveis e obrigatórios.
|
||||
for domain in TaxDomain:
|
||||
spec = DOMAIN_FIELDS[domain.value]
|
||||
assert spec.label
|
||||
assert set(spec.campos_obrigatorios) <= set(spec.campos_aplicaveis)
|
||||
for grupo in spec.campos_um_de:
|
||||
assert set(grupo) <= set(spec.campos_aplicaveis)
|
||||
assert not set(grupo) & set(spec.campos_obrigatorios) # um-de nunca também obrigatório
|
||||
|
||||
|
||||
def test_icms_declares_the_exclusive_situacao_group():
|
||||
# A UI exige "situação tributária" (cst OU csosn) a partir DAQUI — sem
|
||||
# este grupo no catálogo, o form dinâmico deixaria passar regra de ICMS
|
||||
# sem situação e o usuário comeria um 422 imprevisto (review frontend).
|
||||
assert ("cst", "csosn") in DOMAIN_FIELDS["icms"].campos_um_de
|
||||
assert ("cst", "csosn") in DOMAIN_FIELDS["icmsst"].campos_um_de
|
||||
|
||||
|
||||
def test_matcher_weights_are_strict_powers_of_two():
|
||||
# Cada peso supera a SOMA de todos os mais fracos (64 > 63): nenhuma
|
||||
# combinação de matchers genéricos vence um mais seletivo. É a correção
|
||||
# do review do frontend sobre "contagem de matchers".
|
||||
ordered = sorted(MATCHER_WEIGHTS.values())
|
||||
for i, w in enumerate(ordered):
|
||||
assert w > sum(ordered[:i])
|
||||
assert MATCHER_WEIGHTS["ncm_prefix"] == 64
|
||||
assert MATCHER_WEIGHTS["cest"] == 32
|
||||
assert MATCHER_WEIGHTS["consumidor_final"] == 16
|
||||
assert MATCHER_WEIGHTS["indicador_ie"] == 8
|
||||
assert MATCHER_WEIGHTS["uf_destino_tipo"] == 4
|
||||
assert MATCHER_WEIGHTS["tipo_operacao"] == 2
|
||||
assert MATCHER_WEIGHTS["crt"] == 1
|
||||
|
||||
|
||||
def test_transpose_cfop_uses_the_explicit_table_for_st():
|
||||
# 5405 → 6403, NÃO 6405: é por isso que a tabela explícita existe.
|
||||
assert transpose_cfop("5405") == "6403"
|
||||
assert transpose_cfop("5102") == "6102"
|
||||
assert transpose_cfop("1202") == "2202"
|
||||
|
||||
|
||||
def test_transpose_cfop_generic_fallback_swaps_the_first_digit():
|
||||
# CFOP fora da tabela: regra geral 5xxx→6xxx / 1xxx→2xxx.
|
||||
assert transpose_cfop("5949") == "6949"
|
||||
assert transpose_cfop("1949") == "2949"
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Presets fiscais seed-editáveis: teste PURO do dict `PRESETS` (dados,
|
||||
não a API `/tax-profiles/apply-preset/*` -- aquela é do produto, DB/HTTP,
|
||||
não faz parte do núcleo puro extraído). Os valores REAIS vieram do iCode
|
||||
da Thiago (perfis "Padrão" cód 110 e "Óleos/ST" cód 108) -- estes testes
|
||||
travam o contrato de dados que o consumidor (auto) usa para semear
|
||||
`TaxRule`/`TaxProfile`."""
|
||||
from decimal import Decimal
|
||||
|
||||
from sowai_fiscal.presets import PRESETS, FiscalPreset, PresetRule
|
||||
|
||||
|
||||
def test_presets_catalog_has_the_two_icode_profiles():
|
||||
assert {"autopecas_simples_padrao", "autopecas_simples_st"} <= PRESETS.keys()
|
||||
|
||||
|
||||
def test_padrao_preset_has_venda_and_devolucao_icms_rules():
|
||||
preset = PRESETS["autopecas_simples_padrao"]
|
||||
assert isinstance(preset, FiscalPreset)
|
||||
assert preset.regime_alvo == "1"
|
||||
|
||||
icms_rules = [r for r in preset.rules if r.tax_domain == "icms"]
|
||||
assert len(icms_rules) == 2
|
||||
|
||||
venda = next(r for r in icms_rules if r.tipo_operacao == "venda")
|
||||
assert venda.csosn == "102"
|
||||
assert venda.cfop == "5102"
|
||||
assert venda.aliquota == Decimal("0")
|
||||
|
||||
devolucao = next(r for r in icms_rules if r.tipo_operacao == "devolucao")
|
||||
assert devolucao.csosn == "102"
|
||||
assert devolucao.cfop == "1202"
|
||||
|
||||
|
||||
def test_padrao_preset_pis_cofins_cst_08():
|
||||
preset = PRESETS["autopecas_simples_padrao"]
|
||||
pis = next(r for r in preset.rules if r.tax_domain == "pis")
|
||||
cofins = next(r for r in preset.rules if r.tax_domain == "cofins")
|
||||
assert pis.cst == "08"
|
||||
assert cofins.cst == "08"
|
||||
assert pis.aliquota == Decimal("0")
|
||||
assert cofins.aliquota == Decimal("0")
|
||||
|
||||
|
||||
def test_st_preset_has_venda_and_devolucao_icmsst_rules():
|
||||
preset = PRESETS["autopecas_simples_st"]
|
||||
icmsst_rules = [r for r in preset.rules if r.tax_domain == "icmsst"]
|
||||
assert len(icmsst_rules) == 2
|
||||
|
||||
venda = next(r for r in icmsst_rules if r.tipo_operacao == "venda")
|
||||
assert venda.csosn == "500"
|
||||
assert venda.cfop == "5405"
|
||||
|
||||
devolucao = next(r for r in icmsst_rules if r.tipo_operacao == "devolucao")
|
||||
assert devolucao.csosn == "500"
|
||||
assert devolucao.cfop == "1202"
|
||||
|
||||
|
||||
def test_st_preset_pis_cofins_cst_04_monofasico():
|
||||
preset = PRESETS["autopecas_simples_st"]
|
||||
pis = next(r for r in preset.rules if r.tax_domain == "pis")
|
||||
cofins = next(r for r in preset.rules if r.tax_domain == "cofins")
|
||||
assert pis.cst == "04"
|
||||
assert cofins.cst == "04"
|
||||
|
||||
|
||||
def test_no_preset_rule_carries_origem():
|
||||
# NENHUM preset carrega `origem`: origem é sempre do Part (review do
|
||||
# frontend, 2026-07-14) -- `PresetRule` não tem sequer o campo.
|
||||
assert not hasattr(PresetRule(tax_domain="icms"), "origem")
|
||||
@@ -0,0 +1,217 @@
|
||||
"""Resolvedor puro do Bloco B. Sem banco, sem I/O — TaxRule montada em
|
||||
memória. Os dois perfis REAIS da Thiago (extração do iCode,
|
||||
thoughts/2026-07-13-icode-config-fiscal-extraida.md) são casos de teste
|
||||
literais: CSOSN 102 → 5102/6102; CSOSN 500 → 5405/6403."""
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from sowai_fiscal.resolver import (
|
||||
AmbiguousRuleError,
|
||||
FiscalConfigError,
|
||||
FiscalItem,
|
||||
FiscalOperation,
|
||||
resolve_fiscal,
|
||||
)
|
||||
|
||||
_PROFILE = uuid.uuid4()
|
||||
_ORG = uuid.uuid4()
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeTaxRule:
|
||||
"""Satisfaz `sowai_fiscal.types.TaxRuleLike` por estrutura — o
|
||||
substituto, nos testes da lib, do ORM `TaxRule` que o auto usa."""
|
||||
|
||||
id: uuid.UUID
|
||||
organization_id: uuid.UUID
|
||||
tax_profile_id: uuid.UUID
|
||||
tax_domain: str
|
||||
crt: str | None = None
|
||||
uf_destino_tipo: str | None = None
|
||||
consumidor_final: bool | None = None
|
||||
indicador_ie: str | None = None
|
||||
tipo_operacao: str | None = None
|
||||
ncm_prefix: str | None = None
|
||||
cest: str | None = None
|
||||
cst: str | None = None
|
||||
csosn: str | None = None
|
||||
cfop: str | None = None
|
||||
base_calc_percent: Decimal | None = None
|
||||
aliquota: Decimal | None = None
|
||||
mva: Decimal | None = None
|
||||
aliquota_st: Decimal | None = None
|
||||
fcp_percent: Decimal | None = None
|
||||
codigo_beneficio: str | None = None
|
||||
deleted_at: datetime | None = None
|
||||
|
||||
|
||||
def _rule(**kw) -> FakeTaxRule:
|
||||
defaults = dict(
|
||||
id=uuid.uuid4(), organization_id=_ORG, tax_profile_id=_PROFILE,
|
||||
)
|
||||
return FakeTaxRule(**{**defaults, **kw})
|
||||
|
||||
|
||||
def _op(**kw) -> FiscalOperation:
|
||||
defaults = dict(
|
||||
crt="1", uf_origem="PR", uf_destino="PR",
|
||||
indicador_ie="nao_contribuinte", consumidor_final=True,
|
||||
tipo_operacao="venda",
|
||||
item=FiscalItem(
|
||||
tax_profile_id=_PROFILE, ncm="87089990", cest=None, origem="0",
|
||||
quantity=Decimal("2"), unit_price=Decimal("75.00"),
|
||||
),
|
||||
)
|
||||
return FiscalOperation(**{**defaults, **kw})
|
||||
|
||||
|
||||
_PADRAO = [ # preset "Padrão" da Thiago (CSOSN 102)
|
||||
_rule(tax_domain="icms", crt="1", csosn="102", cfop="5102", aliquota=Decimal("0")),
|
||||
_rule(tax_domain="pis", cst="08", aliquota=Decimal("0")),
|
||||
_rule(tax_domain="cofins", cst="08", aliquota=Decimal("0")),
|
||||
]
|
||||
|
||||
|
||||
def test_padrao_interna_resolves_5102_csosn_102():
|
||||
result = resolve_fiscal(_PADRAO, _op())
|
||||
assert result.cfop == "5102"
|
||||
assert result.csosn == "102"
|
||||
assert result.cst is None
|
||||
assert result.origem == "0" # ecoado do ITEM (Part), nunca de regra
|
||||
icms = next(t for t in result.tributos if t.tax_domain == "icms")
|
||||
assert icms.base_calc == Decimal("150.00")
|
||||
assert icms.valor == Decimal("0.00")
|
||||
assert icms.rule_id == _PADRAO[0].id
|
||||
|
||||
|
||||
def test_padrao_interestadual_transposes_to_6102():
|
||||
result = resolve_fiscal(_PADRAO, _op(uf_destino="SP"))
|
||||
assert result.cfop == "6102"
|
||||
|
||||
|
||||
def test_st_profile_resolves_5405_and_6403():
|
||||
st_rules = [_rule(tax_domain="icmsst", crt="1", csosn="500", cfop="5405")]
|
||||
assert resolve_fiscal(st_rules, _op()).cfop == "5405"
|
||||
assert resolve_fiscal(st_rules, _op(uf_destino="SP")).cfop == "6403" # NÃO 6405
|
||||
|
||||
|
||||
def test_more_selective_ncm_rule_beats_generic_rule_with_more_matchers():
|
||||
# A correção do review: NCM (peso 64) vence 3 matchers genéricos
|
||||
# (crt=1 + uf=4 + indicador_ie=8 = 13).
|
||||
generic = _rule(
|
||||
tax_domain="icms", crt="1", uf_destino_tipo="interna",
|
||||
indicador_ie="nao_contribuinte", csosn="102", cfop="5102",
|
||||
)
|
||||
by_ncm = _rule(tax_domain="icms", ncm_prefix="8708", csosn="500", cfop="5405")
|
||||
result = resolve_fiscal([generic, by_ncm], _op())
|
||||
assert result.csosn == "500"
|
||||
|
||||
|
||||
def test_rule_with_non_matching_matcher_is_skipped():
|
||||
rule_sp = _rule(tax_domain="icms", uf_destino_tipo="interestadual", csosn="102", cfop="5102")
|
||||
with pytest.raises(FiscalConfigError):
|
||||
resolve_fiscal([rule_sp], _op()) # operação interna; a regra não casa
|
||||
|
||||
|
||||
def test_tie_same_weight_raises_ambiguous():
|
||||
a = _rule(tax_domain="icms", crt="1", csosn="102", cfop="5102")
|
||||
b = _rule(tax_domain="icms", crt="1", csosn="101", cfop="5102")
|
||||
with pytest.raises(AmbiguousRuleError) as exc:
|
||||
resolve_fiscal([a, b], _op())
|
||||
assert {a.id, b.id} == set(exc.value.rule_ids)
|
||||
|
||||
|
||||
def test_fail_closed_lists_the_missing_domain():
|
||||
with pytest.raises(FiscalConfigError) as exc:
|
||||
resolve_fiscal([], _op())
|
||||
assert "icms" in " ".join(exc.value.missing)
|
||||
|
||||
|
||||
def test_optional_domain_absent_does_not_block():
|
||||
# Sem regra de FCP/IPI → simplesmente não entra no resultado.
|
||||
result = resolve_fiscal(_PADRAO, _op())
|
||||
assert {t.tax_domain for t in result.tributos} == {"icms", "pis", "cofins"}
|
||||
|
||||
|
||||
def test_st_value_math_with_mva():
|
||||
# base 100; MVA 40% → base_st 140; aliq_st 18% → 25.20 − ICMS próprio 12.00 = 13.20
|
||||
rules = [
|
||||
_rule(tax_domain="icms", cst="00", cfop="5102", aliquota=Decimal("12")),
|
||||
_rule(
|
||||
tax_domain="icmsst", cst="10", cfop="5405",
|
||||
mva=Decimal("40"), aliquota_st=Decimal("18"),
|
||||
),
|
||||
]
|
||||
op = _op(crt="3", item=FiscalItem(
|
||||
tax_profile_id=_PROFILE, ncm="27101259", cest="0600100", origem="0",
|
||||
quantity=Decimal("1"), unit_price=Decimal("100.00"),
|
||||
))
|
||||
result = resolve_fiscal(rules, op)
|
||||
st = next(t for t in result.tributos if t.tax_domain == "icmsst")
|
||||
assert st.valor == Decimal("13.20")
|
||||
|
||||
|
||||
def test_reducao_de_base():
|
||||
# base 100 com redução p/ 60% e alíquota 18% → 10.80
|
||||
rules = [_rule(
|
||||
tax_domain="icms", cst="20", cfop="5102",
|
||||
base_calc_percent=Decimal("60"), aliquota=Decimal("18"),
|
||||
)]
|
||||
result = resolve_fiscal(rules, _op(crt="3", item=FiscalItem(
|
||||
tax_profile_id=_PROFILE, ncm="87089990", cest=None, origem="0",
|
||||
quantity=Decimal("1"), unit_price=Decimal("100.00"),
|
||||
)))
|
||||
icms = next(t for t in result.tributos if t.tax_domain == "icms")
|
||||
assert icms.valor == Decimal("10.80")
|
||||
|
||||
|
||||
def test_anchor_matches_without_cfop_names_the_field():
|
||||
"""M5 (review 2026-07-15): a âncora casa (tem csosn) mas não tem CFOP --
|
||||
`FiscalConfigError.missing` deve nomear o campo faltante, não só o
|
||||
domínio."""
|
||||
rule = _rule(tax_domain="icms", csosn="102", cfop=None)
|
||||
with pytest.raises(FiscalConfigError) as exc:
|
||||
resolve_fiscal([rule], _op())
|
||||
assert "cfop" in " ".join(exc.value.missing)
|
||||
|
||||
|
||||
def test_anchor_matches_without_cst_or_csosn_names_the_field():
|
||||
"""M5 (review 2026-07-15): a âncora casa (tem cfop) mas não tem cst NEM
|
||||
csosn -- `FiscalConfigError.missing` deve nomear cst/csosn."""
|
||||
rule = _rule(tax_domain="icms", cfop="5102", cst=None, csosn=None)
|
||||
with pytest.raises(FiscalConfigError) as exc:
|
||||
resolve_fiscal([rule], _op())
|
||||
assert "cst/csosn" in " ".join(exc.value.missing)
|
||||
|
||||
|
||||
def test_deleted_rule_is_ignored():
|
||||
from datetime import datetime, timezone
|
||||
|
||||
dead = _rule(tax_domain="icms", csosn="102", cfop="5102",
|
||||
deleted_at=datetime.now(timezone.utc))
|
||||
with pytest.raises(FiscalConfigError):
|
||||
resolve_fiscal([dead], _op())
|
||||
|
||||
|
||||
def test_fcp_calculates_from_fcp_percent():
|
||||
"""I1 (auditoria Fable Bloco B): o domínio `fcp` não tem `aliquota` no
|
||||
catálogo (só `fcp_percent`, DOMAIN_FIELDS['fcp']) -- `_linha` genérico
|
||||
só multiplica `aliquota`, então toda linha FCP saía com `valor=0.00`
|
||||
(preview que mente). Prova de detecção: SEM o fix deste teste, `valor`
|
||||
vem 0.00 em vez de 2.00 -- FCP 2% sobre base 100."""
|
||||
rules = [
|
||||
*_PADRAO,
|
||||
_rule(tax_domain="fcp", fcp_percent=Decimal("2")),
|
||||
]
|
||||
op = _op(item=FiscalItem(
|
||||
tax_profile_id=_PROFILE, ncm="87089990", cest=None, origem="0",
|
||||
quantity=Decimal("1"), unit_price=Decimal("100.00"),
|
||||
))
|
||||
result = resolve_fiscal(rules, op)
|
||||
fcp = next(t for t in result.tributos if t.tax_domain == "fcp")
|
||||
assert fcp.valor == Decimal("2.00")
|
||||
assert fcp.fcp_percent == Decimal("2")
|
||||
@@ -0,0 +1,303 @@
|
||||
"""1b.1 Task 5: `xml_builder.build_nfe` -- montagem PURA do XML da NF-e 55
|
||||
(NT 2025.002 v1.40). Todos os testes montam `DadosEmissao` em memória
|
||||
(nenhum banco, nenhum I/O) e inspecionam o objeto `Nfe` resultante
|
||||
diretamente OU sua serialização via `xsdata.formats.dataclass.serializers.
|
||||
XmlSerializer` (o mesmo serializador que `emissao.py`, Task 6, usará antes
|
||||
de assinar)."""
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
from xsdata.formats.dataclass.serializers import XmlSerializer
|
||||
from xsdata.formats.dataclass.serializers.config import SerializerConfig
|
||||
|
||||
from sowai_fiscal.resolver import FiscalResult, TributoLinha
|
||||
from sowai_fiscal.xml_builder import (
|
||||
TEXTO_HOMOLOGACAO,
|
||||
DadosEmissao,
|
||||
DestinatarioData,
|
||||
EmitenteData,
|
||||
ItemData,
|
||||
PagamentoData,
|
||||
build_nfe,
|
||||
)
|
||||
|
||||
_NFE_NAMESPACE = "http://www.portalfiscal.inf.br/nfe"
|
||||
|
||||
|
||||
def _emitente(**overrides) -> EmitenteData:
|
||||
base = dict(
|
||||
cnpj="12345678000190",
|
||||
razao_social="EMPRESA TESTE LTDA",
|
||||
nome_fantasia="Empresa Teste",
|
||||
ie="1234567890",
|
||||
crt="1",
|
||||
address_street="Rua Teste",
|
||||
address_number="100",
|
||||
address_complement=None,
|
||||
address_district="Centro",
|
||||
address_city="Curitiba",
|
||||
address_state="PR",
|
||||
address_zip="80000000",
|
||||
address_city_ibge_code="4106902",
|
||||
)
|
||||
base.update(overrides)
|
||||
return EmitenteData(**base)
|
||||
|
||||
|
||||
def _destinatario(**overrides) -> DestinatarioData:
|
||||
base = dict(
|
||||
nome="CLIENTE TESTE LTDA",
|
||||
cnpj="99887766000155",
|
||||
cpf=None,
|
||||
indicador_ie="9",
|
||||
address_street="Av Cliente",
|
||||
address_number="50",
|
||||
address_district="Bairro",
|
||||
address_city="Curitiba",
|
||||
address_state="PR",
|
||||
address_zip="80000000",
|
||||
address_city_ibge_code="4106902",
|
||||
)
|
||||
base.update(overrides)
|
||||
return DestinatarioData(**base)
|
||||
|
||||
|
||||
def _tributo(tax_domain: str, *, cst=None, csosn=None, aliquota=None, valor="0.00", base_calc="100.00") -> TributoLinha:
|
||||
return TributoLinha(
|
||||
tax_domain=tax_domain,
|
||||
cst=cst,
|
||||
csosn=csosn,
|
||||
base_calc=Decimal(base_calc),
|
||||
base_calc_percent=Decimal("100"),
|
||||
aliquota=Decimal(aliquota) if aliquota is not None else None,
|
||||
valor=Decimal(valor),
|
||||
mva=None,
|
||||
aliquota_st=None,
|
||||
fcp_percent=None,
|
||||
codigo_beneficio=None,
|
||||
rule_id=uuid.uuid4(),
|
||||
)
|
||||
|
||||
|
||||
def _fiscal_result(cfop: str, *, csosn="102", cst=None, tributos=None, origem="0") -> FiscalResult:
|
||||
return FiscalResult(
|
||||
cfop=cfop,
|
||||
cst=cst,
|
||||
csosn=csosn,
|
||||
origem=origem,
|
||||
consumidor_final=True,
|
||||
indicador_ie="9",
|
||||
tributos=tributos if tributos is not None else [
|
||||
_tributo("pis", cst="08"),
|
||||
_tributo("cofins", cst="08"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _item(cfop: str, **overrides) -> ItemData:
|
||||
base = dict(
|
||||
codigo="PC001",
|
||||
descricao="Filtro de oleo",
|
||||
ncm="84212300",
|
||||
cfop=cfop,
|
||||
unidade_comercial="UN",
|
||||
unidade_tributavel="UN",
|
||||
quantidade=Decimal("2"),
|
||||
valor_unitario=Decimal("50.00"),
|
||||
fiscal_result=_fiscal_result(cfop),
|
||||
)
|
||||
base.update(overrides)
|
||||
return ItemData(**base)
|
||||
|
||||
|
||||
def _pagamento(**overrides) -> PagamentoData:
|
||||
base = dict(tpag="01", valor=Decimal("100.00"))
|
||||
base.update(overrides)
|
||||
return PagamentoData(**base)
|
||||
|
||||
|
||||
def _dados(**overrides) -> DadosEmissao:
|
||||
base = dict(
|
||||
emitente=_emitente(),
|
||||
itens=[_item("5102")],
|
||||
pagamento=_pagamento(),
|
||||
ambiente="homologacao",
|
||||
chave_acesso="4" * 44,
|
||||
numero=1014,
|
||||
serie=1,
|
||||
cnf="87654321",
|
||||
dh_emi="2026-07-16T10:00:00-03:00",
|
||||
uf_destino_tipo="interna",
|
||||
destinatario=_destinatario(),
|
||||
)
|
||||
base.update(overrides)
|
||||
return DadosEmissao(**base)
|
||||
|
||||
|
||||
def _render(nfe) -> str:
|
||||
config = SerializerConfig(xml_declaration=False, indent=None)
|
||||
return XmlSerializer(config=config).render(nfe, ns_map={None: _NFE_NAMESPACE})
|
||||
|
||||
|
||||
# --- venda Padrão intra-PR ---------------------------------------------
|
||||
|
||||
|
||||
def test_venda_padrao_intra_estado_cfop_5102_csosn_102_e_origem_do_item():
|
||||
dados = _dados(uf_destino_tipo="interna", itens=[_item("5102", fiscal_result=_fiscal_result("5102", csosn="102", origem="1"))])
|
||||
|
||||
nfe = build_nfe(dados)
|
||||
|
||||
det = nfe.infNFe.det[0]
|
||||
assert det.prod.CFOP == "5102"
|
||||
assert det.imposto.ICMS.ICMSSN102.CSOSN == "102"
|
||||
assert det.imposto.ICMS.ICMSSN102.orig == "1" # origem SEMPRE do item, nunca de regra
|
||||
assert nfe.infNFe.ide.idDest == "1"
|
||||
|
||||
|
||||
def test_totais_batem_com_a_soma_dos_itens_decimal_2_casas():
|
||||
itens = [
|
||||
_item("5102", quantidade=Decimal("2"), valor_unitario=Decimal("50.00")),
|
||||
_item("5102", quantidade=Decimal("3"), valor_unitario=Decimal("10.005")),
|
||||
]
|
||||
dados = _dados(itens=itens)
|
||||
|
||||
nfe = build_nfe(dados)
|
||||
|
||||
# 2*50.00 = 100.00 ; 3*10.005 = 30.015 -> arredonda p/ 30.02 (ROUND_HALF_UP)
|
||||
assert nfe.infNFe.total.ICMSTot.vProd == "130.02"
|
||||
assert nfe.infNFe.total.ICMSTot.vNF == "130.02"
|
||||
# Decimal com 2 casas sempre (nunca "100.0" ou "100")
|
||||
assert nfe.infNFe.total.ICMSTot.vProd.count(".") == 1
|
||||
assert len(nfe.infNFe.total.ICMSTot.vProd.split(".")[1]) == 2
|
||||
|
||||
|
||||
# --- interestadual --------------------------------------------------------
|
||||
|
||||
|
||||
def test_venda_interestadual_cfop_6102_iddest_2():
|
||||
dados = _dados(
|
||||
uf_destino_tipo="interestadual",
|
||||
itens=[_item("6102", fiscal_result=_fiscal_result("6102", csosn="102"))],
|
||||
ind_final="1",
|
||||
destinatario=_destinatario(indicador_ie="1", address_state="SP"),
|
||||
)
|
||||
|
||||
nfe = build_nfe(dados)
|
||||
|
||||
det = nfe.infNFe.det[0]
|
||||
assert det.prod.CFOP == "6102"
|
||||
assert nfe.infNFe.ide.idDest == "2"
|
||||
assert nfe.infNFe.ide.indFinal == "1"
|
||||
assert nfe.infNFe.dest.indIEDest == "1"
|
||||
|
||||
|
||||
# --- pagamento -------------------------------------------------------------
|
||||
|
||||
|
||||
def test_pag_detpag_tpag_do_pagamento():
|
||||
dados = _dados(pagamento=_pagamento(tpag="03", valor=Decimal("55.50")))
|
||||
|
||||
nfe = build_nfe(dados)
|
||||
|
||||
det_pag = nfe.infNFe.pag.detPag[0]
|
||||
assert det_pag.tPag == "03"
|
||||
assert det_pag.vPag == "55.50"
|
||||
|
||||
|
||||
def test_pag_tpag_17_para_pix():
|
||||
dados = _dados(pagamento=_pagamento(tpag="17", valor=Decimal("100.00")))
|
||||
|
||||
nfe = build_nfe(dados)
|
||||
|
||||
assert nfe.infNFe.pag.detPag[0].tPag == "17"
|
||||
|
||||
|
||||
# --- homologação -------------------------------------------------------------
|
||||
|
||||
|
||||
def test_homologacao_forca_xnome_do_destinatario_para_texto_oficial():
|
||||
dados = _dados(ambiente="homologacao", destinatario=_destinatario(nome="Cliente Real Ltda"))
|
||||
|
||||
nfe = build_nfe(dados)
|
||||
|
||||
assert nfe.infNFe.dest.xNome == TEXTO_HOMOLOGACAO
|
||||
assert nfe.infNFe.ide.tpAmb == "2"
|
||||
|
||||
|
||||
def test_producao_mantem_xnome_real_do_destinatario():
|
||||
dados = _dados(ambiente="producao", destinatario=_destinatario(nome="Cliente Real Ltda"))
|
||||
|
||||
nfe = build_nfe(dados)
|
||||
|
||||
assert nfe.infNFe.dest.xNome == "Cliente Real Ltda"
|
||||
assert nfe.infNFe.ide.tpAmb == "1"
|
||||
|
||||
|
||||
# --- grupo UB (IBS/CBS) -----------------------------------------------------
|
||||
|
||||
|
||||
def test_grupo_ibscbs_ausente_sem_linhas_ibs_cbs_crt1_2026():
|
||||
dados = _dados(itens=[_item("5102", fiscal_result=_fiscal_result("5102"))])
|
||||
|
||||
nfe = build_nfe(dados)
|
||||
|
||||
assert nfe.infNFe.det[0].imposto.IBSCBS is None
|
||||
|
||||
|
||||
def test_grupo_ibscbs_presente_com_linhas_ibs_cbs():
|
||||
fr = _fiscal_result(
|
||||
"5102",
|
||||
tributos=[
|
||||
_tributo("pis", cst="08"),
|
||||
_tributo("cofins", cst="08"),
|
||||
_tributo("ibs", aliquota="0.1000", valor="10.00", base_calc="100.00"),
|
||||
_tributo("cbs", aliquota="0.9000", valor="90.00", base_calc="100.00"),
|
||||
],
|
||||
)
|
||||
dados = _dados(itens=[_item("5102", fiscal_result=fr)])
|
||||
|
||||
nfe = build_nfe(dados)
|
||||
|
||||
ibscbs = nfe.infNFe.det[0].imposto.IBSCBS
|
||||
assert ibscbs is not None
|
||||
assert ibscbs.gIBSCBS.gIBSUF.vIBSUF == "10.00"
|
||||
assert ibscbs.gIBSCBS.gCBS.vCBS == "90.00"
|
||||
|
||||
|
||||
# --- serialização -----------------------------------------------------------
|
||||
|
||||
|
||||
def test_serializacao_gera_xml_bem_formado_com_namespace_correto():
|
||||
dados = _dados()
|
||||
|
||||
nfe = build_nfe(dados)
|
||||
xml = _render(nfe)
|
||||
|
||||
assert xml.startswith(f'<NFe xmlns="{_NFE_NAMESPACE}">')
|
||||
assert f'Id="NFe{dados.chave_acesso}"' in xml
|
||||
assert "<infNFe" in xml and "</infNFe>" in xml
|
||||
# bem-formado: um parser XML real não estoura
|
||||
from lxml import etree
|
||||
|
||||
etree.fromstring(xml.encode("utf-8"))
|
||||
|
||||
|
||||
def test_unsupported_icms_situation_raises_clear_error():
|
||||
from sowai_fiscal.xml_builder import UnsupportedIcmsSituationError
|
||||
|
||||
dados = _dados(itens=[_item("5102", fiscal_result=_fiscal_result("5102", csosn="201"))])
|
||||
|
||||
with pytest.raises(UnsupportedIcmsSituationError):
|
||||
build_nfe(dados)
|
||||
|
||||
|
||||
def test_origem_none_levanta_valueerror_sem_fallback_silencioso():
|
||||
"""I3 (review Opus, 2026-07-16): origem None chegando no builder é erro
|
||||
de PROGRAMAÇÃO (a validação de completude da emissão barra peça sem
|
||||
`icms_origem` com 409 antes) -- jamais um default '0' silencioso, que
|
||||
emitiria origem nacional para peça importada (imposto errado)."""
|
||||
dados = _dados(itens=[_item("5102", fiscal_result=_fiscal_result("5102", origem=None))])
|
||||
|
||||
with pytest.raises(ValueError, match="origem"):
|
||||
build_nfe(dados)
|
||||
@@ -0,0 +1,382 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "6.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/b0/83f481780d1548750b8ce2ec824073deef2f452d9cd1a6faff8507e3d16d/lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2", size = 8526461, upload-time = "2026-05-18T19:17:25.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/d5/30fa0f808002c7329397bfbb24e306789c0b29f04aa5842c07b174b4216f/lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d", size = 4595375, upload-time = "2026-05-18T19:17:34.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/d2/edb71cf0e561581a7c5eb2626244320eb04e9f8ce6d563184fd668b45073/lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510", size = 4923654, upload-time = "2026-05-18T19:17:42.917Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/77/1bc7eeb0de4577d783fb625aa092cc9357883bba35845a3666bf1259f3dc/lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a", size = 5067921, upload-time = "2026-05-18T19:17:49.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/3c/c0690d74bd2bc17bc03b5b0d093569ead597dd0bfa088bf99eef8c24e19c/lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d", size = 5002456, upload-time = "2026-05-18T19:17:59.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/8d/d1b3271af0c0f1e27e8472a849e4d2c65bc7766884b9ad2da9e76e145c88/lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8", size = 5202776, upload-time = "2026-05-18T19:18:08.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/45/689824ffb237fd10125ad273f32b28ff04dc6203c2822c85ff65a93df65e/lxml-6.1.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009", size = 5329945, upload-time = "2026-05-18T19:18:13.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/c0/ef73af53767e958fd87d437c170f272e2f6e6c0f854939f133a895f1e711/lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6", size = 4659237, upload-time = "2026-05-18T19:18:18.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/5e/e1158e40397585e91cb0472374a1f63d0926a1ddeaa92f13d1a1ffe306d5/lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8", size = 5265904, upload-time = "2026-05-18T19:18:24.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/16/8687e5d1400ed1c0bc41dace232ebb7553952b618ea1f2e5fb6e2cfbbe23/lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83", size = 5045225, upload-time = "2026-05-18T19:17:20.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/18/d877bd1ae2e5ffdfd4836565aba350db31feb2f2656d6ce70316ed66a05e/lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6", size = 4712721, upload-time = "2026-05-18T19:17:40.512Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/4d/1f44fd1d770b10dacbf6b5c6e520f4d6e0708744930f719dc04e67cab981/lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c", size = 5252549, upload-time = "2026-05-18T19:17:51.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/5d/1d66b84f850089254c230ef6ea6b267a5a54e2e179a5d960036a05d501d7/lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08", size = 5226877, upload-time = "2026-05-18T19:18:00.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/00/84c4b5302d42a2d0184f38d538c8a197f33b52a50bd4f7bcfe990bce3036/lxml-6.1.1-cp311-cp311-win32.whl", hash = "sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621", size = 3594072, upload-time = "2026-05-18T19:17:12.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/9d/2e2f7d876349f45e0f3e29f72da311668853d59b58d473a2dea4f0160135/lxml-6.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28", size = 4025469, upload-time = "2026-05-18T19:17:50.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/d5/570e6390e4110331e6208b2ba83d1482cc9146808ee118b22824a34c1070/lxml-6.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b", size = 3667640, upload-time = "2026-05-19T19:22:48.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821, upload-time = "2026-05-18T19:17:42.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/00/ff3009c88e65de8011630acf8ab5a09cb2becd2aaf47fba2f3449f6224e9/lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1", size = 4624252, upload-time = "2026-05-18T19:17:47.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746, upload-time = "2026-05-18T19:18:29.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723, upload-time = "2026-05-18T19:18:34.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557, upload-time = "2026-05-18T19:18:39.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036, upload-time = "2026-05-18T19:18:44.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367, upload-time = "2026-05-18T19:18:49.217Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171, upload-time = "2026-05-18T19:18:52.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874, upload-time = "2026-05-18T19:18:55.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492, upload-time = "2026-05-18T19:19:01.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232, upload-time = "2026-05-18T19:18:12.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023, upload-time = "2026-05-18T19:18:15.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773, upload-time = "2026-05-18T19:18:23.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088, upload-time = "2026-05-18T19:18:31.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382, upload-time = "2026-05-18T19:17:18.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255, upload-time = "2026-05-18T19:17:56.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610, upload-time = "2026-05-19T19:22:50.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/32/86a3f0f724a3a402d4627937a7fc27b160e45e7012b4adf47f6e1e844511/lxml-6.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e", size = 3930127, upload-time = "2026-05-18T19:19:02.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/44/d832e82af08723761556d004b1d04d281c09f9a8cecd7d3148548c9941a3/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004", size = 4210769, upload-time = "2026-05-18T19:20:41.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/39/0dc5949f759ed7d951e0bb8c2f2d9d7aca1908d22352fa84a8afd2ea54af/lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e", size = 4318163, upload-time = "2026-05-18T19:20:44.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/fb/8ab3845fe046ba4cbf74536bcf6801a774b7caf4350de1c5d37f1f0a9e90/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2", size = 4250945, upload-time = "2026-05-18T19:20:47.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/1b/7553ab136894374ffae8851ec06f98f511cd8e66246e41b6be059d0a7289/lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf", size = 4401664, upload-time = "2026-05-18T19:20:50.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/a4/441aee36c6f6b249823d20fd91f9be9ab89d7c5a8ae542a4a4ca6d342d56/lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84", size = 3508989, upload-time = "2026-05-18T19:18:38.158Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nfelib"
|
||||
version = "2.5.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "lxml" },
|
||||
{ name = "xsdata" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/77/803771579d7167523dc111972687f11eeb3d7eede661646bd4211819c4d4/nfelib-2.5.2.tar.gz", hash = "sha256:4266adae49f7412900359a215b436aea49c561997298119676f4ee616379d3c3", size = 605114, upload-time = "2026-03-30T21:19:25.661Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/7d/dfaca94035969d820e9db8cf60bb3dd73c5a548c8725b65432fdbc3616bf/nfelib-2.5.2-py3-none-any.whl", hash = "sha256:88f1823d5e41cb3cb244167f846da9866365a3a7fd8dd6506740de19acdef477", size = 937849, upload-time = "2026-03-30T21:19:24.096Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.13.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.46.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sowai-fiscal"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "lxml" },
|
||||
{ name = "nfelib" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "xsdata" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "lxml", specifier = ">=5.2" },
|
||||
{ name = "nfelib", specifier = ">=2.5.2" },
|
||||
{ name = "pydantic", specifier = ">=2.7" },
|
||||
{ name = "xsdata", specifier = ">=24.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "pytest", specifier = ">=8" },
|
||||
{ name = "pytest-asyncio", specifier = ">=0.24" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xsdata"
|
||||
version = "26.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/71e9e8eac669091fd434ed494d806c8cc37614aecb34ce4c62c283f99abf/xsdata-26.2.tar.gz", hash = "sha256:c631af71aaa75734f8ce92a08fcf8389d905dee2aab0b5032c9032e9071009a6", size = 349690, upload-time = "2026-02-15T16:13:31.274Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/92/f0edcbc2f895ecea14a68e492b24c157625e251279a94b172a6b263290e7/xsdata-26.2-py3-none-any.whl", hash = "sha256:85a591a4405d903416afbd4a917e8dda8ea44641a3e66d72134bc2a31b3c16b0", size = 235561, upload-time = "2026-02-15T16:13:29.614Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user