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())