diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index 58022493..00000000
--- a/.coveragerc
+++ /dev/null
@@ -1,2 +0,0 @@
-[run]
-omit = nfelib/v4_00/*
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 00000000..40b2d856
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,41 @@
+name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI
+
+on:
+ release:
+ types: [released]
+
+jobs:
+ build-n-publish:
+ name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
+ runs-on: ubuntu-latest
+ environment:
+ name: release
+ url: https://pypi.org/p/nfelib
+ permissions:
+ id-token: write
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python 3.9
+ uses: actions/setup-python@v5
+ with:
+ python-version: 3.9
+ - name: Install pypa/build
+ run: >-
+ python -m
+ pip install
+ build
+ --user
+ - name: Build a binary wheel and a source tarball
+ run: >-
+ python -m
+ build
+ --sdist
+ --wheel
+ --outdir dist/
+ .
+ - name: Publish distribution 📦 to Test PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ repository-url: https://test.pypi.org/legacy/
+ - name: Publish distribution 📦 to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 9e89447c..4f7af5fd 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -22,9 +22,9 @@ jobs:
- {name: Python 3.11, python: '3.11', os: ubuntu}
- {name: Python 3.12, python: '3.12', os: ubuntu}
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install libxml2
@@ -33,9 +33,13 @@ jobs:
sudo apt-get install libxml2-dev libxslt-dev
- name: Install dependencies
run: |
- python -m pip install --upgrade pip setuptools tox
+ python -m pip install --upgrade pip setuptools pytest pytest-xdist xmlschema pytest-cov
+ python -m pip install .[test]
- name: Test
run: |
- tox -e py -v --color=yes
- - name: Upload coverage reports to Codecov
- uses: codecov/codecov-action@v3
+ pytest --cov=./nfelib --cov-report=xml --cov-branch --doctest-glob="docs/*.md"
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ fail_ci_if_error: true
diff --git a/.gitignore b/.gitignore
index dd142967..9ef94acb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -69,4 +69,4 @@ docs/_build/
# Backup files
*~
-*.swp
\ No newline at end of file
+*.swp
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2487ed48..2c9978a7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,54 +1,38 @@
exclude: |
(?x)
- setup.cfg|
- ^tests|
-repos:
- - repo: https://github.com/asottile/pyupgrade
- rev: v2.10.1
- hooks:
- - id: pyupgrade
- args: [--py37-plus]
- - repo: https://github.com/asottile/reorder_python_imports
- rev: v2.4.0
- hooks:
- - id: reorder-python-imports
- - repo: https://github.com/ambv/black
- rev: 22.3.0
- hooks:
- - id: black
- - repo: https://gitlab.com/pycqa/flake8
- rev: 3.9.0
- hooks:
- - id: flake8
- additional_dependencies: [
- flake8-bugbear,
- flake8-annotations,
- flake8-comprehensions,
- ]
- args: ["--suppress-none-returning"]
+ ^README\.md$
+repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v3.4.0
+ rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: debug-statements
- - repo: git@github.com:humitos/mirrors-docformatter.git
- rev: v1.0
- hooks:
- - id: docformatter
- args: ["--in-place", "--pre-summary-newline"]
+ - repo: https://github.com/crate-ci/typos
+ rev: v1.22.9
+ hooks:
+ - id: typos
+ exclude: ^tests/|.xsd$
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.5.0
+ hooks:
+ - id: ruff
+ args: [ --fix, --show-fixes]
+ - id: ruff-format
+ exclude: ^nfelib/[^/]+\.py$
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v0.812
+ rev: v1.10.1
hooks:
- id: mypy
- additional_dependencies: [tokenize-rt]
- - repo: https://github.com/asottile/setup-cfg-fmt
- rev: v1.17.0
- hooks:
- - id: setup-cfg-fmt
- args: ["--max-py-version=3.9"]
- - repo: https://github.com/PyCQA/doc8
- rev: 0.9.0a1
- hooks:
- - id: doc8
+ files: ^(nfelib/)
+ args: [ "--check-untyped-defs", "--ignore-missing-imports" ]
+ - repo: https://github.com/pre-commit/mirrors-prettier
+ rev: v4.0.0-alpha.8
+ hooks:
+ - id: prettier
+ additional_dependencies:
+ - prettier@3.2.5
+ types_or: [markdown]
+ args: [--prose-wrap=always, --print-width=88]
+ exclude: ^nfelib/[^/]+\.xml$
diff --git a/.xsdata.xml b/.xsdata.xml
index 214aa5b8..657f8fff 100644
--- a/.xsdata.xml
+++ b/.xsdata.xml
@@ -30,11 +30,11 @@
-
+
-
+
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..8d1dd0bb
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,133 @@
+[build-system]
+requires = ["setuptools>=61.2"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools.packages.find]
+include = ["nfelib*"]
+
+[project]
+name = "nfelib"
+description = "nfelib: electronic invoicing library for Brazil"
+authors = [{name = "Raphaël Valyi", email = "raphael.valyi@akretion.com.br"}]
+license = {text = "MIT"}
+classifiers = [
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+]
+keywords = ["e-invoicing", "ERP", "Odoo", "NFe", "CTe", "MDFe", "BPe", "NFSe"]
+requires-python = ">=3.8"
+dependencies = [
+ "xsdata",
+]
+dynamic = ["version"]
+
+[project.readme]
+file = "README.md"
+content-type = "text/markdown"
+
+[project.urls]
+Homepage = "https://github.com/akretion/nfelib"
+Source = "https://github.com/akretion/nfelib"
+Documentation = "https://nfelib.readthedocs.io/"
+Changelog = "https://nfelib.readthedocs.io/en/latest/changelog/"
+
+[project.optional-dependencies]
+sign = [
+ "erpbrasil.assinatura",
+]
+pdf = [
+ "brazilfiscalreport",
+]
+test = [
+ "pre-commit",
+ "pytest",
+ "pytest-benchmark",
+ "pytest-cov",
+ "xmldiff",
+ "requests",
+ "beautifulsoup4",
+ "erpbrasil.assinatura",
+ "brazilfiscalreport",
+]
+
+[tool.setuptools]
+include-package-data = true
+license-files = ["MIT-LICENSE"]
+
+[tool.setuptools.dynamic]
+version = {attr = "nfelib.__version__"}
+
+[tool.ruff]
+target-version = "py38"
+
+[tool.ruff.lint]
+select = [
+ # pycodestyle
+ "E",
+ # Pyflakes
+ "F",
+ # pyupgrade
+ "UP",
+ # flake8-bugbear
+ "B",
+ # flake8-simplify
+ "SIM",
+ # flake8-annotations
+ "ANN",
+ # flake8-comprehensions
+ "C4",
+ # isort
+ "I",
+ # pydocstyle
+ "D",
+ # flake-raise
+ "RSE",
+ # flake-return
+ "RET",
+ # Ruff rules
+ "RUF",
+]
+
+ignore = [
+ "ANN101",
+ "ANN102",
+ "ANN201",
+ "ANN202",
+ "ANN204",
+ "ANN206",
+ "ANN401",
+ "E203",
+ "B028",
+ "B904",
+ "D100",
+ "D104",
+ "D107",
+ "RUF009",
+ "RUF012",
+]
+
+[tool.ruff.lint.per-file-ignores]
+"**/{tests}/*" = ["ANN001", "ANN002", "ANN003", "E501", "B018", "D"]
+"**/utils/testing.py" = ["D"]
+"docs/*" = ["D"]
+
+[tool.ruff.lint.pydocstyle]
+convention = "google"
+
+[tool.ruff.format]
+line-ending = "lf"
+
+[tool.pytest.ini_options]
+addopts = "--color=yes"
+doctest_optionflags = "NORMALIZE_WHITESPACE"
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 60684932..00000000
--- a/setup.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from setuptools import setup
-
-setup()
diff --git a/tests/cte/test_cte.py b/tests/cte/test_cte.py
index 54963906..f6789686 100644
--- a/tests/cte/test_cte.py
+++ b/tests/cte/test_cte.py
@@ -2,32 +2,33 @@
import os
from xmldiff import main
+from unittest import TestCase
from xsdata.formats.dataclass.serializers.config import SerializerConfig
from xsdata.formats.dataclass.parsers import XmlParser
from xsdata.formats.dataclass.serializers import XmlSerializer
from pathlib import Path
-from nfelib.cte.bindings import v4_0
+from nfelib.cte.bindings import v4_0 # noqa: F401
-def test_in_out_cte():
- path = os.path.join("nfelib", "cte", "samples", "v4_0")
- for filename in os.listdir(path):
- input_file = os.path.join(path, filename)
- parser = XmlParser()
- obj = parser.from_path(Path(input_file))
- serializer = XmlSerializer(config=SerializerConfig(pretty_print=True))
- xml = serializer.render(
- obj=obj,
- ns_map={None: "http://www.portalfiscal.inf.br/cte"}
- )
+class CTeTests(TestCase):
+ def test_in_out_cte(self):
+ path = os.path.join("nfelib", "cte", "samples", "v4_0")
+ for filename in os.listdir(path):
+ input_file = os.path.join(path, filename)
+ parser = XmlParser()
+ obj = parser.from_path(Path(input_file))
+ serializer = XmlSerializer(config=SerializerConfig(pretty_print=True))
+ xml = serializer.render(
+ obj=obj, ns_map={None: "http://www.portalfiscal.inf.br/cte"}
+ )
- output_file = "tests/output_cte.xml"
- with open(output_file, "w") as f:
- f.write(xml)
+ output_file = "tests/output_cte.xml"
+ with open(output_file, "w") as f:
+ f.write(xml)
- diff = main.diff_files(input_file, output_file)
- assert len(diff) == 0
- if len(diff) != 0:
- break
+ diff = main.diff_files(input_file, output_file)
+ self.assertEqual(len(diff), 0)
+ if len(diff) != 0:
+ break
diff --git a/tests/mdfe/test_mdfe.py b/tests/mdfe/test_mdfe.py
index 88b1f82d..08a3c926 100644
--- a/tests/mdfe/test_mdfe.py
+++ b/tests/mdfe/test_mdfe.py
@@ -2,32 +2,33 @@
import os
from xmldiff import main
+from unittest import TestCase
from xsdata.formats.dataclass.serializers.config import SerializerConfig
from xsdata.formats.dataclass.parsers import XmlParser
from xsdata.formats.dataclass.serializers import XmlSerializer
from pathlib import Path
-from nfelib.mdfe.bindings import v3_0
+from nfelib.mdfe.bindings import v3_0 # noqa: F401
-def test_in_out_mdfe():
- path = os.path.join("nfelib", "mdfe", "samples", "v3_0")
- for filename in os.listdir(path):
- input_file = os.path.join(path, filename)
- parser = XmlParser()
- obj = parser.from_path(Path(input_file))
- serializer = XmlSerializer(config=SerializerConfig(pretty_print=True))
- xml = serializer.render(
- obj=obj,
- ns_map={None: "http://www.portalfiscal.inf.br/mdfe"}
- )
+class MDFeTests(TestCase):
+ def test_in_out_mdfe(self):
+ path = os.path.join("nfelib", "mdfe", "samples", "v3_0")
+ for filename in os.listdir(path):
+ input_file = os.path.join(path, filename)
+ parser = XmlParser()
+ obj = parser.from_path(Path(input_file))
+ serializer = XmlSerializer(config=SerializerConfig(pretty_print=True))
+ xml = serializer.render(
+ obj=obj, ns_map={None: "http://www.portalfiscal.inf.br/mdfe"}
+ )
- output_file = "tests/output_mdfe.xml"
- with open(output_file, "w") as f:
- f.write(xml)
+ output_file = "tests/output_mdfe.xml"
+ with open(output_file, "w") as f:
+ f.write(xml)
- diff = main.diff_files(input_file, output_file)
- assert len(diff) == 0
- if len(diff) != 0:
- break
+ diff = main.diff_files(input_file, output_file)
+ self.assertEqual(len(diff), 0)
+ if len(diff) != 0:
+ break
diff --git a/tests/nfe_evento_cce/test_cce.py b/tests/nfe_evento_cce/test_cce.py
index 811e7b1b..8f7c2def 100644
--- a/tests/nfe_evento_cce/test_cce.py
+++ b/tests/nfe_evento_cce/test_cce.py
@@ -2,6 +2,7 @@
import os
from xmldiff import main
+from unittest import TestCase
from xsdata.formats.dataclass.serializers.config import SerializerConfig
from xsdata.formats.dataclass.parsers import XmlParser
@@ -11,22 +12,23 @@
from nfelib.nfe_evento_cce.bindings.v1_0.cce_v1_00 import Evento
-def test_in_out_leiauteCCe():
- path = os.path.join("nfelib", "nfe_evento_cce", "samples", "v1_0")
- for filename in os.listdir(path):
- input_file = os.path.join(path, filename)
- parser = XmlParser()
- obj = parser.from_path(Path(input_file), Evento)
- serializer = XmlSerializer(config=SerializerConfig(pretty_print=True))
- xml = serializer.render(
- obj=obj, ns_map={None: "http://www.portalfiscal.inf.br/nfe"}
- )
+class CCeTests(TestCase):
+ def test_in_out_leiauteCCe(self):
+ path = os.path.join("nfelib", "nfe_evento_cce", "samples", "v1_0")
+ for filename in os.listdir(path):
+ input_file = os.path.join(path, filename)
+ parser = XmlParser()
+ obj = parser.from_path(Path(input_file), Evento)
+ serializer = XmlSerializer(config=SerializerConfig(pretty_print=True))
+ xml = serializer.render(
+ obj=obj, ns_map={None: "http://www.portalfiscal.inf.br/nfe"}
+ )
- output_file = "tests/output_nfe_evento_cce.xml"
- with open(output_file, "w") as f:
- f.write(xml)
+ output_file = "tests/output_nfe_evento_cce.xml"
+ with open(output_file, "w") as f:
+ f.write(xml)
- diff = main.diff_files(input_file, output_file)
- assert len(diff) == 0
- if len(diff) != 0:
- break
+ diff = main.diff_files(input_file, output_file)
+ self.assertEqual(len(diff), 0)
+ if len(diff) != 0:
+ break
diff --git a/tests/nfse/test_nfse.py b/tests/nfse/test_nfse.py
index 0d8831e3..f08159fc 100644
--- a/tests/nfse/test_nfse.py
+++ b/tests/nfse/test_nfse.py
@@ -9,8 +9,8 @@
from xsdata.formats.dataclass.serializers import XmlSerializer
from pathlib import Path
-from nfelib.nfse.bindings.v1_0 import nfse_v1_00
-from nfelib.nfse.bindings.v1_0 import dps_v1_00
+from nfelib.nfse.bindings.v1_0 import nfse_v1_00 # noqa: F401
+from nfelib.nfse.bindings.v1_0 import dps_v1_00 # noqa: F401
class NFseTests(TestCase):
diff --git a/tests/test_all.py b/tests/test_all.py
index f14e1433..e22ae786 100644
--- a/tests/test_all.py
+++ b/tests/test_all.py
@@ -1,16 +1,9 @@
# Copyright (C) 2019 - TODAY Raphaël Valyi - Akretion
-import os
-import sys
import importlib
import inspect
from enum import EnumMeta
-from xmldiff import main
-from xsdata.formats.dataclass.serializers.config import SerializerConfig
-from xsdata.formats.dataclass.parsers import XmlParser
-from xsdata.formats.dataclass.serializers import XmlSerializer
-from pathlib import Path
import pkgutil
import logging
diff --git a/tests/test_fingerprint.py b/tests/test_fingerprint.py
index 84a2031f..03aff009 100755
--- a/tests/test_fingerprint.py
+++ b/tests/test_fingerprint.py
@@ -1,7 +1,9 @@
+from unittest import TestCase
+
import hashlib
import json
import logging
-from os import environ, path
+from os import environ
from pathlib import Path
import requests
@@ -39,33 +41,36 @@
}
-def test_fingerprint():
- if environ.get("SKIP_FINGERPRINT"):
- _logger.info("Skipping fingerprint test")
- return True
- fingerprint = {}
- for code, scrap_params in PAGES.items():
- url = scrap_params[0]
- md5 = "ELEMENT NOT FOUND"
- _logger.info("Fetching %s ..." % (url,))
- if len(scrap_params) > 1:
- page = requests.get(url, headers=HEADERS)
- soup = BeautifulSoup(page.text, "html.parser")
- if scrap_params[2] == "id" and soup.find(
+class FingerPrintTests(TestCase):
+ def test_fingerprint(self):
+ if environ.get("SKIP_FINGERPRINT"):
+ _logger.info("Skipping fingerprint test")
+ return True
+ fingerprint = {}
+ for code, scrap_params in PAGES.items():
+ url = scrap_params[0]
+ md5 = "ELEMENT NOT FOUND"
+ _logger.info("Fetching %s ..." % (url,))
+ if len(scrap_params) > 1:
+ page = requests.get(url, headers=HEADERS)
+ soup = BeautifulSoup(page.text, "html.parser")
+ if scrap_params[2] == "id" and soup.find(
scrap_params[1], {"id": scrap_params[3]}
):
- fragment = soup.find(
- scrap_params[1], {"id": scrap_params[3]}
- ).text.encode("utf-8")
+ fragment = soup.find(
+ scrap_params[1], {"id": scrap_params[3]}
+ ).text.encode("utf-8")
+ md5 = hashlib.md5(fragment).hexdigest()
+ else:
+ fragment = requests.get(
+ url, headers=HEADERS
+ ).content # .decode('utf-8')
md5 = hashlib.md5(fragment).hexdigest()
- else:
- fragment = requests.get(url, headers=HEADERS).content # .decode('utf-8')
- md5 = hashlib.md5(fragment).hexdigest()
- fingerprint[code] = (url, md5)
+ fingerprint[code] = (url, md5)
- _logger.info(fingerprint)
- json_string = json.dumps(fingerprint, indent=4)
- target = Path("tests/fingerprint.txt").read_text()
- with open("tests/fingerprint.txt", "w") as outfile:
- outfile.write(json_string)
- assert target.strip() == json_string.strip()
+ _logger.info(fingerprint)
+ json_string = json.dumps(fingerprint, indent=4)
+ target = Path("tests/fingerprint.txt").read_text()
+ with open("tests/fingerprint.txt", "w") as outfile:
+ outfile.write(json_string)
+ self.assertEqual(target.strip(), json_string.strip())