From 0b51483f9cb9dad7683b6eebee6553ec061907de Mon Sep 17 00:00:00 2001 From: Luis Iturrios Date: Sun, 7 Jun 2026 17:18:55 -0600 Subject: [PATCH 1/6] ci: automate semantic release workflows --- .github/workflows/codeql-analysis.yml | 8 +- .github/workflows/continuous_deployment.yml | 59 -- .github/workflows/continuous_integration.yml | 58 +- .github/workflows/dependency-review.yml | 4 +- .github/workflows/greetings.yml | 16 - .github/workflows/prerelease.yml | 68 +++ .github/workflows/release.yml | 64 ++ .github/workflows/stale.yml | 28 - .github/workflows/test.yml | 55 ++ MANIFEST.in | 1 - Pipfile | 20 - Pipfile.lock | 612 ------------------- cfdiclient/solicitadescargaEmitidos.py | 2 +- cfdiclient/solicitadescargaRecibidos.py | 2 +- ejemplo_completo.py | 2 +- pyproject.toml | 88 +++ setup.cfg | 2 - setup.py | 32 - 18 files changed, 293 insertions(+), 828 deletions(-) delete mode 100644 .github/workflows/continuous_deployment.yml delete mode 100644 .github/workflows/greetings.yml create mode 100644 .github/workflows/prerelease.yml create mode 100644 .github/workflows/release.yml delete mode 100644 .github/workflows/stale.yml create mode 100644 .github/workflows/test.yml delete mode 100644 MANIFEST.in delete mode 100644 Pipfile delete mode 100644 Pipfile.lock create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b1f316a..f510a3f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,11 +38,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -53,7 +53,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) # - name: Autobuild - # uses: github/codeql-action/autobuild@v2 + # uses: github/codeql-action/autobuild@v4 # 鈩癸笍 Command-line programs to run using the OS shell. # 馃摎 https://git.io/JvXDl @@ -67,4 +67,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/continuous_deployment.yml b/.github/workflows/continuous_deployment.yml deleted file mode 100644 index cd32c1f..0000000 --- a/.github/workflows/continuous_deployment.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Continuous Deployment - -on: - push: - # Sequence of patterns matched against refs/tags - tags: - - v[0-9]+.[0-9]+.* # add .* to allow dev releases -permissions: - id-token: write - packages: write - contents: write -jobs: - deploy: - name: pipenv PyPI Upload - runs-on: ubuntu-latest - env: - CI: "1" - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - draft: false - prerelease: false - - - name: Set up Python 3.9 - uses: actions/setup-python@v5 - with: - python-version: 3.9 - - - name: Install dependencies - run: | - python -m pip install --upgrade --upgrade-strategy=eager pip pipenv - python -m pip install . - python -m pipenv install --dev - env: - PIPENV_DEFAULT_PYTHON_VERSION: "3.9" - - - name: Build wheels - run: | - python -m pipenv run python setup.py sdist bdist_wheel - # to upload to test pypi, pass repository_url: https://test.pypi.org/legacy/ and use secrets.TEST_PYPI_TOKEN - - name: Publish a Python distribution to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - packages-dir: dist/ - - - name: Push changes - uses: ad-m/github-push-action@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 420a49f..00736f0 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -26,56 +26,16 @@ on: - ".editorconfig" jobs: build: - name: ${{matrix.os}} / ${{ matrix.python-version }} - runs-on: ${{ matrix.os }}-latest + name: ${{ matrix.runner }} / Python ${{ matrix.python-version }} strategy: fail-fast: false matrix: - # python-version: [2.7, 3.6, 3.7, 3.8, 3.9] - # os: [MacOS, Ubuntu, Windows] - # architecture: [x64, x86] - python-version: [3.7, 3.8, 3.9] - os: [MacOS, Ubuntu, Windows] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + runner: [macos-latest, ubuntu-latest, windows-latest] architecture: [x64] - steps: - - uses: actions/checkout@v1 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - architecture: ${{ matrix.architecture }} - - - name: Get python path - id: python-path - run: | - echo ::set-output name=path::$(python -c "import sys; print(sys.executable)") - - - name: Install latest pip - run: | - python -m pip install --upgrade pip pipenv --upgrade-strategy=eager - - - name: Install - env: - PIPENV_DEFAULT_PYTHON_VERSION: ${{ matrix.python-version }} - PYTHONWARNINGS: ignore:DEPRECATION - PYTHONIOENCODING: "utf-8" - GIT_ASK_YESNO: "false" - run: | - git submodule sync - git submodule update --init --recursive - python -m pip install -e . --upgrade - pipenv install --deploy --dev --python=${{ steps.python-path.outputs.path }} - - - name: Run tests - env: - PIPENV_DEFAULT_PYTHON_VERSION: ${{ matrix.python-version }} - PYTHONWARNINGS: ignore:DEPRECATION - PIPENV_NOSPIN: "1" - CI: "1" - GIT_ASK_YESNO: "false" - PYPI_VENDOR_DIR: "./tests/pypi/" - PYTHONIOENCODING: "utf-8" - GIT_SSH_COMMAND: ssh -o StrictHostKeyChecking=accept-new -o CheckHostIP=no - run: | - pipenv run pytest + uses: ./.github/workflows/test.yml + with: + python-version: ${{ matrix.python-version }} + runner: ${{ matrix.runner }} + architecture: ${{ matrix.architecture }} + build-distribution: ${{ matrix.runner == 'ubuntu-latest' && matrix.python-version == '3.13' }} diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 0e72a00..1fd74e4 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -15,6 +15,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: 'Dependency Review' - uses: actions/dependency-review-action@v1 + uses: actions/dependency-review-action@v5 diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml deleted file mode 100644 index 5dc69cd..0000000 --- a/.github/workflows/greetings.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Greetings - -on: [pull_request, issues] - -jobs: - greeting: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - steps: - - uses: actions/first-interaction@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: 'Bienvenido nuestro proyecto python-cfdiclient' - pr-message: 'Gracias por colaborar en python-cfdiclient' diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 0000000..af54cdd --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -0,0 +1,68 @@ +name: Prerelease + +on: + pull_request: + branches: + - master + types: + - opened + - synchronize + - reopened + +permissions: + contents: read + +jobs: + test: + name: Test + uses: ./.github/workflows/test.yml + with: + python-version: "3.13" + runner: ubuntu-latest + build-distribution: true + + prerelease: + name: Prerelease to PyPI + if: github.event.pull_request.head.repo.full_name == github.repository + needs: test + runs-on: ubuntu-latest + environment: pypi-prerelease + concurrency: + group: ${{ github.workflow }}-pr-${{ github.event.pull_request.number }} + cancel-in-progress: false + permissions: + contents: write + id-token: write + pull-requests: read + env: + CI: "1" + + steps: + - name: Checkout pull request branch + uses: actions/checkout@v6 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 + fetch-tags: true + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Create prerelease tag and distributions + id: prerelease + uses: python-semantic-release/python-semantic-release@v10.5.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + prerelease: "true" + prerelease_token: "dev" + commit: "false" + tag: "true" + push: "true" + vcs_release: "false" + changelog: "false" + + - name: Publish prerelease distributions to PyPI + if: steps.prerelease.outputs.released == 'true' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ + print-hash: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4b9719d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,64 @@ +name: Release + +on: + push: + branches: + - master + +permissions: + contents: read + +jobs: + release: + name: Release + runs-on: ubuntu-latest + environment: pypi-release + concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: false + permissions: + contents: write + id-token: write + env: + CI: "1" + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Set up Python 3.9 + uses: actions/setup-python@v6 + with: + python-version: "3.9" + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + + - name: Create version, tag, release and distributions + id: release + uses: python-semantic-release/python-semantic-release@v10.5.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + git_committer_name: github-actions[bot] + git_committer_email: 41898282+github-actions[bot]@users.noreply.github.com + changelog: "false" + + - name: Publish package distributions to PyPI + if: steps.release.outputs.released == 'true' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ + print-hash: true + + - name: Upload distributions to GitHub Release + if: steps.release.outputs.released == 'true' + uses: python-semantic-release/publish-action@v10.5.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ steps.release.outputs.tag }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index bda7082..0000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,28 +0,0 @@ -# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. -# -# You can adjust the behavior by modifying this file. -# For more information, see: -# https://github.com/actions/stale -name: Mark stale issues and pull requests - -on: - schedule: - - cron: '17 1 * * *' - -jobs: - stale: - - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - - steps: - - uses: actions/stale@v5 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: 'Automatizaci贸n cerrado no hay actividad' - stale-pr-message: 'Automatizaci贸n cerrado no hay actividad' - stale-issue-label: 'no-issue-activity' - stale-pr-label: 'no-pr-activity' -, diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e9c100d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,55 @@ +name: Test + +on: + workflow_call: + inputs: + python-version: + required: true + type: string + runner: + required: true + type: string + architecture: + required: false + type: string + default: x64 + build-distribution: + required: false + type: boolean + default: false + +jobs: + test: + name: ${{ inputs.runner }} / Python ${{ inputs.python-version }} + runs-on: ${{ inputs.runner }} + steps: + - uses: actions/checkout@v6 + + - name: Set up Python ${{ inputs.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python-version }} + architecture: ${{ inputs.architecture }} + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install latest pip + run: | + python -m pip install --upgrade pip + + - name: Install + run: | + python -m pip install -e ".[dev]" + + - name: Run tests + env: + PYTHONWARNINGS: ignore:DEPRECATION + CI: "1" + PYTHONIOENCODING: "utf-8" + run: | + python -m pytest + + - name: Build distribution + if: inputs.build-distribution + run: | + python -m build diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 858b0cf..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include cfdiclient/*.xml diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 57b9e13..0000000 --- a/Pipfile +++ /dev/null @@ -1,20 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -lxml = ">=4.2.5" -requests = ">=2.21.0" -pycryptodome = ">=3.7.2" -pyOpenSSL = ">=18.0.0" - -[dev-packages] -importlib-metadata = ">=2.1.1" -atomicwrites = "*" -typing-extensions = "*" -pytest = ">=4.6.11" -autopep8 = "*" -setuptools = "==68" -wheel = "*" -twine = "*" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index e83182f..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,612 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "a9501c1ced4b7d5d91657f22fa4270d3933a57c12670efe163412249dd736d37" - }, - "pipfile-spec": 6, - "requires": {}, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", - "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3" - ], - "markers": "python_version >= '3.6'", - "version": "==2025.4.26" - }, - "cffi": { - "hashes": [ - "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d", - "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771", - "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872", - "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c", - "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc", - "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762", - "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202", - "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5", - "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548", - "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a", - "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f", - "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20", - "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218", - "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c", - "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e", - "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56", - "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224", - "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a", - "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2", - "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a", - "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819", - "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346", - "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b", - "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e", - "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534", - "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb", - "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0", - "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156", - "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd", - "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87", - "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc", - "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195", - "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33", - "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f", - "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d", - "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd", - "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728", - "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7", - "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca", - "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99", - "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf", - "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e", - "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c", - "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5", - "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69" - ], - "version": "==1.14.6" - }, - "cryptography": { - "hashes": [ - "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d", - "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959", - "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6", - "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873", - "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2", - "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713", - "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1", - "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177", - "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250", - "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca", - "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d", - "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9" - ], - "markers": "python_version >= '3.6'", - "version": "==3.4.7" - }, - "idna": { - "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.10" - }, - "lxml": { - "hashes": [ - "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d", - "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3", - "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2", - "sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae", - "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f", - "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927", - "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3", - "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7", - "sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59", - "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f", - "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade", - "sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96", - "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468", - "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b", - "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4", - "sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354", - "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83", - "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04", - "sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16", - "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791", - "sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a", - "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51", - "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1", - "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a", - "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f", - "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee", - "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec", - "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969", - "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28", - "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a", - "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa", - "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106", - "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d", - "sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617", - "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4", - "sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92", - "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0", - "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4", - "sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24", - "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2", - "sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e", - "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0", - "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654", - "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2", - "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23", - "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586" - ], - "index": "pypi", - "version": "==4.6.3" - }, - "pycparser": { - "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.20" - }, - "pycryptodome": { - "hashes": [ - "sha256:09c1555a3fa450e7eaca41ea11cd00afe7c91fef52353488e65663777d8524e0", - "sha256:12222a5edc9ca4a29de15fbd5339099c4c26c56e13c2ceddf0b920794f26165d", - "sha256:1723ebee5561628ce96748501cdaa7afaa67329d753933296321f0be55358dce", - "sha256:1c5e1ca507de2ad93474be5cfe2bfa76b7cf039a1a32fc196f40935944871a06", - "sha256:2603c98ae04aac675fefcf71a6c87dc4bb74a75e9071ae3923bbc91a59f08d35", - "sha256:2dea65df54349cdfa43d6b2e8edb83f5f8d6861e5cf7b1fbc3e34c5694c85e27", - "sha256:31c1df17b3dc5f39600a4057d7db53ac372f492c955b9b75dd439f5d8b460129", - "sha256:38661348ecb71476037f1e1f553159b80d256c00f6c0b00502acac891f7116d9", - "sha256:3e2e3a06580c5f190df843cdb90ea28d61099cf4924334d5297a995de68e4673", - "sha256:3f840c49d38986f6e17dbc0673d37947c88bc9d2d9dba1c01b979b36f8447db1", - "sha256:501ab36aae360e31d0ec370cf5ce8ace6cb4112060d099b993bc02b36ac83fb6", - "sha256:60386d1d4cfaad299803b45a5bc2089696eaf6cdd56f9fc17479a6f89595cfc8", - "sha256:6260e24d41149268122dd39d4ebd5941e9d107f49463f7e071fd397e29923b0c", - "sha256:6bbf7fee7b7948b29d7e71fcacf48bac0c57fb41332007061a933f2d996f9713", - "sha256:6d2df5223b12437e644ce0a3be7809471ffa71de44ccd28b02180401982594a6", - "sha256:758949ca62690b1540dfb24ad773c6da9cd0e425189e83e39c038bbd52b8e438", - "sha256:77997519d8eb8a4adcd9a47b9cec18f9b323e296986528186c0e9a7a15d6a07e", - "sha256:7fd519b89585abf57bf47d90166903ec7b43af4fe23c92273ea09e6336af5c07", - "sha256:98213ac2b18dc1969a47bc65a79a8fca02a414249d0c8635abb081c7f38c91b6", - "sha256:99b2f3fc51d308286071d0953f92055504a6ffe829a832a9fc7a04318a7683dd", - "sha256:9b6f711b25e01931f1c61ce0115245a23cdc8b80bf8539ac0363bdcf27d649b6", - "sha256:a3105a0eb63eacf98c2ecb0eb4aa03f77f40fbac2bdde22020bb8a536b226bb8", - "sha256:a8eb8b6ea09ec1c2535bf39914377bc8abcab2c7d30fa9225eb4fe412024e427", - "sha256:a92d5c414e8ee1249e850789052608f582416e82422502dc0ac8c577808a9067", - "sha256:d3d6958d53ad307df5e8469cc44474a75393a434addf20ecd451f38a72fe29b8", - "sha256:e0a4d5933a88a2c98bbe19c0c722f5483dc628d7a38338ac2cb64a7dbd34064b", - "sha256:e3bf558c6aeb49afa9f0c06cee7fb5947ee5a1ff3bd794b653d39926b49077fa", - "sha256:e61e363d9a5d7916f3a4ce984a929514c0df3daf3b1b2eb5e6edbb131ee771cf", - "sha256:f977cdf725b20f6b8229b0c87acb98c7717e742ef9f46b113985303ae12a99da", - "sha256:fc7489a50323a0df02378bc2fff86eb69d94cc5639914346c736be981c6a02e7" - ], - "index": "pypi", - "version": "==3.10.1" - }, - "pyopenssl": { - "hashes": [ - "sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51", - "sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b" - ], - "index": "pypi", - "version": "==20.0.1" - }, - "requests": { - "hashes": [ - "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", - "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" - ], - "index": "pypi", - "version": "==2.25.1" - }, - "urllib3": { - "hashes": [ - "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", - "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.6" - } - }, - "develop": { - "atomicwrites": { - "hashes": [ - "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", - "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "autopep8": { - "hashes": [ - "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0", - "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9" - ], - "index": "pypi", - "version": "==1.5.7" - }, - "backports.tarfile": { - "hashes": [ - "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", - "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991" - ], - "markers": "python_version >= '3.8'", - "version": "==1.2.0" - }, - "certifi": { - "hashes": [ - "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", - "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3" - ], - "markers": "python_version >= '3.6'", - "version": "==2025.4.26" - }, - "charset-normalizer": { - "hashes": [ - "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", - "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", - "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", - "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", - "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", - "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", - "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d", - "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", - "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184", - "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", - "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", - "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64", - "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", - "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", - "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", - "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344", - "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", - "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", - "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", - "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", - "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", - "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", - "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", - "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", - "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", - "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", - "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", - "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", - "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58", - "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", - "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", - "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2", - "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", - "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", - "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", - "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", - "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", - "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f", - "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", - "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", - "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", - "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", - "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", - "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", - "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", - "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", - "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4", - "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", - "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", - "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", - "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", - "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", - "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", - "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", - "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", - "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", - "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", - "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa", - "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", - "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", - "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", - "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", - "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", - "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", - "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02", - "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", - "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", - "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", - "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", - "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", - "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", - "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", - "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", - "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", - "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", - "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", - "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", - "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", - "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", - "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", - "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", - "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", - "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", - "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", - "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", - "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", - "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", - "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da", - "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", - "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f", - "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", - "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f" - ], - "markers": "python_version >= '3.7'", - "version": "==3.4.2" - }, - "docutils": { - "hashes": [ - "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", - "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2" - ], - "markers": "python_version >= '3.9'", - "version": "==0.21.2" - }, - "id": { - "hashes": [ - "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", - "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658" - ], - "markers": "python_version >= '3.8'", - "version": "==1.5.0" - }, - "idna": { - "hashes": [ - "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", - "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" - ], - "markers": "python_version >= '3.6'", - "version": "==3.10" - }, - "importlib-metadata": { - "hashes": [ - "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", - "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==8.7.0" - }, - "iniconfig": { - "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" - ], - "version": "==1.1.1" - }, - "jaraco.classes": { - "hashes": [ - "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", - "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" - ], - "markers": "python_version >= '3.8'", - "version": "==3.4.0" - }, - "jaraco.context": { - "hashes": [ - "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", - "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4" - ], - "markers": "python_version >= '3.8'", - "version": "==6.0.1" - }, - "jaraco.functools": { - "hashes": [ - "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", - "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649" - ], - "markers": "python_version >= '3.8'", - "version": "==4.1.0" - }, - "keyring": { - "hashes": [ - "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", - "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd" - ], - "markers": "python_version >= '3.9'", - "version": "==25.6.0" - }, - "markdown-it-py": { - "hashes": [ - "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", - "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" - ], - "markers": "python_version >= '3.8'", - "version": "==3.0.0" - }, - "mdurl": { - "hashes": [ - "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", - "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" - ], - "markers": "python_version >= '3.7'", - "version": "==0.1.2" - }, - "more-itertools": { - "hashes": [ - "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", - "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e" - ], - "markers": "python_version >= '3.9'", - "version": "==10.7.0" - }, - "nh3": { - "hashes": [ - "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", - "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", - "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", - "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde", - "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", - "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251", - "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", - "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d", - "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", - "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", - "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", - "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82", - "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b", - "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b", - "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", - "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", - "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", - "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", - "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9", - "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", - "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", - "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", - "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243", - "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286" - ], - "markers": "python_version >= '3.8'", - "version": "==0.2.21" - }, - "packaging": { - "hashes": [ - "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", - "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" - ], - "markers": "python_version >= '3.8'", - "version": "==25.0" - }, - "pluggy": { - "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.13.1" - }, - "pycodestyle": { - "hashes": [ - "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", - "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.7.0" - }, - "pygments": { - "hashes": [ - "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", - "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c" - ], - "markers": "python_version >= '3.8'", - "version": "==2.19.1" - }, - "pytest": { - "hashes": [ - "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", - "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" - ], - "index": "pypi", - "version": "==6.2.4" - }, - "readme-renderer": { - "hashes": [ - "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", - "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1" - ], - "markers": "python_version >= '3.9'", - "version": "==44.0" - }, - "requests": { - "hashes": [ - "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", - "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" - ], - "markers": "python_version >= '3.8'", - "version": "==2.32.3" - }, - "requests-toolbelt": { - "hashes": [ - "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", - "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.0.0" - }, - "rfc3986": { - "hashes": [ - "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", - "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.0" - }, - "rich": { - "hashes": [ - "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", - "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725" - ], - "markers": "python_full_version >= '3.8.0'", - "version": "==14.0.0" - }, - "setuptools": { - "hashes": [ - "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f", - "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==68.0.0" - }, - "twine": { - "hashes": [ - "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", - "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==6.1.0" - }, - "typing-extensions": { - "hashes": [ - "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", - "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==4.14.0" - }, - "urllib3": { - "hashes": [ - "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", - "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813" - ], - "markers": "python_version >= '3.9'", - "version": "==2.4.0" - }, - "wheel": { - "hashes": [ - "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", - "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==0.45.1" - }, - "zipp": { - "hashes": [ - "sha256:dd2f28c3ce4bc67507bfd3781d21b7bb2be31103b51a4553ad7d90b84e57ace5", - "sha256:fe208f65f2aca48b81f9e6fd8cf7b8b32c26375266b009b413d45306b6148343" - ], - "markers": "python_version >= '3.9'", - "version": "==3.22.0" - } - } -} diff --git a/cfdiclient/solicitadescargaEmitidos.py b/cfdiclient/solicitadescargaEmitidos.py index 16a460e..8a59d86 100644 --- a/cfdiclient/solicitadescargaEmitidos.py +++ b/cfdiclient/solicitadescargaEmitidos.py @@ -13,7 +13,7 @@ class SolicitaDescargaEmitidos(WebServiceRequest): def solicitar_descarga( self, token, rfc_solicitante, fecha_inicial, fecha_final, rfc_emisor=None, rfc_receptor=None, tipo_solicitud='CFDI', - tipo_comprobante=None, estado_comprobante=None, + tipo_comprobante=None, estado_comprobante='Vigente', rfc_a_cuenta_terceros=None, complemento=None, uuid=None ): diff --git a/cfdiclient/solicitadescargaRecibidos.py b/cfdiclient/solicitadescargaRecibidos.py index 57fd5c8..169c810 100644 --- a/cfdiclient/solicitadescargaRecibidos.py +++ b/cfdiclient/solicitadescargaRecibidos.py @@ -13,7 +13,7 @@ class SolicitaDescargaRecibidos(WebServiceRequest): def solicitar_descarga( self, token, rfc_solicitante, fecha_inicial, fecha_final, rfc_emisor=None, rfc_receptor=None, tipo_solicitud='CFDI', - tipo_comprobante=None, estado_comprobante=None, + tipo_comprobante=None, estado_comprobante='Vigente', rfc_a_cuenta_terceros=None, complemento=None, uuid=None ): diff --git a/ejemplo_completo.py b/ejemplo_completo.py index 4895e7f..e035c7c 100644 --- a/ejemplo_completo.py +++ b/ejemplo_completo.py @@ -44,7 +44,7 @@ # Recibidos solicitud = descarga.solicitar_descarga( - token, RFC, FECHA_INICIAL, FECHA_FINAL, rfc_receptor=RFC,tipo_solicitud='Metadata', estado_comprobante='Todos', + token, RFC, FECHA_INICIAL, FECHA_FINAL, rfc_receptor=RFC,tipo_solicitud='Metadata', estado_comprobante='Vigente', ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7f58874 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,88 @@ +[build-system] +requires = ["setuptools>=78.1.1"] +build-backend = "setuptools.build_meta" + +[project] +name = "cfdiclient" +version = "1.6.2" +description = "Cliente Python Web Service del SAT para la descarga masiva de CFDIs" +readme = "README.md" +requires-python = ">=3.9" +license = "GPL-3.0-only" +authors = [ + { name = "Luis Iturrios", email = "luisiturrios1@gmail.com" }, +] +dependencies = [ + "lxml>=6.1.0", + "requests>=2.32.5", + "pycryptodome>=3.19.1", + "pyOpenSSL>=26.0.0", + "cryptography>=46.0.6", + "idna>=3.15", + "urllib3>=2.6.3", +] +classifiers = [ + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Operating System :: OS Independent", +] + +[project.optional-dependencies] +dev = [ + "atomicwrites", + "autopep8", + "build", + "importlib-metadata>=2.1.1", + "pygments>=2.20.0", + "pytest>=8.4.2", + "python-semantic-release>=10.5.3", + "setuptools>=78.1.1", + "twine", + "typing-extensions", + "wheel>=0.46.2", +] + +[project.urls] +Homepage = "https://github.com/luisiturrios1/python-cfdiclient" +Repository = "https://github.com/luisiturrios1/python-cfdiclient" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +include = ["cfdiclient*"] + +[tool.setuptools.package-data] +cfdiclient = ["*.xml"] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.semantic_release] +version_toml = ["pyproject.toml:project.version"] +version_variables = ["cfdiclient/__init__.py:version"] +build_command = "python -m pip install --upgrade build && python -m build" +commit_message = "chore(release): v{version}" +commit_parser = "conventional" +tag_format = "v{version}" + +[tool.semantic_release.branches.master] +match = "master" +prerelease = false +prerelease_token = "rc" + +[tool.semantic_release.commit_parser_options] +minor_tags = ["feat"] +patch_tags = ["fix", "perf"] +other_allowed_tags = ["build", "chore", "ci", "docs", "style", "refactor", "test"] +allowed_tags = ["feat", "fix", "perf", "build", "chore", "ci", "docs", "style", "refactor", "test"] +default_bump_level = 0 +parse_squash_commits = true +ignore_merge_commits = true + +[tool.semantic_release.publish] +dist_glob_patterns = ["dist/*"] +upload_to_vcs_release = true diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 25c6497..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[tool:pytest] -testpaths = tests diff --git a/setup.py b/setup.py deleted file mode 100644 index 68c82fc..0000000 --- a/setup.py +++ /dev/null @@ -1,32 +0,0 @@ -import setuptools - -with open("README.md", "r") as fh: - long_description = fh.read() - -setuptools.setup( - name="cfdiclient", - version="1.6.2", - author="Luis Iturrios", - author_email="luisiturrios1@gmail.com", - description="Cliente Python Web Service del SAT para la descarga masiva de CFDIs", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/luisiturrios1/python-cfdiclient", - packages=setuptools.find_packages(), - include_package_data=True, - classifiers=[ - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "Operating System :: OS Independent", - ], - install_requires=[ - "lxml>=4.2.5", - "requests>=2.21.0", - "pycryptodome>=3.7.2", - "pyOpenSSL>=18.0.0", - ], -) From 0ffee1817c815298a250e02a94d8a7721abd85b1 Mon Sep 17 00:00:00 2001 From: Luis Iturrios Date: Sun, 7 Jun 2026 17:30:38 -0600 Subject: [PATCH 2/6] ci: add prerelease branch group --- .github/workflows/continuous_integration.yml | 2 +- pyproject.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 00736f0..2e67495 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -30,7 +30,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14"] runner: [macos-latest, ubuntu-latest, windows-latest] architecture: [x64] uses: ./.github/workflows/test.yml diff --git a/pyproject.toml b/pyproject.toml index 7f58874..606b07b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,11 @@ match = "master" prerelease = false prerelease_token = "rc" +[tool.semantic_release.branches.prerelease] +match = "^(?!master$).+" +prerelease = true +prerelease_token = "dev" + [tool.semantic_release.commit_parser_options] minor_tags = ["feat"] patch_tags = ["fix", "perf"] From 11a98040628a4ee64ad675730dd6dbbac5fe8a57 Mon Sep 17 00:00:00 2001 From: Luis Iturrios Date: Sun, 7 Jun 2026 17:35:09 -0600 Subject: [PATCH 3/6] ci: run full test matrix before prerelease --- .github/workflows/prerelease.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index af54cdd..77062e5 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -14,12 +14,19 @@ permissions: jobs: test: - name: Test + name: ${{ matrix.runner }} / Python ${{ matrix.python-version }} + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12", "3.13", "3.14"] + runner: [macos-latest, ubuntu-latest, windows-latest] + architecture: [x64] uses: ./.github/workflows/test.yml with: - python-version: "3.13" - runner: ubuntu-latest - build-distribution: true + python-version: ${{ matrix.python-version }} + runner: ${{ matrix.runner }} + architecture: ${{ matrix.architecture }} + build-distribution: ${{ matrix.runner == 'ubuntu-latest' && matrix.python-version == '3.13' }} prerelease: name: Prerelease to PyPI From 236e5631ed2ad1426c5ea60a5e0c35b40ebfd61d Mon Sep 17 00:00:00 2001 From: Luis Iturrios Date: Sun, 7 Jun 2026 17:40:34 -0600 Subject: [PATCH 4/6] fix: CI version --- .github/workflows/release.yml | 4 ++-- pyproject.toml | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4b9719d..b16fd11 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,10 +29,10 @@ jobs: fetch-depth: 0 fetch-tags: true - - name: Set up Python 3.9 + - name: Set up Python 3.13 uses: actions/setup-python@v6 with: - python-version: "3.9" + python-version: "3.13" cache: pip cache-dependency-path: pyproject.toml diff --git a/pyproject.toml b/pyproject.toml index 606b07b..cd19cdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,11 +22,10 @@ dependencies = [ "urllib3>=2.6.3", ] classifiers = [ - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", ] From d42a04a308dd0b6c1b714c287f3c52f07a935232 Mon Sep 17 00:00:00 2001 From: Luis Iturrios Date: Sun, 7 Jun 2026 17:44:53 -0600 Subject: [PATCH 5/6] ci: Quitar pipeline viejo --- .github/workflows/continuous_integration.yml | 41 -------------------- 1 file changed, 41 deletions(-) delete mode 100644 .github/workflows/continuous_integration.yml diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml deleted file mode 100644 index 2e67495..0000000 --- a/.github/workflows/continuous_integration.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Continuous Integration -on: - push: - branches: - - master - paths-ignore: - - "docs/**" - - "*.ini" - - "*.md" - - "**/*.txt" - - ".gitignore" - - ".gitmodules" - - ".gitattributes" - - ".editorconfig" - pull_request: - branches: - - master - paths-ignore: - - "docs/**" - - "*.ini" - - "*.md" - - "**/*.txt" - - ".gitignore" - - ".gitmodules" - - ".gitattributes" - - ".editorconfig" -jobs: - build: - name: ${{ matrix.runner }} / Python ${{ matrix.python-version }} - strategy: - fail-fast: false - matrix: - python-version: ["3.11", "3.12", "3.13", "3.14"] - runner: [macos-latest, ubuntu-latest, windows-latest] - architecture: [x64] - uses: ./.github/workflows/test.yml - with: - python-version: ${{ matrix.python-version }} - runner: ${{ matrix.runner }} - architecture: ${{ matrix.architecture }} - build-distribution: ${{ matrix.runner == 'ubuntu-latest' && matrix.python-version == '3.13' }} From 3331e22cf1da491c7ce84cd4696435e819d98df9 Mon Sep 17 00:00:00 2001 From: Luis Iturrios Date: Sun, 7 Jun 2026 18:25:09 -0600 Subject: [PATCH 6/6] chore: Actualizar readme y agents --- AGENTS.md | 68 ++++++++++ README.md | 389 +++++++++++++++++++++++++++++++++--------------------- 2 files changed, 305 insertions(+), 152 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a2e17ad --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,68 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +This repository packages `cfdiclient`, a Python client for SAT CFDI web services. +Source code lives in `cfdiclient/`, with one module per service concern: authentication +(`autenticacion.py`), signing (`signer.py`, `fiel.py`), mass download flows +(`solicitadescarga*.py`, `verificasolicituddescarga.py`, `descargamasiva.py`), and +shared helpers (`utils.py`, `webservicerequest.py`). SOAP/XML templates are bundled +beside the modules as `cfdiclient/*.xml`; keep template edits synchronized with the +Python code that loads them. Tests are in `tests/`. Example usage is in +`README.md` and `ejemplo_completo.py`. Sample certificate files are under +`certificados/` and should not be replaced with real private credentials. + +## Build, Test, and Development Commands + +Create an isolated environment and install development dependencies: + +```bash +python -m venv .venv +source .venv/bin/activate +python -m pip install -e ".[dev]" +``` + +Run the test suite: + +```bash +pytest +``` + +Build distributable artifacts in `dist/`: + +```bash +python -m build +``` + +Optionally lint with the included Pylint configuration: + +```bash +pylint --rcfile=pylint.rc cfdiclient tests +``` + +## Coding Style & Naming Conventions + +Follow `.editorconfig`: UTF-8, LF line endings, final newline, and 4-space +indentation for Python. Prefer clear snake_case for functions, variables, and module +names. Preserve the existing public API names and Spanish SAT domain terminology +(`Fiel`, `Autenticacion`, `SolicitaDescarga`, `rfc_solicitante`) unless a change is +explicitly intended to be breaking. Keep XML template filenames aligned with their +service modules. + +## Testing Guidelines + +Tests use `pytest`, with discovery configured for `tests/` in `pyproject.toml`. +Name test files `test_*.py` and test functions `test_*`. Add focused unit tests for +parsing, request construction, signature behavior, and error handling when changing +service modules. Avoid tests that require live SAT credentials or network access; +mock HTTP responses and use fixture data instead. + +## Commit & Pull Request Guidelines + +The project is configured for `python-semantic-release` with conventional commits. +Use tags such as `fix:`, `feat:`, `perf:`, `docs:`, `test:`, `refactor:`, `ci:`, +`build:`, `style:`, and `chore:`. `feat` creates minor releases; `fix` and `perf` +create patch releases. Pull requests should describe the behavioral change, mention +affected SAT service flows, link related issues, and include the commands run +(`pytest`, `python -m build`, linting when relevant). Do not include real RFC keys, +FIEL passwords, tokens, or downloaded CFDI data in commits, logs, or PR text. diff --git a/README.md b/README.md index 2d666ec..31b7d18 100644 --- a/README.md +++ b/README.md @@ -1,233 +1,318 @@ # python-cfdiclient -Cliente Python Web Service del SAT para la descarga masiva de xml +Cliente Python para consumir los servicios web del SAT relacionados con CFDI: -## Consulta y recuperaci贸n de comprobantes (Nuevo) +- Autenticaci贸n con e.firma/FIEL. +- Solicitud de descarga masiva de CFDI o metadata. +- Verificaci贸n del estado de una solicitud. +- Descarga de paquetes generados por el SAT. +- Consulta del estado de un CFDI. -https://www.sat.gob.mx/consultas/42968/consulta-y-recuperacion-de-comprobantes-(nuevo) +El paquete est谩 pensado para integrarse en aplicaciones que ya cuentan con los +archivos `.cer`, `.key`, la contrase帽a de la e.firma y el RFC del contribuyente. +No incluye credenciales reales ni debe usarse para almacenarlas. -## Instalacion +Servicio SAT de referencia: + -En Windows requiere Microsoft Visual C++ Compiler for Python 2.7 +## Requisitos -```bash -pip install cfdiclient -``` +- Python 3.9 o superior. +- Certificado `.cer` y llave privada `.key` de la e.firma en formato DER. +- Contrase帽a de la llave privada. +- Acceso de red a los servicios del SAT. -## Ejemplo Completo - -```python -import base64 -import datetime -import os -import time +## Instalaci贸n -from cfdiclient import (Autenticacion, DescargaMasiva, Fiel, SolicitaDescarga, - VerificaSolicitudDescarga) +Instala la versi贸n publicada en PyPI: -RFC = 'IUAL9406031K4' -FIEL_CER = 'asd.cer' -FIEL_KEY = 'df.key' -FIEL_PAS = '' -FECHA_INICIAL = datetime.date(2020, 1, 1) -FECHA_FINAL = datetime.date(2020, 6, 24) -PATH = 'Inputs/IUAL9406031K4/' +```bash +python -m pip install cfdiclient +``` -cer_der = open(os.path.join(PATH, FIEL_CER), 'rb').read() -key_der = open(os.path.join(PATH, FIEL_KEY), 'rb').read() +Para trabajar con el proyecto localmente: -fiel = Fiel(cer_der, key_der, FIEL_PAS) +```bash +python -m venv .venv +source .venv/bin/activate +python -m pip install -e ".[dev]" +pytest +``` -auth = Autenticacion(fiel) +## Flujo de descarga masiva -token = auth.obtener_token() +La descarga masiva no entrega los archivos inmediatamente. El flujo normal es: -print('TOKEN: ', token) +1. Cargar la e.firma con `Fiel`. +2. Obtener un token con `Autenticacion`. +3. Crear una solicitud con `SolicitaDescargaEmitidos` o + `SolicitaDescargaRecibidos`. +4. Consultar peri贸dicamente la solicitud con `VerificaSolicitudDescarga`. +5. Cuando `estado_solicitud` sea `3`, descargar cada paquete con + `DescargaMasiva`. -descarga = SolicitaDescarga(fiel) +Estados de solicitud reportados por el SAT: -# EMITIDOS -# solicitud = descarga.solicitar_descarga( -# token, RFC, FECHA_INICIAL, FECHA_FINAL, rfc_emisor=RFC, tipo_solicitud='CFDI' -# ) +| Estado | Significado | +| --- | --- | +| `0` | Token inv谩lido | +| `1` | Aceptada | +| `2` | En proceso | +| `3` | Terminada | +| `4` | Error | +| `5` | Rechazada | +| `6` | Vencida | -# RECIBIDOS -solicitud = descarga.solicitar_descarga( - token, RFC, FECHA_INICIAL, FECHA_FINAL, rfc_receptor=RFC, tipo_solicitud='CFDI' -) +## Ejemplo completo -print('SOLICITUD:', solicitud) +Este ejemplo solicita CFDI recibidos, espera a que el SAT termine la solicitud y +guarda cada paquete como archivo `.zip`. -while True: +```python +import base64 +import datetime +import time +from pathlib import Path + +from cfdiclient import ( + Autenticacion, + DescargaMasiva, + Fiel, + SolicitaDescargaRecibidos, + VerificaSolicitudDescarga, +) - token = auth.obtener_token() +RFC = "XAXX010101000" +FIEL_CER = Path("certificados/ejemploCer.cer") +FIEL_KEY = Path("certificados/ejemploKey.key") +FIEL_PASS = "contrasena" - print('TOKEN: ', token) +FECHA_INICIAL = datetime.datetime(2025, 1, 1) +FECHA_FINAL = datetime.datetime(2025, 1, 31, 23, 59, 59) - verificacion = VerificaSolicitudDescarga(fiel) +cer_der = FIEL_CER.read_bytes() +key_der = FIEL_KEY.read_bytes() +fiel = Fiel(cer_der, key_der, FIEL_PASS) - verificacion = verificacion.verificar_descarga( - token, RFC, solicitud['id_solicitud']) +auth = Autenticacion(fiel) +token = auth.obtener_token() - print('SOLICITUD:', verificacion) +solicitador = SolicitaDescargaRecibidos(fiel) +solicitud = solicitador.solicitar_descarga( + token=token, + rfc_solicitante=RFC, + fecha_inicial=FECHA_INICIAL, + fecha_final=FECHA_FINAL, + rfc_receptor=RFC, + tipo_solicitud="CFDI", +) - estado_solicitud = int(verificacion['estado_solicitud']) +if solicitud["cod_estatus"] != "5000": + raise RuntimeError(f"Solicitud no aceptada por el SAT: {solicitud}") - # 0, Token invalido. - # 1, Aceptada - # 2, En proceso - # 3, Terminada - # 4, Error - # 5, Rechazada - # 6, Vencida +id_solicitud = solicitud["id_solicitud"] +verificador = VerificaSolicitudDescarga(fiel) - if estado_solicitud <= 2: +while True: + token = auth.obtener_token() + verificacion = verificador.verificar_descarga(token, RFC, id_solicitud) + estado = int(verificacion["estado_solicitud"]) - # Si el estado de solicitud esta Aceptado o en proceso el programa espera - # 60 segundos y vuelve a tratar de verificar + if estado in (1, 2): time.sleep(60) - continue - elif estado_solicitud >= 4: - - print('ERROR:', estado_solicitud) + if estado != 3: + raise RuntimeError(f"La solicitud no termin贸 correctamente: {verificacion}") - break + descargador = DescargaMasiva(fiel) - else: - # Si el estatus es 3 se trata de descargar los paquetes + for id_paquete in verificacion["paquetes"]: + respuesta = descargador.descargar_paquete(token, RFC, id_paquete) + paquete = base64.b64decode(respuesta["paquete_b64"]) + Path(f"{id_paquete}.zip").write_bytes(paquete) - for paquete in verificacion['paquetes']: + break +``` - descarga = DescargaMasiva(fiel) +## Preparar e.firma y token - descarga = descarga.descargar_paquete(token, RFC, paquete) +Los ejemplos siguientes asumen que ya cargaste `cer_der`, `key_der` y `token`. +Puedes obtenerlos as铆: - print('PAQUETE: ', paquete) +```python +from pathlib import Path - with open('{}.zip'.format(paquete), 'wb') as fp: +from cfdiclient import Autenticacion, Fiel - fp.write(base64.b64decode(descarga['paquete_b64'])) +cer_der = Path("certificado.cer").read_bytes() +key_der = Path("llave.key").read_bytes() - break +fiel = Fiel(cer_der, key_der, "contrasena") +token = Autenticacion(fiel).obtener_token() ``` -## Ejemplo +## Solicitar CFDI emitidos -### Autenticacion +Usa `SolicitaDescargaEmitidos` cuando quieras comprobantes emitidos por el RFC +solicitante. ```python -from cfdiclient import Autenticacion -from cfdiclient import Fiel +import datetime -FIEL_KEY = 'Claveprivada_FIEL_XAXX010101000_20180918_134149.key' -FIEL_CER = 'XAXX010101000.cer' -FIEL_PAS = 'contrasena' -cer_der = open(FIEL_CER, 'rb').read() -key_der = open(FIEL_KEY, 'rb').read() -fiel = Fiel(cer_der, key_der, FIEL_PAS) +from cfdiclient import Fiel, SolicitaDescargaEmitidos -auth = Autenticacion(fiel) +fiel = Fiel(cer_der, key_der, "contrasena") +descarga = SolicitaDescargaEmitidos(fiel) -token = auth.obtener_token() +resultado = descarga.solicitar_descarga( + token=token, + rfc_solicitante="XAXX010101000", + fecha_inicial=datetime.datetime(2025, 1, 1), + fecha_final=datetime.datetime(2025, 1, 31, 23, 59, 59), + rfc_emisor="XAXX010101000", + tipo_solicitud="CFDI", +) -print(token) +print(resultado) +# { +# "id_solicitud": "be2a3e76-684f-416a-afdf-0f9378c346be", +# "cod_estatus": "5000", +# "mensaje": "Solicitud Aceptada", +# } ``` -### Solicita Descarga +## Solicitar CFDI recibidos + +Usa `SolicitaDescargaRecibidos` cuando quieras comprobantes recibidos por el RFC +solicitante. ```python import datetime -from cfdiclient import SolicitaDescarga -from cfdiclient import Fiel - -FIEL_KEY = 'Claveprivada_FIEL_XAXX010101000_20180918_134149.key' -FIEL_CER = 'XAXX010101000.cer' -FIEL_PAS = 'contrasena' -cer_der = open(FIEL_CER, 'rb').read() -key_der = open(FIEL_KEY, 'rb').read() - -fiel = Fiel(cer_der, key_der, FIEL_PAS) - -descarga = SolicitaDescarga(fiel) - -token = 'eyJh' -rfc_solicitante = 'XAXX010101000' -fecha_inicial = datetime.datetime(2018, 1, 1) -fecha_final = datetime.datetime(2018, 12, 31) -rfc_emisor = 'XAXX010101000' -rfc_receptor = 'XAXX010101000' -# Emitidos -result = descarga.solicitar_descarga(token, rfc_solicitante, fecha_inicial, fecha_final, rfc_emisor=rfc_emisor) -print(result) -# Recibidos -result = descarga.solicitar_descarga(token, rfc_solicitante, fecha_inicial, fecha_final, rfc_receptor=rfc_receptor) -print(result) -# {'mensaje': 'Solicitud Aceptada', 'cod_estatus': '5000', 'id_solicitud': 'be2a3e76-684f-416a-afdf-0f9378c346be'} + +from cfdiclient import Fiel, SolicitaDescargaRecibidos + +fiel = Fiel(cer_der, key_der, "contrasena") +descarga = SolicitaDescargaRecibidos(fiel) + +resultado = descarga.solicitar_descarga( + token=token, + rfc_solicitante="XAXX010101000", + fecha_inicial=datetime.datetime(2025, 1, 1), + fecha_final=datetime.datetime(2025, 1, 31, 23, 59, 59), + rfc_receptor="XAXX010101000", + tipo_solicitud="Metadata", + estado_comprobante="Vigente", +) + +print(resultado) ``` -### Verifica Solicitud Descarga +## Verificar una solicitud ```python -from cfdiclient import VerificaSolicitudDescarga -from cfdiclient import Fiel - -FIEL_KEY = 'Claveprivada_FIEL_XAXX010101000_20180918_134149.key' -FIEL_CER = 'XAXX010101000.cer' -FIEL_PAS = 'contrasena' -cer_der = open(FIEL_CER, 'rb').read() -key_der = open(FIEL_KEY, 'rb').read() +from cfdiclient import Fiel, VerificaSolicitudDescarga -fiel = Fiel(cer_der, key_der, FIEL_PAS) +fiel = Fiel(cer_der, key_der, "contrasena") +verificador = VerificaSolicitudDescarga(fiel) -v_descarga = VerificaSolicitudDescarga(fiel) +resultado = verificador.verificar_descarga( + token="eyJhbGci...", + rfc_solicitante="XAXX010101000", + id_solicitud="6331caae-c253-406f-9332-126f89cc474a", +) -token = 'eyJhbGci' -rfc_solicitante = 'XAXX010101000' -id_solicitud = '6331caae-c253-406f-9332-126f89cc474a' -result = v_descarga.verificar_descarga(token, rfc_solicitante, id_solicitud) -print(result) -# {'estado_solicitud': '3', 'numero_cfdis': '8', 'cod_estatus': '5000', 'paquetes': ['a4897f62-a279-4f52-bc35-03bde4081627_01'], 'codigo_estado_solicitud': '5000', 'mensaje': 'Solicitud Aceptada'} +print(resultado) +# { +# "cod_estatus": "5000", +# "estado_solicitud": "3", +# "codigo_estado_solicitud": "5000", +# "numero_cfdis": "8", +# "mensaje": "Solicitud Aceptada", +# "paquetes": ["a4897f62-a279-4f52-bc35-03bde4081627_01"], +# } ``` -### Descargar Paquetes +## Descargar un paquete ```python -from cfdiclient import DescargaMasiva -from cfdiclient import Fiel - -FIEL_KEY = 'Claveprivada_FIEL_XAXX010101000_20180918_134149.key' -FIEL_CER = 'XAXX010101000.cer' -FIEL_PAS = 'contrasena' -cer_der = open(FIEL_CER, 'rb').read() -key_der = open(FIEL_KEY, 'rb').read() +import base64 +from pathlib import Path -fiel = Fiel(cer_der, key_der, FIEL_PAS) +from cfdiclient import DescargaMasiva, Fiel +fiel = Fiel(cer_der, key_der, "contrasena") descarga = DescargaMasiva(fiel) -token = 'eyJhbG' -rfc_solicitante = 'XAXX010101000' -id_paquete = '2d8bbdf1-c36d-4b51-a57c-c1744acdd89c_01' -result = descarga.descargar_paquete(token, rfc_solicitante, id_paquete) -print(result) -# {'cod_estatus': '', 'mensaje': '', 'paquete_b64': 'eyJhbG=='} +resultado = descarga.descargar_paquete( + token="eyJhbGci...", + rfc_solicitante="XAXX010101000", + id_paquete="2d8bbdf1-c36d-4b51-a57c-c1744acdd89c_01", +) + +Path("paquete.zip").write_bytes(base64.b64decode(resultado["paquete_b64"])) ``` -### Valida estado de documento +## Consultar estado de un CFDI ```python from cfdiclient import Validacion validacion = Validacion() -rfc_emisor = 'XAXX010101000' -rfc_receptor = 'XAXX010101000' -total = '1000.41' -uuid = '0XXX0X00-000-0XX0-XX0X-000X0X0XXX00' -estado = validacion.obtener_estado(rfc_emisor, rfc_receptor, total, uuid) +estado = validacion.obtener_estado( + rfc_emisor="XAXX010101000", + rfc_receptor="XAXX010101000", + total="1000.41", + uuid="0XXX0X00-000-0XX0-XX0X-000X0X0XXX00", +) print(estado) -# {'codigo_estatus': 'S - Comprobante obtenido satisfactoriamente.', 'es_cancelable': 'Cancelable con aceptaci贸n', 'estado': 'Vigente'} +# { +# "codigo_estatus": "S - Comprobante obtenido satisfactoriamente.", +# "es_cancelable": "Cancelable con aceptaci贸n", +# "estado": "Vigente", +# } +``` + +## Par谩metros comunes de solicitud + +`SolicitaDescargaEmitidos.solicitar_descarga` y +`SolicitaDescargaRecibidos.solicitar_descarga` aceptan estos par谩metros: + +| Par谩metro | Descripci贸n | +| --- | --- | +| `token` | Token obtenido con `Autenticacion.obtener_token()`. | +| `rfc_solicitante` | RFC del contribuyente que realiza la solicitud. | +| `fecha_inicial` | Inicio del rango como `datetime.datetime` o `datetime.date`. | +| `fecha_final` | Fin del rango como `datetime.datetime` o `datetime.date`. | +| `rfc_emisor` | RFC emisor. 脷salo normalmente en solicitudes de emitidos. | +| `rfc_receptor` | RFC receptor. 脷salo normalmente en solicitudes de recibidos. | +| `tipo_solicitud` | `"CFDI"` para XML o `"Metadata"` para metadata. | +| `tipo_comprobante` | Filtro opcional por tipo de comprobante. | +| `estado_comprobante` | Filtro opcional; por defecto `"Vigente"`. | +| `rfc_a_cuenta_terceros` | Filtro opcional para comprobantes a cuenta de terceros. | +| `complemento` | Filtro opcional por complemento. | +| `uuid` | Filtro opcional por UUID. | + +## Desarrollo + +Comandos 煤tiles para colaboradores: + +```bash +pytest +pylint --rcfile=pylint.rc cfdiclient tests +python -m build ``` + +Las pruebas deben ejecutarse sin credenciales reales y sin depender de servicios +vivos del SAT. Al agregar cambios sobre SOAP/XML, mant茅n sincronizados los +templates `cfdiclient/*.xml` con el c贸digo que los llena. + +## Seguridad + +- No subas certificados, llaves privadas, contrase帽as, tokens ni paquetes CFDI + reales al repositorio. +- Los archivos en `certificados/` son datos de ejemplo. +- Prefiere variables de entorno o un secreto administrado por tu plataforma para + cargar credenciales en aplicaciones productivas.