././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707659114.0697205 twine-5.0.0/0000755000175100001770000000000014562147552012262 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/.coveragerc0000644000175100001770000000046214562147542014404 0ustar00runnerdocker[run] branch = True dynamic_context = test_function [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain if non-runnable code isn't run if __name__ == .__main__.: [html] show_contexts = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/.flake80000644000175100001770000000076014562147542013437 0ustar00runnerdocker[flake8] # Matching black's default max-line-length = 88 extend-ignore = # Missing docstring in __init__ D107 per-file-ignores = # TODO: Incrementally add missing docstrings # D100 Missing docstring in public module # D101 Missing docstring in public class # D102 Missing docstring in public method # D103 Missing docstring in public function # D104 Missing docstring in public package twine/*: D100,D101,D102,D103,D104 tests/*: D100,D101,D102,D103,D104 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/.git-blame-ignore-revs0000644000175100001770000000067314562147542016367 0ustar00runnerdocker# When making commits that are strictly formatting/style changes, add the # commit hash here, so git blame can ignore the change. # Use as needed with: # git blame --ignore-revs-file .git-blame-ignore-revs # Or automatically with: # git config blame.ignoreRevsFile .git-blame-ignore-revs a12ad693137d82770e6118ea8d90955e2c753305 # Format twine and tests with black f468612c021eae225b07f1b654bdb620d1500bf1 # Sort imports with isort ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707659114.0577204 twine-5.0.0/.github/0000755000175100001770000000000014562147552013622 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707659114.0577204 twine-5.0.0/.github/ISSUE_TEMPLATE/0000755000175100001770000000000014562147552016005 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/.github/ISSUE_TEMPLATE/01_upload_failed.yml0000644000175100001770000001031314562147542021615 0ustar00runnerdockername: "Upload Error" description: "Failed to upload artifact(s)" labels: ["support"] body: - type: markdown attributes: value: > ## Your Environment Thank you for taking the time to report an issue. To more efficiently resolve this issue, we'd like to know some basic information about your system and setup. - type: checkboxes attributes: label: "Is there an existing issue for this?" description: "Please search to see if there's an existing issue for what you're reporting" options: - label: "I have searched the existing issues (open and closed), and could not find an existing issue" required: true - type: textarea id: search-keywords attributes: label: "What keywords did you use to search existing issues?" description: "In the event that you could not find a duplicate, but it existed, this will help us better link issues in the future" placeholder: | authorization artifactory jfrog devpi - type: dropdown id: environment-os attributes: label: "What operating system(s) are you using?" multiple: true options: - "Windows 10" - "Windows 11" - "Linux" - "macOS" - "Other" validations: required: true - type: markdown id: environment-os-other attributes: label: "If you selected 'Other', describe your Operating System here" validations: required: false - type: textarea id: environment-py attributes: label: "What version of Python are you running?" description: "Please copy and paste the command and output used to retrieve this (this will be console rendered automatically)" placeholder: | $ python --version Python 3.11.4 render: console validations: required: true - type: textarea id: environment-installer attributes: label: "How did you install twine? Did you use your operating system's package manager or pip or something else?" description: "Please copy and paste the command(s) you used to install twine" placeholder: | $ pip install twine $ dnf install twine $ apt install twine $ brew install twine render: console validations: required: true - type: textarea id: version attributes: label: "What version of twine do you have installed (include the complete output)" description: "Please copy and paste the complete output of `twine --version`" placeholder: | $ twine --version twine version 1.15.0 (pkginfo: 1.4.2, requests: 2.19.1, setuptools: 40.4.3, requests-toolbelt: 0.8.0, tqdm: 4.26.0) render: console validations: required: true - type: markdown id: package-repository attributes: label: "Which package repository are you using?" placeholder: | pypi.org test.pypi.org validations: required: true - type: textarea id: issue attributes: label: "Please describe the issue that you are experiencing" placeholder: "When I run twine upload it does ... but I expect it to do ..." validations: required: true - type: textarea id: reproduction-steps attributes: label: "Please list the steps required to reproduce this behaviour" placeholder: | 1. Install twine in a virtual environment 1. Build this package at github.com/... 1. Run `twine upload dist/*` validations: required: true - type: textarea id: pkg-info attributes: label: "Please include the PKG-INFO file contents from the artifact you're attempting to upload" description: "note: this will be email formatted automatically" render: email validations: required: true - type: textarea id: pypirc attributes: label: "A redacted version of your `.pypirc` file" description: "REMOVE ALL USERNAMES & PASSWORDS; note: this will be ini formatted automatically" render: ini validations: required: true - type: textarea id: other attributes: label: "Anything else you'd like to mention?" validations: required: false ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/.github/ISSUE_TEMPLATE/02_bug.yml0000644000175100001770000000727414562147542017617 0ustar00runnerdockername: "Bug Report" description: "Something went wrong with twine (other than an upload error)" labels: ["bug"] body: - type: markdown attributes: value: > ## Your Environment Thank you for taking the time to report an issue. To more efficiently resolve this issue, we'd like to know some basic information about your system and setup. - type: checkboxes attributes: label: "Is there an existing issue for this?" description: "Please search to see if there's an existing issue for what you're reporting" options: - label: "I have searched the existing issues (open and closed), and could not find an existing issue" required: true - type: textarea id: search-keywords attributes: label: "What keywords did you use to search existing issues?" description: "In the event that you could not find a duplicate, but it existed, this will help us better link issues in the future" placeholder: | authorization artifactory jfrog devpi - type: dropdown id: environment-os attributes: label: "What operating system are you using?" multiple: true options: - "Windows 10" - "Windows 11" - "Linux" - "macOS" - "Other" validations: required: true - type: markdown id: environment-os-other attributes: label: "If you selected 'Other', describe your Operating System here" validations: required: false - type: textarea id: environment-py attributes: label: "What version of Python are you running?" description: "Please copy and paste the command and output used to retrieve this (this will be console rendered automatically)" placeholder: | $ python --version Python 3.11.4 render: console validations: required: true - type: textarea id: environment-installer attributes: label: "How did you install twine? Did you use your operating system's package manager or pip or something else?" description: "Please copy and paste the command(s) you used to install twine" placeholder: | $ pip install twine $ dnf install twine $ apt install twine $ brew install twine render: console validations: required: true - type: textarea id: version attributes: label: "What version of twine do you have installed (include the complete output)" description: "Please copy and paste the complete output of `twine --version`" placeholder: | $ twine --version twine version 1.15.0 (pkginfo: 1.4.2, requests: 2.19.1, setuptools: 40.4.3, requests-toolbelt: 0.8.0, tqdm: 4.26.0) render: console validations: required: true - type: markdown id: package-repository attributes: label: "Which package repository are you using?" placeholder: | pypi.org test.pypi.org validations: required: true - type: textarea id: issue attributes: label: "Please describe the issue that you are experiencing" placeholder: "When I run twine upload it does ... but I expect it to do ..." validations: required: true - type: textarea id: reproduction-steps attributes: label: "Please list the steps required to reproduce this behaviour" placeholder: | 1. Install twine in a virtual environment 1. Build this package at github.com/... 1. Run `twine upload dist/*` validations: required: true - type: textarea id: other attributes: label: "Anything else you'd like to mention?" validations: required: false ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/.github/ISSUE_TEMPLATE/03_feature.yml0000644000175100001770000000252114562147542020464 0ustar00runnerdockername: "Feature Request" description: "Something is missing from Twine" labels: ["enhancement", "feature request"] body: - type: checkboxes attributes: label: "Is there an existing issue for this?" description: "Please search to see if there's an existing issue for what you're reporting" options: - label: "I have searched the existing issues (open and closed), and could not find an existing issue" required: true - type: textarea id: search-keywords attributes: label: "What keywords did you use to search existing issues?" description: "In the event that you could not find a duplicate, but it existed, this will help us better link issues in the future" placeholder: | authorization artifactory jfrog devpi - type: textarea id: problem attributes: label: "Please describe the problem you are attempting to solve with this request" description: "Is there missing behaviour or some other issue?" placeholder: "When I run twine upload it does ... but I wish it would ..." validations: required: true - type: textarea id: proposed-solution attributes: label: "How do you think we should solve this?" - type: textarea id: other attributes: label: "Anything else you'd like to mention?" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/.github/ISSUE_TEMPLATE/04_other.yml0000644000175100001770000000206414562147542020155 0ustar00runnerdockername: "Other" description: "This does not fit into the other categories" labels: ["enhancement", "feature request"] body: - type: checkboxes attributes: label: "Is there an existing issue for this?" description: "Please search to see if there's an existing issue for what you're reporting" options: - label: "I have searched the existing issues (open and closed), and could not find an existing issue" required: true - type: textarea id: search-keywords attributes: label: "What keywords did you use to search existing issues?" description: "In the event that you could not find a duplicate, but it existed, this will help us better link issues in the future" placeholder: | authorization artifactory jfrog devpi - type: textarea id: other-description attributes: label: "Please describe why your using this option" validations: required: true - type: textarea id: other attributes: label: "Anything else you'd like to mention?" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/.github/ISSUE_TEMPLATE/config.yml0000644000175100001770000000022014562147542017766 0ustar00runnerdockerblank_issues_enabled: false contact_links: - name: Packaging issues or metadata issues url: https://github.com/pypa/packaging-problems/issues ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/.github/dependabot.yml0000644000175100001770000000016514562147542016453 0ustar00runnerdockerversion: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707659114.0577204 twine-5.0.0/.github/workflows/0000755000175100001770000000000014562147552015657 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/.github/workflows/codeql-analysis.yml0000644000175100001770000000537114562147542021477 0ustar00runnerdocker# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ "main" ] pull_request: # The branches below must be a subset of the branches above branches: [ "main" ] schedule: - cron: '43 23 * * 6' permissions: contents: read jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'python' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, Go, 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@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/.github/workflows/main.yml0000644000175100001770000000625214562147542017332 0ustar00runnerdockername: Main on: push: branches: - main pull_request: schedule: - cron: "0 0 * * *" # daily concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: FORCE_COLOR: "1" TOX_TESTENV_PASSENV: "FORCE_COLOR" MIN_PYTHON_VERSION: "3.8" DEFAULT_PYTHON_VERSION: "3.10" permissions: contents: read jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ env.DEFAULT_PYTHON_VERSION }} - name: Install dependencies run: python -m pip install tox - name: Run linting run: python -m tox -e lint test: strategy: matrix: python-version: - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" platform: - ubuntu-latest - macos-latest - windows-latest runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: python -m pip install tox - name: Run type-checking run: python -m tox -e types - name: Run tests run: python -m tox -e py # Because the tests can be flaky, they shouldn't be required for merge, but # it's still helpful to run them on PRs. See: # https://github.com/pypa/twine/issues/684#issuecomment-703150619 integration: # Only run on Ubuntu because most of the tests are skipped on Windows runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ env.MIN_PYTHON_VERSION }} - name: Install dependencies run: python -m pip install tox - name: Run tests run: python -m tox -e integration docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ env.MIN_PYTHON_VERSION }} - name: Install dependencies run: python -m pip install tox - name: Build docs run: python -m tox -e docs # https://github.com/marketplace/actions/alls-green#why check: # This job does nothing and is only used for the branch protection if: always() needs: - lint - test - integration - docs runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: allowed-failures: integration # can be flaky jobs: ${{ toJSON(needs) }} release: needs: - check if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ env.MIN_PYTHON_VERSION }} - name: Install dependencies run: python -m pip install tox - name: Release run: tox -e release env: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/.github/workflows/release.yml0000644000175100001770000000406714562147542020030 0ustar00runnerdockername: Publish to PyPI on: push: tags: - "*" permissions: contents: read jobs: build: name: "Build dists" runs-on: "ubuntu-latest" environment: name: "publish" outputs: hashes: ${{ steps.hash.outputs.hashes }} steps: - name: "Checkout repository" uses: "actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3" - name: "Setup Python" uses: "actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b" with: python-version: "3.x" - name: "Install dependencies" run: python -m pip install build - name: "Build dists" run: | SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) \ python -m build - name: "Generate hashes" id: hash run: | cd dist && echo "::set-output name=hashes::$(sha256sum * | base64 -w0)" - name: "Upload dists" uses: "actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce" with: name: "dist" path: "dist/" if-no-files-found: error retention-days: 5 provenance: needs: [build] permissions: actions: read contents: write id-token: write # Needed to access the workflow's OIDC identity. uses: "slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.5.0" with: base64-subjects: "${{ needs.build.outputs.hashes }}" upload-assets: true compile-generator: true # Workaround for https://github.com/slsa-framework/slsa-github-generator/issues/1163 publish: name: "Publish to PyPI" if: startsWith(github.ref, 'refs/tags/') needs: ["build", "provenance"] permissions: contents: write id-token: write runs-on: "ubuntu-latest" steps: - name: "Download dists" uses: "actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a" with: name: "dist" path: "dist/" - name: "Publish dists to PyPI" uses: "pypa/gh-action-pypi-publish@48b317d84d5f59668bb13be49d1697e36b3ad009" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/.gitignore0000644000175100001770000000245314562147542014255 0ustar00runnerdocker# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST pip-wheel-metadata/ # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json monkeytype.sqlite3 mypy/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/.isort.cfg0000644000175100001770000000024114562147542014155 0ustar00runnerdocker[settings] profile=black force_single_line=True single_line_exclusions=typing default_section=THIRDPARTY known_third_party=build known_first_party=twine,helpers ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/.readthedocs.yaml0000644000175100001770000000112014562147542015502 0ustar00runnerdocker# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 sphinx: configuration: docs/conf.py fail_on_warning: true formats: - htmlzip - pdf - epub build: os: ubuntu-22.04 tools: python: "3.11" python: # Install twine first, because RTD uses `--upgrade-strategy eager`, # which installs the latest version of docutils via readme_renderer. # However, Sphinx 4.2.0 requires docutils>=0.14,<0.18. install: - method: pip path: . - requirements: docs/requirements.txt ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/AUTHORS0000644000175100001770000000277714562147542013346 0ustar00runnerdocker# A list of people who have contributed to twine in order of their first # contribution. # # Uses the format of ``Name (url)`` with the ``(url)`` # being optional. Donald Stufft (https://caremad.io/) Jannis Leidel Ralf Schmitt Ian Cordasco Marc Abramowitz (http://marc-abramowitz.com/) Tom Myers Rodrigue Cloutier Tyrel Souza (https://tyrelsouza.com) Adam Talsma Jens Diemer (http://jensdiemer.de/) Andrew Watts Anna Martelli Ravenscroft Sumana Harihareswara Dustin Ingram (https://di.codes) Jesse Jarzynka (https://www.jessejoe.com/) László Kiss Kollár Frances Hocutt Tathagata Dasgupta Wasim Thabraze Varun Kamath Brian Rutledge Peter Stensmyr (http://www.peterstensmyr.com) Felipe Mulinari Rocha Campos Devesh Kumar Singh Yesha Maggi Cyril de Catheu (https://catheu.tech/) Thomas Miedema Hugo van Kemenade (https://github.com/hugovk) Jacob Woliver (jmw.sh) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/LICENSE0000644000175100001770000002273714562147542013301 0ustar00runnerdocker Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707659114.0697205 twine-5.0.0/PKG-INFO0000644000175100001770000000635414562147552013367 0ustar00runnerdockerMetadata-Version: 2.1 Name: twine Version: 5.0.0 Summary: Collection of utilities for publishing packages on PyPI Home-page: https://twine.readthedocs.io/ Author: Donald Stufft and individual contributors Author-email: donald@stufft.io Project-URL: Source, https://github.com/pypa/twine/ Project-URL: Documentation, https://twine.readthedocs.io/en/latest/ Project-URL: Packaging tutorial, https://packaging.python.org/tutorials/packaging-projects/ Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Natural Language :: English Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: POSIX Classifier: Operating System :: POSIX :: BSD Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Python: >=3.8 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: pkginfo>=1.8.1 Requires-Dist: readme-renderer>=35.0 Requires-Dist: requests>=2.20 Requires-Dist: requests-toolbelt!=0.9.0,>=0.8.0 Requires-Dist: urllib3>=1.26.0 Requires-Dist: importlib-metadata>=3.6 Requires-Dist: keyring>=15.1 Requires-Dist: rfc3986>=1.4.0 Requires-Dist: rich>=12.0.0 .. image:: https://img.shields.io/pypi/v/twine.svg :target: https://pypi.org/project/twine .. image:: https://img.shields.io/pypi/pyversions/twine.svg :target: https://pypi.org/project/twine .. image:: https://img.shields.io/readthedocs/twine :target: https://twine.readthedocs.io .. image:: https://img.shields.io/github/actions/workflow/status/pypa/twine/main.yml?branch=main :target: https://github.com/pypa/twine/actions twine ===== Twine is a utility for `publishing`_ Python packages on `PyPI`_. It provides build system independent uploads of source and binary `distribution artifacts `_ for both new and existing `projects`_. See our `documentation`_ for a description of features, installation and usage instructions, and links to additional resources. Contributing ------------ See our `developer documentation`_ for how to get started, an architectural overview, and our future development plans. Code of Conduct --------------- Everyone interacting in the Twine project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. .. _`publishing`: https://packaging.python.org/tutorials/packaging-projects/ .. _`PyPI`: https://pypi.org .. _`distributions`: https://packaging.python.org/glossary/#term-Distribution-Package .. _`projects`: https://packaging.python.org/glossary/#term-Project .. _`documentation`: https://twine.readthedocs.io/ .. _`developer documentation`: https://twine.readthedocs.io/en/latest/contributing.html .. _`PSF Code of Conduct`: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/README.rst0000644000175100001770000000313014562147542013745 0ustar00runnerdocker.. image:: https://img.shields.io/pypi/v/twine.svg :target: https://pypi.org/project/twine .. image:: https://img.shields.io/pypi/pyversions/twine.svg :target: https://pypi.org/project/twine .. image:: https://img.shields.io/readthedocs/twine :target: https://twine.readthedocs.io .. image:: https://img.shields.io/github/actions/workflow/status/pypa/twine/main.yml?branch=main :target: https://github.com/pypa/twine/actions twine ===== Twine is a utility for `publishing`_ Python packages on `PyPI`_. It provides build system independent uploads of source and binary `distribution artifacts `_ for both new and existing `projects`_. See our `documentation`_ for a description of features, installation and usage instructions, and links to additional resources. Contributing ------------ See our `developer documentation`_ for how to get started, an architectural overview, and our future development plans. Code of Conduct --------------- Everyone interacting in the Twine project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. .. _`publishing`: https://packaging.python.org/tutorials/packaging-projects/ .. _`PyPI`: https://pypi.org .. _`distributions`: https://packaging.python.org/glossary/#term-Distribution-Package .. _`projects`: https://packaging.python.org/glossary/#term-Project .. _`documentation`: https://twine.readthedocs.io/ .. _`developer documentation`: https://twine.readthedocs.io/en/latest/contributing.html .. _`PSF Code of Conduct`: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707659114.0577204 twine-5.0.0/changelog/0000755000175100001770000000000014562147552014211 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/changelog/.gitignore0000644000175100001770000000001414562147542016173 0ustar00runnerdocker!.gitignore ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707659114.0617206 twine-5.0.0/docs/0000755000175100001770000000000014562147552013212 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/changelog.rst0000644000175100001770000005553714562147542015711 0ustar00runnerdocker========= Changelog ========= This project follows the `semantic versioning `_ and `pre-release versioning `_ schemes recommended by the Python Packaging Authority. .. Do *NOT* add changelog entries here! This changelog is managed by towncrier and is built at release time. See https://twine.readthedocs.io/en/latest/contributing.html#changelog-entries for details. .. towncrier release notes start Twine 5.0.0 (2024-02-10) ------------------------ Bugfixes ^^^^^^^^ - Use ``email.message`` instead of ``cgi`` as ``cgi`` has been deprecated (`#969 `_) Misc ^^^^ - `#931 `_, `#991 `_, `#1028 `_, `#1040 `_ Twine 4.0.2 (2022-11-30) ------------------------ Bugfixes ^^^^^^^^ - Remove deprecated function to fix ``twine check`` with pkginfo 1.9.0. (`#941 `_) Twine 4.0.1 (2022-06-01) ------------------------ Bugfixes ^^^^^^^^ - Improve logging when keyring fails. (`#890 `_) - Reconfigure root logger to show all log messages. (`#896 `_) Twine 4.0.0 (2022-03-31) ------------------------ Features ^^^^^^^^ - Drop support for Python 3.6. (`#869 `_) - Use Rich to add color to ``upload`` output. (`#851 `_) - Use Rich to add color to ``check`` output. (`#874 `_) - Use Rich instead of tqdm for upload progress bar. (`#877 `_) Bugfixes ^^^^^^^^ - Remove Twine's dependencies from the ``User-Agent`` header when uploading. (`#871 `_) - Improve detection of disabled BLAKE2 hashing due to FIPS mode. (`#879 `_) - Restore warning for missing ``long_description``. (`#887 `_) Twine 3.8.0 (2022-02-02) ------------------------ Features ^^^^^^^^ - Add ``--verbose`` logging for querying keyring credentials. (`#849 `_) - Log all upload responses with ``--verbose``. (`#859 `_) - Show more helpful error message for invalid metadata. (`#861 `_) Bugfixes ^^^^^^^^ - Require a recent version of urllib3. (`#858 `_) Twine 3.7.1 (2021-12-07) ------------------------ Improved Documentation ^^^^^^^^^^^^^^^^^^^^^^ - Fix broken link to packaging tutorial. (`#844 `_) Twine 3.7.0 (2021-12-01) ------------------------ Features ^^^^^^^^ - Add support for core metadata version 2.2, defined in PEP 643. (`#833 `_) Twine 3.6.0 (2021-11-10) ------------------------ Features ^^^^^^^^ - Add support for Python 3.10. (`#827 `_) Twine 3.5.0 (2021-11-02) ------------------------ Features ^^^^^^^^ - Show more helpful messages for invalid passwords. (`#815 `_) - Allow the ``--skip-existing`` option to work with GCP Artifact Registry. (`#823 `_) Bugfixes ^^^^^^^^ - Add a helpful error message when an upload fails due to missing a trailing slash in the URL. (`#812 `_) - Generalize ``--verbose`` suggestion when an upload fails. (`#817 `_) Twine 3.4.2 (2021-07-20) ------------------------ Bugfixes ^^^^^^^^ - Improve error message for unsupported metadata. (`#755 `_) - Improve error message for a missing config file. (`#770 `_) - Do not include md5_digest or blake2_256_digest if FIPS mode is enabled on the host. This removes those fields from the metadata before sending the metadata to the repository. (`#776 `_) Twine 3.4.1 (2021-03-16) ------------------------ Bugfixes ^^^^^^^^ - Fix a regression that was causing some namespace packages with dots in them fail to upload to PyPI. (`#745 `_) Twine 3.4.0 (2021-03-15) ------------------------ Features ^^^^^^^^ - Prefer importlib.metadata for entry point handling. (`#728 `_) - Rely on importlib_metadata 3.6 for nicer entry point processing. (`#732 `_) - Eliminate dependency on setuptools/pkg_resources and replace with packaging and importlib_metadata. (`#736 `_) Twine 3.3.0 (2020-12-23) ------------------------ Features ^^^^^^^^ - Print files to be uploaded using ``upload --verbose`` (`#670 `_) - Print configuration file location when using ``upload --verbose`` (`#675 `_) - Print source and values of credentials when using ``upload --verbose`` (`#685 `_) - Add support for Python 3.9 (`#708 `_) - Turn warnings into errors when using ``check --strict`` (`#715 `_) Bugfixes ^^^^^^^^ - Make password optional when using ``upload --client-cert`` (`#678 `_) - Support more Nexus versions with ``upload --skip-existing`` (`#693 `_) - Support Gitlab Enterprise with ``upload --skip-existing`` (`#698 `_) - Show a better error message for malformed files (`#714 `_) Improved Documentation ^^^^^^^^^^^^^^^^^^^^^^ - Adopt PSF code of conduct (`#680 `_) - Adopt towncrier for the changleog (`#718 `_) Twine 3.2.0 (2020-06-24) ------------------------ Features ^^^^^^^^ - Improve display of HTTP errors during upload (`#666 `_) - Print packages and signatures to be uploaded when using ``--verbose`` option (`#652 `_) - Use red text when printing errors on the command line (`#649 `_) - Require repository URL scheme to be ``http`` or ``https`` (`#602 `_) - Add type annotations, checked with mypy, with :pep:`561` support for users of Twine's API (`#231 `_) Bugfixes ^^^^^^^^ - Update URL to ``.pypirc`` specification (`#655 `_) - Don't raise an exception when Python version can't be parsed from filename (`#612 `_) - Fix inaccurate retry message during ``upload`` (`#611 `_) - Clarify error messages for archive format (`#601 `_) Twine 3.1.1 (2019-11-27) ------------------------ Bugfixes ^^^^^^^^ - Restore ``--non-interactive`` as a flag not expecting an argument. (`#548 `_) Twine 3.1.0 (2019-11-23) ------------------------ Features ^^^^^^^^ - Add support for specifying ``--non-interactive`` as an environment variable. (`#547 `_) Twine 3.0.0 (2019-11-18) ------------------------ Features ^^^^^^^^ - When a client certificate is indicated, all password processing is disabled. (`#336 `_) - Add ``--non-interactive`` flag to abort upload rather than interactively prompt if credentials are missing. (`#489 `_) - Twine now unconditionally requires the keyring library and no longer supports uninstalling ``keyring`` as a means to disable that functionality. Instead, use ``keyring --disable`` keyring functionality if necessary. (`#524 `_) - Add Python 3.8 to classifiers. (`#518 `_) Bugfixes ^^^^^^^^ - More robust handling of server response in ``--skip-existing`` (`#332 `_) Twine 2.0.0 (2019-09-24) ------------------------ Features ^^^^^^^^ - Twine now requires Python 3.6 or later. Use pip 9 or pin to "twine<2" to install twine on older Python versions. (`#437 `_) Bugfixes ^^^^^^^^ - Require requests 2.20 or later to avoid reported security vulnerabilities in earlier releases. (`#491 `_) Twine 1.15.0 (2019-09-17) ------------------------- Features ^^^^^^^^ - Improved output on ``check`` command: Prints a message when there are no distributions given to check. Improved handling of errors in a distribution's markup, avoiding messages flowing through to the next distribution's errors. (`#488 `_) Twine 1.14.0 (2019-09-06) ------------------------- Features ^^^^^^^^ - Show Warehouse URL after uploading a package (`#459 `_) - Better error handling and gpg2 fallback if gpg not available. (`#456 `_) - Now provide a more meaningful error on redirect during upload. (`#310 `_) Bugfixes ^^^^^^^^ - Fail more gracefully when encountering bad metadata (`#341 `_) Twine 1.13.0 (2019-02-13) ------------------------- Features ^^^^^^^^ - Add disable_progress_bar option to disable tqdm. (`#427 `_) - Allow defining an empty username and password in .pypirc. (`#426 `_) - Support keyring.get_credential. (`#419 `_) - Support keyring.get_username_and_password. (`#418 `_) - Add Python 3.7 to classifiers. (`#416 `_) Bugfixes ^^^^^^^^ - Restore prompts while retaining support for suppressing prompts. (`#452 `_) - Avoid requests-toolbelt to 0.9.0 to prevent attempting to use openssl when it isn't available. (`#447 `_) - Use io.StringIO instead of StringIO. (`#444 `_) - Only install pyblake2 if needed. (`#441 `_) - Use modern Python language features. (`#436 `_) - Specify python_requires in setup.py (`#435 `_) - Use https URLs everywhere. (`#432 `_) - Fix --skip-existing for Nexus Repos. (`#428 `_) - Remove unnecessary usage of readme_render.markdown. (`#421 `_) - Don't crash if there's no package description. (`#412 `_) - Fix keyring support. (`#408 `_) Misc ^^^^ - Refactor tox env and travis config. (`#439 `_) Twine 1.12.1 (2018-09-24) ------------------------- Bugfixes ^^^^^^^^ - Fix regression with upload exit code (`#404 `_) Twine 1.12.0 (2018-09-24) ------------------------- Features ^^^^^^^^ - Add ``twine check`` command to check long description (`#395 `_) - Drop support for Python 3.3 (`#392 `_) - Empower ``--skip-existing`` for Artifactory repositories (`#363 `_) Bugfixes ^^^^^^^^ - Avoid MD5 when Python is compiled in FIPS mode (`#367 `_) Twine 1.11.0 (2018-03-19) ------------------------- Features ^^^^^^^^ - Remove PyPI as default ``register`` package index. (`#320 `_) - Support Metadata 2.1 (:pep:`566`), including Markdown for ``description`` fields. (`#319 `_) Bugfixes ^^^^^^^^ - Raise exception if attempting upload to deprecated legacy PyPI URLs. (`#322 `_) - Avoid uploading to PyPI when given alternate repository URL, and require ``http://`` or ``https://`` in ``repository_url``. (`#269 `_) Misc ^^^^ - `Update PyPI URLs `_. (`#318 `_) - Add new maintainer, release checklists. (`#314 `_) - Add instructions on how to use keyring. (`#277 `_) Twine 1.10.0 (2018-03-07) ------------------------- Features ^^^^^^^^ - Link to changelog from ``README`` (`#46 `_) - Reorganize & improve user & developer documentation. (`#304 `_) - Revise docs predicting future of ``twine`` (`#303 `_) - Add architecture overview to docs (`#296 `_) - Add doc building instructions (`#295 `_) - Declare support for Python 3.6 (`#257 `_) - Improve progressbar (`#256 `_) Bugfixes ^^^^^^^^ - Degrade gracefully when keyring is unavailable (`#315 `_) - Fix changelog formatting (`#299 `_) - Fix syntax highlighting in ``README`` (`#298 `_) - Fix Read the Docs, tox, Travis configuration (`#297 `_) - Fix Travis CI and test configuration (`#286 `_) - Print progress to ``stdout``, not ``stderr`` (`#268 `_) - Fix ``--repository[-url]`` help text (`#265 `_) - Remove obsolete registration guidance (`#200 `_) Twine 1.9.1 (2017-05-27) ------------------------ Bugfixes ^^^^^^^^ - Blacklist known bad versions of Requests. (`#253 `_) Twine 1.9.0 (2017-05-22) ------------------------ Bugfixes ^^^^^^^^ - Twine sends less information about the user's system in the User-Agent string. (`#229 `_) - Fix ``--skip-existing`` when used to upload a package for the first time. (`#220 `_) - Fix precedence of ``--repository-url`` over ``--repository``. (`#206 `_) Misc ^^^^ - Twine will now resolve passwords using the `keyring `_ if available. Module can be required with the ``keyring`` extra. - Twine will use ``hashlib.blake2b`` on Python 3.6+ instead of pyblake2 Twine 1.8.1 (2016-08-09) ------------------------ Misc ^^^^ - Check if a package exists if the URL is one of: * ``https://pypi.python.org/pypi/`` * ``https://upload.pypi.org/`` * ``https://upload.pypi.io/`` This helps people with ``https://upload.pypi.io`` still in their :file:`.pypirc` file. Twine 1.8.0 (2016-08-08) ------------------------ Features ^^^^^^^^ - Switch from upload.pypi.io to upload.pypi.org. (`#201 `_) - Retrieve configuration from the environment as a default. (`#144 `_) * Repository URL will default to ``TWINE_REPOSITORY`` * Username will default to ``TWINE_USERNAME`` * Password will default to ``TWINE_PASSWORD`` - Allow the Repository URL to be provided on the command-line (``--repository-url``) or via an environment variable (``TWINE_REPOSITORY_URL``). (`#166 `_) - Generate Blake2b 256 digests for packages *if* ``pyblake2`` is installed. Users can use ``python -m pip install twine[with-blake2]`` to have ``pyblake2`` installed with Twine. (`#171 `_) Misc ^^^^ - Generate SHA256 digest for all packages by default. - Stop testing on Python 2.6. - Warn users if they receive a 500 error when uploading to ``*pypi.python.org`` (`#199 `_) Twine 1.7.4 (2016-07-09) ------------------------ Bugfixes ^^^^^^^^ - Correct a packaging error. Twine 1.7.3 (2016-07-08) ------------------------ Bugfixes ^^^^^^^^ - Fix uploads to instances of pypiserver using ``--skip-existing``. We were not properly checking the return status code on the response after attempting an upload. (`#195 `_) Misc ^^^^ - Avoid attempts to upload a package if we can find it on Legacy PyPI. Twine 1.7.2 (2016-07-05) ------------------------ Bugfixes ^^^^^^^^ - Fix issue where we were checking the existence of packages even if the user didn't specify ``--skip-existing``. (`#189 `_) (`#191 `_) Twine 1.7.1 (2016-07-05) ------------------------ Bugfixes ^^^^^^^^ - Clint was not specified in the wheel metadata as a dependency. (`#187 `_) Twine 1.7.0 (2016-07-04) ------------------------ Features ^^^^^^^^ - Support ``--cert`` and ``--client-cert`` command-line flags and config file options for feature parity with pip. This allows users to verify connections to servers other than PyPI (e.g., local package repositories) with different certificates. (`#142 `_) - Add progress bar to uploads. (`#152 `_) - Allow ``--skip-existing`` to work for 409 status codes. (`#162 `_) - Implement retries when the CDN in front of PyPI gives us a 5xx error. (`#167 `_) - Switch Twine to upload to pypi.io instead of pypi.python.org. (`#177 `_) Bugfixes ^^^^^^^^ - Allow passwords to have ``%``\ s in them. (`#186 `_) Twine 1.6.5 (2015-12-16) ------------------------ Bugfixes ^^^^^^^^ - Bump requests-toolbelt version to ensure we avoid ConnectionErrors (`#155 `_) Twine 1.6.4 (2015-10-27) ------------------------ Bugfixes ^^^^^^^^ - Paths with hyphens in them break the Wheel regular expression. (`#145 `_) - Exception while accessing the ``repository`` key (sic) when raising a redirect exception. (`#146 `_) Twine 1.6.3 (2015-10-05) ------------------------ Bugfixes ^^^^^^^^ - Fix uploading signatures causing a 500 error after large file support was added. (`#137 `_, `#140 `_) Twine 1.6.2 (2015-09-28) ------------------------ Bugfixes ^^^^^^^^ - Upload signatures with packages appropriately (`#132 `_) As part of the refactor for the 1.6.0 release, we were using the wrong name to find the signature file. This also uncovered a bug where if you're using twine in a situation where ``*`` is not expanded by your shell, we might also miss uploading signatures to PyPI. Both were fixed as part of this. Twine 1.6.1 (2015-09-18) ------------------------ Bugfixes ^^^^^^^^ - Fix signing support for uploads (`#130 `_) Twine 1.6.0 (2015-09-14) ------------------------ Features ^^^^^^^^ - Allow the user to specify the location of their :file:`.pypirc` (`#97 `_) - Support registering new packages with ``twine register`` (`#8 `_) - Add the ``--skip-existing`` flag to ``twine upload`` to allow users to skip releases that already exist on PyPI. (`#115 `_) - Upload wheels first to PyPI (`#106 `_) - Large file support via the ``requests-toolbelt`` (`#104 `_) Bugfixes ^^^^^^^^ - Raise an exception on redirects (`#92 `_) - Work around problems with Windows when using ``getpass.getpass`` (`#116 `_) - Warnings triggered by pkginfo searching for ``PKG-INFO`` files should no longer be user visible. (`#114 `_) - Provide more helpful messages if :file:`.pypirc` is out of date. (`#111 `_) Twine 1.5.0 (2015-03-10) ------------------------ Features ^^^^^^^^ - Support commands not named "gpg" for signing (`#29 `_) Bugfixes ^^^^^^^^ - Display information about the version of setuptools installed (`#85 `_) - Support deprecated pypirc file format (`#61 `_) Misc ^^^^ - Add lower-limit to requests dependency Twine 1.4.0 (2014-12-12) ------------------------ Features ^^^^^^^^ - Switch to a git style dispatching for the commands to enable simpler commands and programmatic invocation. (`#6 `_) - Parse :file:`~/.pypirc` ourselves and use ``subprocess`` instead of the ``distutils.spawn`` module. (`#13 `_) Bugfixes ^^^^^^^^ - Expand globs and check for existence of dists to upload (`#65 `_) - Fix issue uploading packages with ``_``\ s in the name (`#47 `_) - List registered commands in help text (`#34 `_) - Use ``pkg_resources`` to load registered commands (`#32 `_) - Prevent ResourceWarning from being shown (`#28 `_) - Add support for uploading Windows installers (`#26 `_) Twine 1.3.0 (2014-03-31) ------------------------ Features ^^^^^^^^ - Additional functionality. Twine 1.2.2 (2013-10-03) ------------------------ Features ^^^^^^^^ - Basic functionality. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/conf.py0000644000175100001770000002264714562147542014523 0ustar00runnerdocker# twine documentation build configuration file, created by # sphinx-quickstart on Tue Aug 13 11:51:54 2013. # # This file is execfile()d with the current directory set to its containing # dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # import sys # import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # sys.path.insert(0, os.path.abspath(os.pardir)) import twine # -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = "1.7.0" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.intersphinx", "sphinx.ext.coverage", "sphinx.ext.viewcode", "sphinxcontrib.programoutput", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "twine" copyright = "2019, Donald Stufft and individual contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = ".".join(twine.__version__.split(".")[:2]) # The full version, including alpha/beta/rc tags. release = twine.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. # pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "furo" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "twinedoc" # -- Options for LaTeX output ------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]) latex_documents = [ ( "index", "twine.tex", "Twine Documentation", "Donald Stufft and individual contributors", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( "index", "twine", "twine Documentation", ["Donald Stufft", "Individual contributors"], 1, ), ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ----------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "twine", "twine Documentation", "Donald Stufft and individual contributors", "twine", "One line description of project.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # See https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-linkcheck_ignore linkcheck_ignore = [ r"https?://127\.0\.0\.1.*", # Avoid errors due to GitHub rate limit # https://github.com/sphinx-doc/sphinx/issues/7388 r"https://github\.com/pypa/twine/issues/.*", # Avoid errors from channels interpreted as anchors r"https://web\.libera\.chat/#", # Avoid error from InvalidPyPIUploadURL docstring r"https://upload\.pypi\.org/legacy/?", # Avoid errors from 403/Login Redirects r"https://(test\.)?pypi\.org/manage/project/twine/collaboration/?", r"https://pypi\.org/manage/project/twine/collaboration/?", ] intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "requests": ("https://requests.readthedocs.io/en/latest/", None), } # Be strict about the invalid references: nitpicky = True # TODO: Try to add these to intersphinx_mapping nitpick_ignore_regex = [ (r"py:.*", r"pkginfo.*"), ] # -- Options for apidoc output ------------------------------------------------ autodoc_default_options = { "members": True, "private-members": True, "undoc-members": True, "member-order": "bysource", } autodoc_class_signature = "separated" autodoc_preserve_defaults = True # autodoc_typehints = "both" # autodoc_typehints_description_target = "documented" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/contributing.rst0000644000175100001770000002310314562147542016451 0ustar00runnerdockerContributing ============ We are happy you have decided to contribute to Twine. Please see `the GitHub repository`_ for code and more documentation, and the `official Python Packaging User Guide`_ for user documentation. To ask questions or get involved, you can join the `Python Packaging Discourse forum`_, ``#pypa`` or ``#pypa-dev`` on `IRC`_, or the `distutils-sig mailing list`_. Everyone interacting in the Twine project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. Getting started --------------- We use `tox`_ to run tests, check code style, and build the documentation. To install ``tox``, run: .. code-block:: bash python3 -m pip install tox Clone the twine repository from GitHub, then run: .. code-block:: bash cd /path/to/your/local/twine tox -e dev This creates a `virtual environment`_, so that twine and its dependencies do not interfere with other packages installed on your machine. In the virtual environment, ``twine`` is pointing at your local copy, so when you make changes, you can easily see their effect. The virtual environment also contains the tools for running tests and checking code style, so you can run them on single files directly or in your code editor. However, we still encourage using the ``tox`` commands below on the whole codebase. To use the virtual environment, run: .. code-block:: bash source venv/bin/activate Building the documentation ^^^^^^^^^^^^^^^^^^^^^^^^^^ Additions and edits to twine's documentation are welcome and appreciated. To preview the docs while you're making changes, run: .. code-block:: bash tox -e watch-docs Then open a web browser to ``_. When you're done making changes, lint and build the docs locally before making a pull request. In your active virtual environment, run: .. code-block:: bash tox -e docs The HTML of the docs will be written to :file:`docs/_build/html`. Code style ^^^^^^^^^^ To automatically reformat your changes with `isort`_ and `black`_, run: .. code-block:: bash tox -e format To detect any remaining code smells with `flake8`_, run: .. code-block:: bash tox -e lint To perform strict type-checking using `mypy`_, run: .. code-block:: bash tox -e types Any errors from ``lint`` or ``types`` need to be fixed manually. Additionally, we prefer that ``import`` statements be used for packages and modules only, rather than individual classes or functions. Testing ^^^^^^^ We use `pytest`_ for writing and running tests. To run the tests in your virtual environment, run: .. code-block:: bash tox -e py To pass options to ``pytest``, e.g. the name of a test, run: .. code-block:: bash tox -e py -- tests/test_upload.py::test_exception_for_http_status Twine is continuously tested against supported versions of Python using `GitHub Actions`_. To run the tests against a specific version, e.g. Python 3.8, you will need it installed on your machine. Then, run: .. code-block:: bash tox -e py38 To run the "integration" tests of uploading to real package indexes, run: .. code-block:: bash tox -e integration To run the tests against all supported Python versions, check code style, and build the documentation, run: .. code-block:: bash tox Submitting changes ------------------ 1. Fork `the GitHub repository`_. 2. Make a branch off of ``main`` and commit your changes to it. 3. Run the tests, check code style, and build the docs as described above. 4. Optionally, add your name to the end of the :file:`AUTHORS` file using the format ``Name (url)``, where the ``(url)`` portion is optional. 5. Submit a pull request to the ``main`` branch on GitHub, referencing an open issue. 6. Add a changelog entry. Changelog entries ^^^^^^^^^^^^^^^^^ The ``docs/changelog.rst`` file is built by `towncrier`_ from files in the ``changelog/`` directory. To add an entry, create a file in that directory named ``{number}.{type}.rst``, where ``{number}`` is the pull request number, and ``{type}`` is ``feature``, ``bugfix``, ``doc``, ``removal``, or ``misc``. For example, if your PR number is 1234 and it's fixing a bug, then you would create ``changelog/1234.bugfix.rst``. PRs can span multiple categories by creating multiple files: if you added a feature and deprecated/removed an old feature in PR #5678, you would create ``changelog/5678.feature.rst`` and ``changelog/5678.removal.rst``. A changelog entry is meant for end users and should only contain details relevant to them. In order to maintain a consistent style, please keep the entry to the point, in sentence case, shorter than 80 characters, and in an imperative tone. An entry should complete the sentence "This change will ...". If one line is not enough, use a summary line in an imperative tone, followed by a description of the change in one or more paragraphs, each wrapped at 80 characters and separated by blank lines. You don't need to reference the pull request or issue number in a changelog entry, since towncrier will add a link using the number in the file name, and the pull request should reference an issue number. Similarly, you don't need to add your name to the entry, since that will be associated with the pull request. Changelog entries are rendered using `reStructuredText`_, but they should only have minimal formatting (such as ````monospaced text````). .. _`towncrier`: https://pypi.org/project/towncrier/ .. _`reStructuredText`: https://www.writethedocs.org/guide/writing/reStructuredText/ Architectural overview ---------------------- Twine is a command-line tool for interacting with PyPI securely over HTTPS. Its three purposes are to be: 1. A user-facing tool for publishing on pypi.org 2. A user-facing tool for publishing on other Python package indexes (e.g., ``devpi`` instances) 3. A useful API for other programs (e.g., ``zest.releaser``) to call for publishing on any Python package index Currently, twine has two principle functions: uploading new packages and registering new `projects`_ (``register`` is no longer supported on PyPI, and is in Twine for use with other package indexes). Its command line arguments are parsed in :file:`twine/cli.py`. The code for registering new projects is in :file:`twine/commands/register.py`, and the code for uploading is in :file:`twine/commands/upload.py`. The file :file:`twine/package.py` contains a single class, ``PackageFile``, which hashes the project files and extracts their metadata. The file :file:`twine/repository.py` contains the ``Repository`` class, whose methods control the URL the package is uploaded to (which the user can specify either as a default, in the :file:`.pypirc` file, or pass on the command line), and the methods that upload the package securely to a URL. For more details, refer to the source documentation (currently a `work in progress `_): .. toctree:: internal/twine Where Twine gets configuration and credentials ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A user can set the repository URL, username, and/or password via command line, ``.pypirc`` files, environment variables, and ``keyring``. Adding a maintainer ------------------- A checklist for adding a new maintainer to the project. #. Add them as a Member in the GitHub repo settings. #. Get them Test PyPI and canon PyPI usernames and add them as a Maintainer on `our Test PyPI project `_ and `canon PyPI `_. Making a new release -------------------- A checklist for creating, testing, and distributing a new version. #. Choose a version number, and create a new branch .. code-block:: bash VERSION=3.4.2 git switch -c release-$VERSION #. Update :file:`docs/changelog.rst` .. code-block:: bash tox -e changelog -- --version $VERSION git commit -am "Update changelog for $VERSION" #. Open a pull request for review #. Merge the pull request, and ensure the `GitHub Actions`_ build passes #. Create a new git tag for the version .. code-block:: bash git switch main git pull --ff-only upstream main git tag -m "Release v$VERSION" $VERSION #. Push to start the release, and watch it in `GitHub Actions`_ .. code-block:: bash git push upstream $VERSION #. View the new release on `PyPI`_ Future development ------------------ See our `open issues`_. In the future, ``pip`` and ``twine`` may merge into a single tool; see `ongoing discussion `_. .. _`official Python Packaging User Guide`: https://packaging.python.org/tutorials/packaging-projects/ .. _`the GitHub repository`: https://github.com/pypa/twine .. _`Python Packaging Discourse forum`: https://discuss.python.org/c/packaging/ .. _`IRC`: https://web.libera.chat/#pypa-dev,#pypa .. _`distutils-sig mailing list`: https://mail.python.org/mailman3/lists/distutils-sig.python.org/ .. _`PSF Code of Conduct`: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md .. _`virtual environment`: https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/ .. _`tox`: https://tox.readthedocs.io/ .. _`pytest`: https://docs.pytest.org/ .. _`GitHub Actions`: https://github.com/pypa/twine/actions .. _`isort`: https://timothycrosley.github.io/isort/ .. _`black`: https://black.readthedocs.io/ .. _`flake8`: https://flake8.pycqa.org/ .. _`mypy`: https://mypy.readthedocs.io/ .. _`projects`: https://packaging.python.org/glossary/#term-Project .. _`open issues`: https://github.com/pypa/twine/issues .. _`PyPI`: https://pypi.org/project/twine/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/index.rst0000644000175100001770000002120514562147542015052 0ustar00runnerdocker.. twine documentation master file, originally created by sphinx-quickstart on Tue Aug 13 11:51:54 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. .. toctree:: :hidden: :maxdepth: 3 changelog contributing Code of Conduct PyPI Project GitHub Repository Python Packaging Tutorial Twine ===== Twine is a utility for `publishing`_ Python packages to `PyPI`_ and other `repositories`_. It provides build system independent uploads of source and binary `distribution artifacts `_ for both new and existing `projects`_. Why Should I Use This? ---------------------- The goal of Twine is to improve PyPI interaction by improving security and testability. The biggest reason to use Twine is that it securely authenticates you to PyPI over HTTPS using a verified connection, regardless of the underlying Python version. Meanwhile, ``python setup.py upload`` will only work correctly and securely if your build system, Python version, and underlying operating system are configured properly. Secondly, Twine encourages you to build your distribution files. ``python setup.py upload`` only allows you to upload a package as a final step after building with ``distutils`` or ``setuptools``, within the same command invocation. This means that you cannot test the exact file you're going to upload to PyPI to ensure that it works before uploading it. Finally, Twine allows you to pre-sign your files and pass the ``.asc`` files into the command line invocation (``twine upload myproject-1.0.1.tar.gz myproject-1.0.1.tar.gz.asc``). This enables you to be assured that you're typing your ``gpg`` passphrase into ``gpg`` itself and not anything else, since *you* will be the one directly executing ``gpg --detach-sign -a ``. Features -------- - Verified HTTPS connections - Uploading doesn't require executing ``setup.py`` - Uploading files that have already been created, allowing testing of distributions before release - Supports uploading any packaging format (including `wheels`_) Installation ------------ .. code-block:: bash pip install twine Using Twine ----------- 1. Create some distributions in the normal way: .. code-block:: bash python -m build 2. Upload to `Test PyPI`_ and verify things look right: .. code-block:: bash twine upload -r testpypi dist/* Twine will prompt for your username and password. 3. Upload to `PyPI`_: .. code-block:: bash twine upload dist/* 4. Done! .. _entering-credentials: .. note:: Like many other command line tools, Twine does not show any characters when you enter your password. If you're using Windows and trying to paste your username, password, or token in the Command Prompt or PowerShell, ``Ctrl-V`` and ``Shift+Insert`` won't work. Instead, you can use "Edit > Paste" from the window menu, or enable "Use Ctrl+Shift+C/V as Copy/Paste" in "Properties". This is a `known issue `_ with Python's ``getpass`` module. More documentation on using Twine to upload packages to PyPI is in the `Python Packaging User Guide`_. Commands -------- ``twine upload`` ^^^^^^^^^^^^^^^^ Uploads one or more distributions to a repository. .. program-output:: twine upload -h ``twine check`` ^^^^^^^^^^^^^^^ Checks whether your distribution's long description will render correctly on PyPI. .. program-output:: twine check -h ``twine register`` ^^^^^^^^^^^^^^^^^^ Pre-register a name with a repository before uploading a distribution. .. warning:: Pre-registration is `not supported on PyPI`_, so the ``register`` command is only necessary if you are using a different repository that requires it. See `issue #1627 on Warehouse`_ (the software running on PyPI) for more details. .. program-output:: twine register -h Configuration ------------- Twine can read repository configuration from a ``.pypirc`` file, either in your home directory, or provided with the ``--config-file`` option. For details on writing and using ``.pypirc``, see the `specification `_ in the Python Packaging User Guide. Environment Variables ^^^^^^^^^^^^^^^^^^^^^ Twine also supports configuration via environment variables. Options passed on the command line will take precedence over options set via environment variables. Definition via environment variable is helpful in environments where it is not convenient to create a ``.pypirc`` file (for example, on a CI/build server). * ``TWINE_USERNAME`` - the username to use for authentication to the repository. * ``TWINE_PASSWORD`` - the password to use for authentication to the repository. * ``TWINE_REPOSITORY`` - the repository configuration, either defined as a section in ``.pypirc`` or provided as a full URL. * ``TWINE_REPOSITORY_URL`` - the repository URL to use. * ``TWINE_CERT`` - custom CA certificate to use for repositories with self-signed or untrusted certificates. * ``TWINE_NON_INTERACTIVE`` - Do not interactively prompt for username/password if the required credentials are missing. Proxy Support ^^^^^^^^^^^^^ Twine can be configured to use a proxy by setting environment variables. For example, to use a proxy for just the ``twine`` command, without ``export``-ing it for other tools: .. code-block:: bash HTTPS_PROXY=socks5://user:pass@host:port twine upload dist/* For more information, see the Requests documentation on :ref:`requests:proxies` and :ref:`requests:socks`, and `an in-depth article about proxy environment variables `_. Keyring Support --------------- Instead of typing in your password every time you upload a distribution, Twine allows storing a username and password securely using `keyring`_. Keyring is installed with Twine but for some systems (Linux mainly) may require `additional installation steps`_. Once Twine is installed, use the ``keyring`` program to set a username and password to use for each repository to which you may upload. For example, to set a username and password for PyPI: .. code-block:: bash keyring set https://upload.pypi.org/legacy/ your-username and enter the password when prompted. For a different repository, replace the URL with the relevant repository URL. For example, for Test PyPI, use ``https://test.pypi.org/legacy/``. The next time you run ``twine``, it will prompt you for a username, and then get the appropriate password from Keyring. .. note:: If you are using Linux in a headless environment (such as on a server) you'll need to do some additional steps to ensure that Keyring can store secrets securely. See `Using Keyring on headless systems`_. Disabling Keyring ^^^^^^^^^^^^^^^^^ In most cases, simply not setting a password with ``keyring`` will allow Twine to fall back to prompting for a password. In some cases, the presence of Keyring will cause unexpected or undesirable prompts from the backing system. In these cases, it may be desirable to disable Keyring altogether. To disable Keyring, run: .. code-block:: bash keyring --disable See `Twine issue #338`_ for discussion and background. .. _`publishing`: https://packaging.python.org/tutorials/packaging-projects/ .. _`PyPI`: https://pypi.org .. _`Test PyPI`: https://packaging.python.org/guides/using-testpypi/ .. _`pypirc`: https://packaging.python.org/specifications/pypirc/ .. _`Python Packaging User Guide`: https://packaging.python.org/tutorials/packaging-projects/ .. _`keyring`: https://pypi.org/project/keyring/ .. _`Using Keyring on headless systems`: https://keyring.readthedocs.io/en/latest/#using-keyring-on-headless-linux-systems .. _`additional installation steps`: https://pypi.org/project/keyring/#installation-linux .. _`developer documentation`: https://twine.readthedocs.io/en/latest/contributing.html .. _`projects`: https://packaging.python.org/glossary/#term-Project .. _`distributions`: https://packaging.python.org/glossary/#term-Distribution-Package .. _`repositories`: https://packaging.python.org/glossary/#term-Package-Index .. _`PSF Code of Conduct`: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md .. _`Warehouse`: https://github.com/pypa/warehouse .. _`wheels`: https://packaging.python.org/glossary/#term-Wheel .. _`not supported on PyPI`: https://packaging.python.org/guides/migrating-to-pypi-org/#registering-package-names-metadata .. _`issue #1627 on Warehouse`: https://github.com/pypa/warehouse/issues/1627 .. _`Twine issue #338`: https://github.com/pypa/twine/issues/338 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707659114.0617206 twine-5.0.0/docs/internal/0000755000175100001770000000000014562147552015026 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/internal/twine.auth.rst0000644000175100001770000000010014562147542017634 0ustar00runnerdockertwine.auth module ================= .. automodule:: twine.auth ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/internal/twine.cli.rst0000644000175100001770000000007514562147542017455 0ustar00runnerdockertwine.cli module ================ .. automodule:: twine.cli ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/internal/twine.commands.check.rst0000644000175100001770000000013614562147542021561 0ustar00runnerdockertwine.commands.check module =========================== .. automodule:: twine.commands.check ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/internal/twine.commands.register.rst0000644000175100001770000000014714562147542022332 0ustar00runnerdockertwine.commands.register module ============================== .. automodule:: twine.commands.register ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/internal/twine.commands.rst0000644000175100001770000000027114562147542020505 0ustar00runnerdockertwine.commands package ====================== .. automodule:: twine.commands .. toctree:: :maxdepth: 4 twine.commands.check twine.commands.register twine.commands.upload ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/internal/twine.commands.upload.rst0000644000175100001770000000014114562147542021764 0ustar00runnerdockertwine.commands.upload module ============================ .. automodule:: twine.commands.upload ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/internal/twine.exceptions.rst0000644000175100001770000000012214562147542021060 0ustar00runnerdockertwine.exceptions module ======================= .. automodule:: twine.exceptions ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/internal/twine.package.rst0000644000175100001770000000011114562147542020270 0ustar00runnerdockertwine.package module ==================== .. automodule:: twine.package ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/internal/twine.repository.rst0000644000175100001770000000012214562147542021116 0ustar00runnerdockertwine.repository module ======================= .. automodule:: twine.repository ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/internal/twine.rst0000644000175100001770000000037114562147542016706 0ustar00runnerdockertwine package ============= .. automodule:: twine .. toctree:: :maxdepth: 4 twine.commands twine.auth twine.cli twine.exceptions twine.package twine.repository twine.settings twine.utils twine.wheel twine.wininst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/internal/twine.settings.rst0000644000175100001770000000011414562147542020540 0ustar00runnerdockertwine.settings module ===================== .. automodule:: twine.settings ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/internal/twine.utils.rst0000644000175100001770000000010314562147542020036 0ustar00runnerdockertwine.utils module ================== .. automodule:: twine.utils ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/internal/twine.wheel.rst0000644000175100001770000000010314562147542020002 0ustar00runnerdockertwine.wheel module ================== .. automodule:: twine.wheel ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/internal/twine.wininst.rst0000644000175100001770000000011114562147542020370 0ustar00runnerdockertwine.wininst module ==================== .. automodule:: twine.wininst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/docs/requirements.txt0000644000175100001770000000033414562147542016475 0ustar00runnerdockerdoc8>=0.8.0 furo>=2021.10.09 readme-renderer>=17.4 # Remove this upper bound when twine's minimum Python is 3.9+. # See: https://github.com/sphinx-doc/sphinx/issues/11767 Sphinx>=6,<7.1 sphinxcontrib-programoutput>=0.17 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/mypy.ini0000644000175100001770000000147114562147542013763 0ustar00runnerdocker[mypy] show_traceback = True ; --strict settings warn_redundant_casts = True warn_unused_configs = True warn_unused_ignores = True ; Enabling this will fail on subclasses of untyped imports, e.g. pkginfo ; disallow_subclassing_any = True disallow_any_generics = True disallow_untyped_calls = True disallow_untyped_defs = True disallow_incomplete_defs = True check_untyped_defs = True disallow_untyped_decorators = True no_implicit_optional = True warn_return_any = True no_implicit_reexport = True strict_equality = True [mypy-requests_toolbelt,requests_toolbelt.*] ; https://github.com/requests/toolbelt/issues/279 ignore_missing_imports = True [mypy-rfc3986] ignore_missing_imports = True [mypy-urllib3] ; https://github.com/urllib3/urllib3/issues/867 ignore_missing_imports = True [mypy-tests.*] ignore_errors = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/pyproject.toml0000644000175100001770000000054314562147542015177 0ustar00runnerdocker# pyproject.toml [build-system] requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.0"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] [tool.towncrier] package = "twine" filename = "docs/changelog.rst" directory = "changelog" issue_format = "`#{issue} `_" underlines = ["-", "^"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/pytest.ini0000644000175100001770000000050614562147542014313 0ustar00runnerdocker[pytest] filterwarnings= # workaround for https://github.com/mozilla/bleach/issues/425 ignore:Using or importing the ABCs:DeprecationWarning:bleach # workaround for https://github.com/pypa/setuptools/issues/479 ignore:the imp module is deprecated::setuptools addopts = --disable-socket --ignore-glob '*integration*.py' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707659114.0697205 twine-5.0.0/setup.cfg0000644000175100001770000000333414562147552014106 0ustar00runnerdocker[metadata] license_files = LICENSE name = twine author = Donald Stufft and individual contributors author_email = donald@stufft.io description = Collection of utilities for publishing packages on PyPI long_description = file:README.rst long_description_content_type = text/x-rst url = https://twine.readthedocs.io/ project_urls = Source = https://github.com/pypa/twine/ Documentation = https://twine.readthedocs.io/en/latest/ Packaging tutorial = https://packaging.python.org/tutorials/packaging-projects/ classifiers = Intended Audience :: Developers License :: OSI Approved :: Apache Software License Natural Language :: English Operating System :: MacOS :: MacOS X Operating System :: POSIX Operating System :: POSIX :: BSD Operating System :: POSIX :: Linux Operating System :: Microsoft :: Windows 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 [options] packages = twine twine.commands python_requires = >=3.8 install_requires = pkginfo >= 1.8.1 readme-renderer >= 35.0 requests >= 2.20 requests-toolbelt >= 0.8.0, != 0.9.0 urllib3 >= 1.26.0 importlib-metadata >= 3.6 keyring >= 15.1 rfc3986 >= 1.4.0 rich >= 12.0.0 include_package_data = True [options.entry_points] twine.registered_commands = check = twine.commands.check:main upload = twine.commands.upload:main register = twine.commands.register:main console_scripts = twine = twine.__main__:main [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707659114.0657206 twine-5.0.0/tests/0000755000175100001770000000000014562147552013424 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/__init__.py0000644000175100001770000000000014562147542015522 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707659114.0657206 twine-5.0.0/tests/alt-fixtures/0000755000175100001770000000000014562147552016053 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/alt-fixtures/twine-1.5.0-py2.py3-none-any.whl0000644000175100001770000003671014562147542023326 0ustar00runnerdockerPKrjFR`{twine/__init__.py}TMo0 W<ˆ3V$EPl! K>ߏǐbrI(=>G9C{J rEgQb^Uh֝(kI'KZuz W'fV o)BM9 n;IB=Xf&Iy'-qT@LuWS1 MiVr]XT<y@tV(Eڀh Akoȑjv0,YgݫF-)[%<<xw{xZ?<6lvۛl}~dۛewg~IX~刯zd;,}Ƌ/hہMK6 rE+Q+[ʷ:P5]q:"ClG1{?;r0%WqبU9kIW!xSr ώ[ԫ3V R&t_ixNvO{x^>>.dV:'T_a}vʚqOYUAXʿBz6_ C뿊P"ůZ{d8e(œPK0d$F>_> twine/cli.pyUQ6~ϯ,}Kt7ע&T!b6FDɩ- ό'Y/ R24ynF7S3hD aV>3O4m\^ !d;5Dk\@*L{nm!7Q4Vi֘zrC *.Hx~GdWQ֠+9K6İd{ X|FZ{ ͞)$k1gꙑ*bb~Q<&>VbcXixrA0[|y He4V+˟Hr[Blb3StaB!wɁUŵm&z↙r!*JV$ycIbKem,Igҭ$}-jŅ "?hOeI *VѨGVE͔~]+/MS)Oޢ6dfF s Z4 eE~dSOo۸Jm0=r+Au?#Z) I-=f<:uI瘷~>$-6W-}0ڰw t C mSh=;cN4ǒB&Q'mO^>v-/Q2==Jx(?T뚙tl솊]4ڡRX8SESQ^Z_+YL閐eY\ݝհvޥYOm@j7?}M] t+Bӯi(X]5&L$z:'~}yY61[rmP]cSnxGe.E@d+Vior oKG_{ NXɳ&x?A| "t+W^YJ'Q~[ FPKsqEg6Itwine/utils.pyWQo6~ׯ8(Wi7%fYQȔLT5coIɒvbKw dqP"x%\˜eX2ILpG\ +0[W'WZFG_  Kر@9B 8ǘD`ya/ֆ B|0fh_SҴo)>bh$Uz9#}qz5-&535(W)yV #ÌA*`f$q+aD@∲(q_P3̷iR~/b|..f<[N' |v=]N3|z0]L?#IA E .x,c^yZC*1( MQ2T$J`JJS*ZQ| {-3s J@P"ʄe:Q̶z8RrS0]?u2y"҂)Uoj&JI58C,ݱ!, 8"rl's^b+A_D3:c@j8O5D`wqB8=*# p.}b)4CBja:c7A g;3Rx[>w +統2jJԬ^a)6M7]ݨ y1J-D1*[WۖtFd:-n|3P֮58* #DO,? w:I|#xc-23d>W $#*FJ uR_ ڂbة*;\z N{ԽB<Mn' l$kͷim쨙_PyYA;f -J&wS+ϝ?> NLanwe'<˧AgB*BI´Px?UN8;UD++^/΄58-^Fb!Vl%0\@;*NhfEY]6Ttv 5VrN{.޹Aq6qL;1r?wNv|{jMp}˭ǭrjV[PKuE; twine/wheel.pyVr6}W`fD2onTܱdR@BMZR{wAR"Ջٳ,TzgDr٥UyG#F -dVƚ՜{0V(',$9D?!NUlwL** H!,E )hDŽdZRpn)>6j895.Z9}~zlbe o/'z;YJ sc\/Qa7L xi,F̪mdɄuF,+KT 0U\xΦ{1O#0]z`b:kjv9]LfzƳrӄn` GRkZՐ\,*^+=0 f-,Ң YJ;sTFY䕫 $ W18z=ю6B:4)~"U$p`xi9P9o~}Z)ٝ ?uɖlArS=1FU"ԑARB}5a4{Ң/h[%`KW-8q展&K/'x'gv}49u\it_t68tOfP&:6Muf~| HG -iSFyևJO~KȂ~ɗ%w~oVߟ|OwiT|B%{"  fR$ -)fMz]̔|/q D?,|73 PKK?EN/UXtwine/wininst.pymTn0+akд=)iN)PieHb)_^l`;;Y27$IW2$DZGUE +a+dp ) 0z:~ + x|d-B i &' bTԄn藇Oyy}07 jxAa<_9$|E|5KXϸ {qn$<O P;SqG6JqH\7@K}¬"'걒tOg1A#,'<:/IeP尒팪4 [S{5e=ætKevayFZ*7C[ 7.,G^Teht}5dz=6.0=NI[߷8+l[ SȗNjNjm5'bXs? Z,a"9jݲ$4sHW=ԺfVS@(&Bvg>#?[ vYc'[?\ ˦7X]SZ+ROHt$s7v{ze&Fzz,scQh엏݅<pN5kpLBh;0;1H}5ixPKuE}KI:twine/commands/__init__.pyuAo0 Drvr3V8@ɐe!HDͿd@a'rh80|kg<[@m0 < 0Drn4 q8GuRD=|ȂvGoHY0g&X\-\JJ^/8 0,EjS՛/{?Z1B߉lOh Q#j(;vDKG?LHU¼pWes}sվ԰j[}dz_e&9o>LISM}Ոzw5%JCGb7g}pGh>q 4}Te$+M@'$Kub PKK$F" A!twine/commands/upload.pyko8m8iM:^-2/#8n;ÇHImz [s8{#r^7{"'/_D.ꊖ9YvVg,'m3AԆfݙߙIĈ٭h7[{RՊ .ɚ5d)92Fv\m4K$-z(`So`5ojN_v Ղ&(^Iuq~y<a5ǪdRr\ m@@’H--=U;Z`@%R jUOQN2oΖdb9|:zq$>wWŻ+X!gW/a&`Urג$5^U҂g됆-hH @[ҐѥZ[V)"t% ~ϑ4W T}@[YZr-d(B2.z~oܔ[2iڸlW3PT]%ZV|']W[eRZ) ɟSqR4wuLu+@TDdR%t, Ҭ|CnY5T]-nXEoCHjdrX^ן߃gW}hjѩ5bgvYQUCwf5NRK Lݦ"m(*՗4ŗ1 oB H;]8M۔5%x)fFlp񏯝c}45:R y@#4Ѥ-k0nsm` pŐ[V7}=h +@VUV`{R,hgG9)N;yosJ1*8Ks}N[Ź+S6"i 2YyPS(AEp~~n,9L7%F"T j]E࠽_Z Å\_>7Nb B |@_jSWb*ۡ_(lJ~ܼ:$:RQ=_^9Koi)fv؇9=4an2`x`lH%;]lxCÍa^pú/iO4SrvNn #48m7ZA@N @g((77#mw}wXUy1ѩǫc?l'Ռ^Çݱ׾'` 'u{;IkZy!HdǩNu %{Ǯꊅ;)n +tw |"#2 7I(l˒n !f ѩb 6JMGAtCW3obwɣitkj .2yj1׽/6 C6n{nD9GMAOet >/Bqv9@[` i P8,:-e uT=Q>!|p̾T9l Lf7*Pl ߱=F0zh*LͭxY 9ϡ1R6:I 9<J3e~8}/hXCsZ_wx`$ovJNF"QO(V#-qjLzDŽC~D+GcM3;:f6zVž*M4}_ac$qvh1i?3dƿV&? huwn4ǎHa%Z3 ~\46lIa(~z+cgo=,?n2o";"q@hǭ59t2#3~gS&`fƢ`yhdggnF DGK|P?I͝{Xi7Ϻ_Gu[&Zqx ?5V… QgwL \a*;7u]&=Aa|8mYO)( -cڄ9O~H`!9E%މ!Hi0'h9KF~X;, жTs= +y/j:2-Sʩ%$ <vp# YDabփq'ɴLyo߿%h)AƐ,.nսme:Vat/)cgT{.aA̟3$a:CFm|IKy뇠cyb?ba˴ď\^K8V;37a6:}7:e?X 3}l爜y^} ^e %PMkjc٧K0R I$MFF~;%5, I_DkCǐpPKۛjFїr%twine-1.5.0.dist-info/DESCRIPTION.rstWo6] $%>݂4m,a;+hȢFRqadIT@ݓ[JF/ eIPTܚڐ4"u)Y)Wx=>s=>8׵+tkmL>Qt#+WI9mS8KM]jLYg~ue)z_iZ蜮Y *ϥud">e 8+H!W 0 '-u[8HHCog&8FoHʵw&|ʂi=(*mתBFC5*R<}:iJuٕ}Nrb0v,$m񑕘Ñ!dm Z~P^>ں0 rV dPu=أ*Kvęi)ЇE)38e҉=g\^& 7 bѦq菎OctPZ wq3tp"B,CV2r?zv3l % qz-r[%òWyeBl78s29:>>ExZ8:5pen>44Wg9Wq`%oվes4O>(:[$#V϶iLoep}QWxևQ&gr'ٻ/5t5=;8w}4>N_N^N//.`[*F YC+XBwA3]nm8MY[Ϗ&nxlC ->w@5ņ"0;`} s)vknQCSfW綯BG}v3NKMXMS wi/$Wrb qWҾ)DDp/)neo{̇?G#ؒd ,n:,}@x'Wihm}R$Iޱr)T[U񾤝ƔI$"1| qc 44np2/Ch-:blHyWugYnѕ_= ፴}ytƼȯpc$ߍf 2gU?PKۛjF s0Qp&twine-1.5.0.dist-info/entry_points.txtN+I/N.,()*)KUUz񹉙yV +"ZY\ZXZ ЃC PKۛjFx6ц8#twine-1.5.0.dist-info/metadata.jsonTk0WttiA),5a`ZgG,i4ﻓ5et>ٺ{]d C6f٥ M/4l2͛  .xźR֠`ج{qӗi(eLbJ U1¤FoS⻨ t]z- @U6>cd9I#^]mC=|D?k+%~naȻN@.(3?".B~<bK ak7 ˄)#VIdt҆Nzqi:<·*^3*#*sIr($J!VUȥ9>IʱE1k)"Wvp.qlO&Z70QyQ]cd2po m@{jDp G¢U!dJѧlj]F!Ac65(c :QbKF 0Up떇Dֵ~E陥E7bŖJl)e;I?g17R'+$?Ԏ7 ~AM_~³w~ic+kqo:lپm_PKۛjF#twine-1.5.0.dist-info/top_level.txt+)KPKۛjF3o_ntwine-1.5.0.dist-info/WHEEL HM K-*ϳR03rOK-J,/RHJ,./Q0323 /, (-JLR()*M ILR(4KM̫#DPKۛjFKItwine-1.5.0.dist-info/METADATAXr۸SvgLzmgw;j]vMc[#M;L1IhY$Eى*/$/x),O߄6RU#v\R]JG5ezTQSKXYH+aK/d=T)šg`[[a"JTyXk~E66WzUŋl\ZƫSy'ӆ,QrXMKbRGqtTG@i͓\9'P^] n\J]0JENT*lŝ(T &[{[z6Z;zfjiW\l_qhXWY i}Ƴ}`*H-Lx.1Gay^fZ\g5FN׻6߼a{^ 񶴭luWPd%DJg(aYf2U4u5 SbBcW٠Q)_Y1Aa߼; BfLJX:YesnI#F Q@LE _H\F|>Vț)\ű7mS֬8f101r\xQ%Z$c (MfPJcqt||AA)xUk+K&3az^udǕ)ٸ)0\.E1wRջ7uqPtt0v+|8`hR@ `z@@`ϖNm椙s6^݉FJ1Cީ%]eݎ%e āG&8wtL9y`?"<k>JЗ;B##̉Iud hpߛcE?DG:%->pBAXn 'Q`Y9m\s T(֤5X:cq@\' Bax /1M "OQ'Lh!abKrXk…RmSQy'h@Jn GHwg}~cT(%Lg]}nmq(t ;DڈM+؊GH{ر~uqԢm3\bx fn;s]{_;0 s7o_lG|5fDK>]BƄF3 .갏 v#kx+>JtEJ8T0hɠdxz+HzFA*a Z[a<`r ~OOB&uEOKY m ⋱VAyϠU,,waϼ6K~$d ͫ$G\W㐭]n~.Ƹ 34pOߔ9#uǮHK 7<7gq =е,tHg!p?^<{iE%0e4(+eZal6Od] VC: nOo? Վ"qLkT"e/](xq.2"_vaZo94pԟ;c49dF[~8cQTwRE߁ȍ?`m8f;pT00(ґp0.ƸY; 1qOSPKۛjF.Nrtwine-1.5.0.dist-info/RECORDuKϲ8$OOn@APD./i墔IFdM7MӒ!_fUF 5"Ve!$<_f}+v4bfL?x]%Q ËAJUa) QZ\Hp9;X`iex%qtQi"HᇍvJl[;HxڎՏR,_JG2~8IpaĜN$kMT(:i֝zR7`3\8jCq :iW4 ϩI"ydUK>$lGw]?Ԙw&/XO][OSX5iw@m*:F5Md{* ) J!(bثstS`L2g[ŢMɲ$k0%tV/EYXٽ%/ީz <#+ r~n3iXzy""l"파o_"_^{O02}6kq37T=Y=nv<^ɋ5HQ4O$4,ZJ`-bD,oIU =*6)9h ,?W.P(FX| +U(=? ̢,9IY' )(wFW8$ @oAWK{w~HFAxᄅ3ۭ[m/\PKrjFR`{twine/__init__.pyPKuEHOJtwine/__main__.pyPK0d$F>_> twine/cli.pyPKsqEg6Itwine/utils.pyPKuE; atwine/wheel.pyPKK?EN/UXCtwine/wininst.pyPKuE}KI: twine/commands/__init__.pyPKK$F" A!twine/commands/upload.pyPKۛjFїr%##twine-1.5.0.dist-info/DESCRIPTION.rstPKۛjF s0Qp&)twine-1.5.0.dist-info/entry_points.txtPKۛjFx6ц8#*twine-1.5.0.dist-info/metadata.jsonPKۛjF#T-twine-1.5.0.dist-info/top_level.txtPKۛjF3o_n-twine-1.5.0.dist-info/WHEELPKۛjFKI5.twine-1.5.0.dist-info/METADATAPKۛjF.Nr6twine-1.5.0.dist-info/RECORDPK$9././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/conftest.py0000644000175100001770000000376214562147542015632 0ustar00runnerdockerimport getpass import logging.config import textwrap import pytest import rich from twine import settings from twine import utils @pytest.fixture(autouse=True) def configure_output(): """ Disable colored output and line wrapping before each test. Some tests (e.g. test_main.py) will end up calling (and making assertions based on) twine.cli.configure_output, which overrides this configuration. This fixture should prevent that leaking into subsequent tests. """ rich.reconfigure( no_color=True, color_system=None, emoji=False, highlight=False, width=500, ) logging.config.dictConfig( { "version": 1, "handlers": { "console": { "class": "logging.StreamHandler", } }, "loggers": { "twine": { "handlers": ["console"], }, }, } ) @pytest.fixture() def config_file(tmpdir, monkeypatch): path = tmpdir / ".pypirc" # Mimic common case of .pypirc in home directory monkeypatch.setattr(utils, "DEFAULT_CONFIG_FILE", path) return path @pytest.fixture def write_config_file(config_file): def _write(config): config_file.write(textwrap.dedent(config)) return config_file return _write @pytest.fixture() def make_settings(write_config_file): """Return a factory function for settings.Settings with defaults.""" default_config = """ [pypi] username:foo password:bar """ def _settings(config=default_config, **settings_kwargs): config_file = write_config_file(config) settings_kwargs.setdefault("sign_with", None) settings_kwargs.setdefault("config_file", config_file) return settings.Settings(**settings_kwargs) return _settings @pytest.fixture def entered_password(monkeypatch): monkeypatch.setattr(getpass, "getpass", lambda prompt: "entered pw") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707659114.0657206 twine-5.0.0/tests/fixtures/0000755000175100001770000000000014562147552015275 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/fixtures/deprecated-pypirc0000644000175100001770000000011314562147542020616 0ustar00runnerdocker[server-login] username:testusername password:testpassword [pypi] foo:bar ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/fixtures/malformed.tar.gz0000644000175100001770000000001214562147542020362 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/fixtures/twine-1.5.0-py2.py3-none-any.whl0000644000175100001770000003671014562147542022550 0ustar00runnerdockerPKrjFR`{twine/__init__.py}TMo0 W<ˆ3V$EPl! K>ߏǐbrI(=>G9C{J rEgQb^Uh֝(kI'KZuz W'fV o)BM9 n;IB=Xf&Iy'-qT@LuWS1 MiVr]XT<y@tV(Eڀh Akoȑjv0,YgݫF-)[%<<xw{xZ?<6lvۛl}~dۛewg~IX~刯zd;,}Ƌ/hہMK6 rE+Q+[ʷ:P5]q:"ClG1{?;r0%WqبU9kIW!xSr ώ[ԫ3V R&t_ixNvO{x^>>.dV:'T_a}vʚqOYUAXʿBz6_ C뿊P"ůZ{d8e(œPK0d$F>_> twine/cli.pyUQ6~ϯ,}Kt7ע&T!b6FDɩ- ό'Y/ R24ynF7S3hD aV>3O4m\^ !d;5Dk\@*L{nm!7Q4Vi֘zrC *.Hx~GdWQ֠+9K6İd{ X|FZ{ ͞)$k1gꙑ*bb~Q<&>VbcXixrA0[|y He4V+˟Hr[Blb3StaB!wɁUŵm&z↙r!*JV$ycIbKem,Igҭ$}-jŅ "?hOeI *VѨGVE͔~]+/MS)Oޢ6dfF s Z4 eE~dSOo۸Jm0=r+Au?#Z) I-=f<:uI瘷~>$-6W-}0ڰw t C mSh=;cN4ǒB&Q'mO^>v-/Q2==Jx(?T뚙tl솊]4ڡRX8SESQ^Z_+YL閐eY\ݝհvޥYOm@j7?}M] t+Bӯi(X]5&L$z:'~}yY61[rmP]cSnxGe.E@d+Vior oKG_{ NXɳ&x?A| "t+W^YJ'Q~[ FPKsqEg6Itwine/utils.pyWQo6~ׯ8(Wi7%fYQȔLT5coIɒvbKw dqP"x%\˜eX2ILpG\ +0[W'WZFG_  Kر@9B 8ǘD`ya/ֆ B|0fh_SҴo)>bh$Uz9#}qz5-&535(W)yV #ÌA*`f$q+aD@∲(q_P3̷iR~/b|..f<[N' |v=]N3|z0]L?#IA E .x,c^yZC*1( MQ2T$J`JJS*ZQ| {-3s J@P"ʄe:Q̶z8RrS0]?u2y"҂)Uoj&JI58C,ݱ!, 8"rl's^b+A_D3:c@j8O5D`wqB8=*# p.}b)4CBja:c7A g;3Rx[>w +統2jJԬ^a)6M7]ݨ y1J-D1*[WۖtFd:-n|3P֮58* #DO,? w:I|#xc-23d>W $#*FJ uR_ ڂbة*;\z N{ԽB<Mn' l$kͷim쨙_PyYA;f -J&wS+ϝ?> NLanwe'<˧AgB*BI´Px?UN8;UD++^/΄58-^Fb!Vl%0\@;*NhfEY]6Ttv 5VrN{.޹Aq6qL;1r?wNv|{jMp}˭ǭrjV[PKuE; twine/wheel.pyVr6}W`fD2onTܱdR@BMZR{wAR"Ջٳ,TzgDr٥UyG#F -dVƚ՜{0V(',$9D?!NUlwL** H!,E )hDŽdZRpn)>6j895.Z9}~zlbe o/'z;YJ sc\/Qa7L xi,F̪mdɄuF,+KT 0U\xΦ{1O#0]z`b:kjv9]LfzƳrӄn` GRkZՐ\,*^+=0 f-,Ң YJ;sTFY䕫 $ W18z=ю6B:4)~"U$p`xi9P9o~}Z)ٝ ?uɖlArS=1FU"ԑARB}5a4{Ң/h[%`KW-8q展&K/'x'gv}49u\it_t68tOfP&:6Muf~| HG -iSFyևJO~KȂ~ɗ%w~oVߟ|OwiT|B%{"  fR$ -)fMz]̔|/q D?,|73 PKK?EN/UXtwine/wininst.pymTn0+akд=)iN)PieHb)_^l`;;Y27$IW2$DZGUE +a+dp ) 0z:~ + x|d-B i &' bTԄn藇Oyy}07 jxAa<_9$|E|5KXϸ {qn$<O P;SqG6JqH\7@K}¬"'걒tOg1A#,'<:/IeP尒팪4 [S{5e=ætKevayFZ*7C[ 7.,G^Teht}5dz=6.0=NI[߷8+l[ SȗNjNjm5'bXs? Z,a"9jݲ$4sHW=ԺfVS@(&Bvg>#?[ vYc'[?\ ˦7X]SZ+ROHt$s7v{ze&Fzz,scQh엏݅<pN5kpLBh;0;1H}5ixPKuE}KI:twine/commands/__init__.pyuAo0 Drvr3V8@ɐe!HDͿd@a'rh80|kg<[@m0 < 0Drn4 q8GuRD=|ȂvGoHY0g&X\-\JJ^/8 0,EjS՛/{?Z1B߉lOh Q#j(;vDKG?LHU¼pWes}sվ԰j[}dz_e&9o>LISM}Ոzw5%JCGb7g}pGh>q 4}Te$+M@'$Kub PKK$F" A!twine/commands/upload.pyko8m8iM:^-2/#8n;ÇHImz [s8{#r^7{"'/_D.ꊖ9YvVg,'m3AԆfݙߙIĈ٭h7[{RՊ .ɚ5d)92Fv\m4K$-z(`So`5ojN_v Ղ&(^Iuq~y<a5ǪdRr\ m@@’H--=U;Z`@%R jUOQN2oΖdb9|:zq$>wWŻ+X!gW/a&`Urג$5^U҂g됆-hH @[ҐѥZ[V)"t% ~ϑ4W T}@[YZr-d(B2.z~oܔ[2iڸlW3PT]%ZV|']W[eRZ) ɟSqR4wuLu+@TDdR%t, Ҭ|CnY5T]-nXEoCHjdrX^ן߃gW}hjѩ5bgvYQUCwf5NRK Lݦ"m(*՗4ŗ1 oB H;]8M۔5%x)fFlp񏯝c}45:R y@#4Ѥ-k0nsm` pŐ[V7}=h +@VUV`{R,hgG9)N;yosJ1*8Ks}N[Ź+S6"i 2YyPS(AEp~~n,9L7%F"T j]E࠽_Z Å\_>7Nb B |@_jSWb*ۡ_(lJ~ܼ:$:RQ=_^9Koi)fv؇9=4an2`x`lH%;]lxCÍa^pú/iO4SrvNn #48m7ZA@N @g((77#mw}wXUy1ѩǫc?l'Ռ^Çݱ׾'` 'u{;IkZy!HdǩNu %{Ǯꊅ;)n +tw |"#2 7I(l˒n !f ѩb 6JMGAtCW3obwɣitkj .2yj1׽/6 C6n{nD9GMAOet >/Bqv9@[` i P8,:-e uT=Q>!|p̾T9l Lf7*Pl ߱=F0zh*LͭxY 9ϡ1R6:I 9<J3e~8}/hXCsZ_wx`$ovJNF"QO(V#-qjLzDŽC~D+GcM3;:f6zVž*M4}_ac$qvh1i?3dƿV&? huwn4ǎHa%Z3 ~\46lIa(~z+cgo=,?n2o";"q@hǭ59t2#3~gS&`fƢ`yhdggnF DGK|P?I͝{Xi7Ϻ_Gu[&Zqx ?5V… QgwL \a*;7u]&=Aa|8mYO)( -cڄ9O~H`!9E%މ!Hi0'h9KF~X;, жTs= +y/j:2-Sʩ%$ <vp# YDabփq'ɴLyo߿%h)AƐ,.nսme:Vat/)cgT{.aA̟3$a:CFm|IKy뇠cyb?ba˴ď\^K8V;37a6:}7:e?X 3}l爜y^} ^e %PMkjc٧K0R I$MFF~;%5, I_DkCǐpPKۛjFїr%twine-1.5.0.dist-info/DESCRIPTION.rstWo6] $%>݂4m,a;+hȢFRqadIT@ݓ[JF/ eIPTܚڐ4"u)Y)Wx=>s=>8׵+tkmL>Qt#+WI9mS8KM]jLYg~ue)z_iZ蜮Y *ϥud">e 8+H!W 0 '-u[8HHCog&8FoHʵw&|ʂi=(*mתBFC5*R<}:iJuٕ}Nrb0v,$m񑕘Ñ!dm Z~P^>ں0 rV dPu=أ*Kvęi)ЇE)38e҉=g\^& 7 bѦq菎OctPZ wq3tp"B,CV2r?zv3l % qz-r[%òWyeBl78s29:>>ExZ8:5pen>44Wg9Wq`%oվes4O>(:[$#V϶iLoep}QWxևQ&gr'ٻ/5t5=;8w}4>N_N^N//.`[*F YC+XBwA3]nm8MY[Ϗ&nxlC ->w@5ņ"0;`} s)vknQCSfW綯BG}v3NKMXMS wi/$Wrb qWҾ)DDp/)neo{̇?G#ؒd ,n:,}@x'Wihm}R$Iޱr)T[U񾤝ƔI$"1| qc 44np2/Ch-:blHyWugYnѕ_= ፴}ytƼȯpc$ߍf 2gU?PKۛjF s0Qp&twine-1.5.0.dist-info/entry_points.txtN+I/N.,()*)KUUz񹉙yV +"ZY\ZXZ ЃC PKۛjFx6ц8#twine-1.5.0.dist-info/metadata.jsonTk0WttiA),5a`ZgG,i4ﻓ5et>ٺ{]d C6f٥ M/4l2͛  .xźR֠`ج{qӗi(eLbJ U1¤FoS⻨ t]z- @U6>cd9I#^]mC=|D?k+%~naȻN@.(3?".B~<bK ak7 ˄)#VIdt҆Nzqi:<·*^3*#*sIr($J!VUȥ9>IʱE1k)"Wvp.qlO&Z70QyQ]cd2po m@{jDp G¢U!dJѧlj]F!Ac65(c :QbKF 0Up떇Dֵ~E陥E7bŖJl)e;I?g17R'+$?Ԏ7 ~AM_~³w~ic+kqo:lپm_PKۛjF#twine-1.5.0.dist-info/top_level.txt+)KPKۛjF3o_ntwine-1.5.0.dist-info/WHEEL HM K-*ϳR03rOK-J,/RHJ,./Q0323 /, (-JLR()*M ILR(4KM̫#DPKۛjFKItwine-1.5.0.dist-info/METADATAXr۸SvgLzmgw;j]vMc[#M;L1IhY$Eى*/$/x),O߄6RU#v\R]JG5ezTQSKXYH+aK/d=T)šg`[[a"JTyXk~E66WzUŋl\ZƫSy'ӆ,QrXMKbRGqtTG@i͓\9'P^] n\J]0JENT*lŝ(T &[{[z6Z;zfjiW\l_qhXWY i}Ƴ}`*H-Lx.1Gay^fZ\g5FN׻6߼a{^ 񶴭luWPd%DJg(aYf2U4u5 SbBcW٠Q)_Y1Aa߼; BfLJX:YesnI#F Q@LE _H\F|>Vț)\ű7mS֬8f101r\xQ%Z$c (MfPJcqt||AA)xUk+K&3az^udǕ)ٸ)0\.E1wRջ7uqPtt0v+|8`hR@ `z@@`ϖNm椙s6^݉FJ1Cީ%]eݎ%e āG&8wtL9y`?"<k>JЗ;B##̉Iud hpߛcE?DG:%->pBAXn 'Q`Y9m\s T(֤5X:cq@\' Bax /1M "OQ'Lh!abKrXk…RmSQy'h@Jn GHwg}~cT(%Lg]}nmq(t ;DڈM+؊GH{ر~uqԢm3\bx fn;s]{_;0 s7o_lG|5fDK>]BƄF3 .갏 v#kx+>JtEJ8T0hɠdxz+HzFA*a Z[a<`r ~OOB&uEOKY m ⋱VAyϠU,,waϼ6K~$d ͫ$G\W㐭]n~.Ƹ 34pOߔ9#uǮHK 7<7gq =е,tHg!p?^<{iE%0e4(+eZal6Od] VC: nOo? Վ"qLkT"e/](xq.2"_vaZo94pԟ;c49dF[~8cQTwRE߁ȍ?`m8f;pT00(ґp0.ƸY; 1qOSPKۛjF.Nrtwine-1.5.0.dist-info/RECORDuKϲ8$OOn@APD./i墔IFdM7MӒ!_fUF 5"Ve!$<_f}+v4bfL?x]%Q ËAJUa) QZ\Hp9;X`iex%qtQi"HᇍvJl[;HxڎՏR,_JG2~8IpaĜN$kMT(:i֝zR7`3\8jCq :iW4 ϩI"ydUK>$lGw]?Ԙw&/XO][OSX5iw@m*:F5Md{* ) J!(bثstS`L2g[ŢMɲ$k0%tV/EYXٽ%/ީz <#+ r~n3iXzy""l"파o_"_^{O02}6kq37T=Y=nv<^ɋ5HQ4O$4,ZJ`-bD,oIU =*6)9h ,?W.P(FX| +U(=? ̢,9IY' )(wFW8$ @oAWK{w~HFAxᄅ3ۭ[m/\PKrjFR`{twine/__init__.pyPKuEHOJtwine/__main__.pyPK0d$F>_> twine/cli.pyPKsqEg6Itwine/utils.pyPKuE; atwine/wheel.pyPKK?EN/UXCtwine/wininst.pyPKuE}KI: twine/commands/__init__.pyPKK$F" A!twine/commands/upload.pyPKۛjFїr%##twine-1.5.0.dist-info/DESCRIPTION.rstPKۛjF s0Qp&)twine-1.5.0.dist-info/entry_points.txtPKۛjFx6ц8#*twine-1.5.0.dist-info/metadata.jsonPKۛjF#T-twine-1.5.0.dist-info/top_level.txtPKۛjF3o_n-twine-1.5.0.dist-info/WHEELPKۛjFKI5.twine-1.5.0.dist-info/METADATAPKۛjF.Nr6twine-1.5.0.dist-info/RECORDPK$9././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/fixtures/twine-1.5.0-py2.py3-none-any.whl.asc0000644000175100001770000000001114562147542023276 0ustar00runnerdockersignature././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/fixtures/twine-1.5.0.tar.gz0000644000175100001770000005142714562147542020221 0ustar00runnerdockerTdist/twine-1.5.0.tar}{7p+y, eli|7M0:3ئܿ=FƉ{ic󥣣zޱ77>ΏwUj WE ¾bĎOlקno/=5;8ۻ;{_'@(@L`:v(#C;ݾASExB!VGFD ANj9q&xNom??/DeUt#u=()wƶe߾gA,x66zNFoHq;E?O Eo4b¯oy֋:v|L CN 1v{ {::x>ebL ;0e^;[kƿ o:oi<㿵܅lmm61>G8 AsIBA1ZN ?[q0A{UDc > ˆ,k_}j Ϭgc׉Vxl7tgV盝8ؙ ԦQGVtYL #\"7M GP<}n#.fiƢNC=2O=Ъqq ,$T%zHA,|0>4KXUDbܸa}L1tQaw\-1o 9ob6<;<7 p5F`,9T0o?ͼG$p,%0P4DځN_v]9uo~T37 Cd}@s<{r끈rQ{.f-a**(@؀ ` C̆#i9 GJ/m.P_! |n; ~h5`rOs*g4Lɍ;mtE1,,`"$]l4Q@amGeI*4u[0ÿC1 RDxgP1<'!Lp`\58,HUTA~A?j&cӌ{{P@$О~'kި=='Gi!h"Ldᕋ)SK9q:5ʼIygVm3!1a6{q@RU㼚 '8DMN ' qz# M`EM%A(WehP #=%+4[߃Q{6诠C^dab6Em,(jIثKjR 2izFЛ)qɦgXb Wb'HcOkQ+OV"p!=-C-#9TV31pmY͏Pg׸ BE+Ne JxFG*Ԅ)zߩ__}#i >OC @'9s ,}y)&!.\D _#σDryOmn$F)$7|Rnx=vM㒦ckA: R|Vÿv14OF\g[d$ȷ}˧;4n2V~R(*5I5Azo"I,+5dJ鹔/s)^;[Jm(R&|JAJV`m P5d.H2#KچpX)2^ԑw;2hjMu!݇DE_+wT"B{HvDLD澔3Cb@F+e5,!m-H@C k&a2CT'B*I*],nMnhKzl >-SabpW^V!I, *]lk3vQLHu4|ƞ1+L .I C+O޴,(BR:Fyh)QO A &Gx(TeUm_Ҏi)~~ @nCeV!kP5Zv}H1Sϑx/d"бK_ZS, WV6"_43ߝ=X5x]\$ +w2厖VsY"Ѫ=Ej"Pm2QB唵JJHk@ҶK8>icI*\E+\8?>[u,mPcX_@3Sg)(2/2zTFZvP7/D|2`"(i h_h7dk_ 0)TkSLPI5%J>@Ha0* y7y0(tϦjW%{1Z^MT @E,8ؘ8> jj%&{jv釗sRI׫;J|RՇ2-A,r M2&$ Dk]]2 \ǡv| ȥ=i#F:3Es" *R(@h߆dv DR#5NI2 @Xj, PF> g8PO9UTI:#ğss.WnezhcwY۟$M$^`A"j0,5An* E_T<R6:Ds lTC y7- 8ô";a:m2/%ySlp"!(=\j翲߻[uc|l0G"{e3j(R ]iB G/ևV%Ϙ9ҖŰёƝdSöZ9JIl뢏eȨeYH'nB4ZNQK4Xǒo!#YE)o OD Ҏe=h_=o4w뿱^Sqe/9 UUgwBx?{wiIR 4?j_ hXU _Ӌ}gopӳˋz_ꙦO}jU% a ==mѻ?Q`qLt(zYn·Ը@@{k rNIP߱"3ƂcKWΪ Xqb cJ((|;1 P]g R^-Gi Jw/NO274pRֵe%elվ{.z(dY75Ty.b8\%0Z6*r>dڣGiRRȎ3ʥL;]pXp˪)kQ.0`ve;^G_^r ro'H؝$siD߰zFF0y2(q gP܈;麴 ՖO?[*z}b0ǧB$LgU, Z? YQvqSƖR]Ի52v' =l <hHj?Q'v@ t{1ڒCce_.$챸\ґԯח%a6]—tK|f*F3=u‡v ٱw&ŘY7pIf/_Z[(\$]Xe< Air㟖=ia'~E,3KKI\z[!. ,zӨˌ<rK j\tsyU/AQ `\'0bb0b,c\sbPa*Ɗ%(̔PG# M]1֬-A |NM NzPo5ݽOpÅDŽLtpĝ‶m+Y3ٷE3L(QjBqoYhl -mYl'鵠=C틯+ j˯*+_☯$jɲWQ)9̢@mx-jו,jָBO qQn):)7 B ֹ5ӹ}9T c[KRe(dB)"}<4 'sNibwNkv孎^ g,taYU)ƖvEyב;©z(=)vo$v؄E E,yT" ^ӗG C_>}{a7g4p܆av?09nY4aw/uuAK!^-ED:/~ca_f9 gT/ĪYaH(Lq9L5J@*P!Cݡ\& $ZyKkjg4ƸDž#oi9PS,P9^Ƣȫ#"|U؁~7J~X_"B`=UC%Y idq@hfj4TUˬlI;vdwXa>.^CC̤=U_$1[aPP2Ê6Җ3do mӛ7 4㨗XHq^77<mk9~ytrqlme㿷w7CxkQfM7J\J(͍Ptj̍DuytB'Ǘǧ'xwqTGg秇^:<5Fm:^](OߛDf|>=YDi$ޡs"}cc2IFhh_:kA JIޅNjBF2gY$]PAiw:5?=v">gq@PeIIR&Џ1JdׁN@)e;2ܦ#Ϭj2걥)I%A$*^aԒ^#W3\[^ӹaϴBi{#R w 9`M x F4 ;ALǒ{N0N8"v+8<+R& iB.^{>Qp!) \h^fFսUk>bR 9$Ӝ] p.qc@lD.o164`j$Drci{v$RxŌšZB>QfE#Ժn Ԡ9LlܿHï GKY$̝q<B󓂲72ƨI4bcJb9%tօ@6<TY8FDB*槤*.`88s(7v,*PǝƜ!T2w aQ1Vݬ"|*d$raK[*RS#A"/jϙNh>'eII1RM"YV FLrj绸-SF2r% 5A+|pC,YJ% 핀L`C d!Z12D!AigoJq8vncYJ.%VN1e8:rI FꍢFJ&ӍeU4dH9TphN8ң0{a2GV6&7*65:q ʲʐv]_y3'h j7`陶Ee:bTK"?clĊ ,6b;[o +a<Se ?9ڳ*_IQk]5w$ʬ(b_H${_(O-Yhj2JQ3cFF=D-51gM%^1%jr֐]TgjJHd( &)(R?*A4p@#̃3H:Uu pHK'/J).& 5^Ղf\rQpUp9}/R-jbKD˜񗉃V QLBBҌR)0X.!nBdDK[ɇS3I/մkmM!M4rՃwA*MS!tx:^>zw)n3t~m{ Bkz0z TfݗRZ>ižAhnɂ[7ܞ]I{VmsVFI]g@=/i& 7=@ C2dђ+,[TKG.(AX2̢Kc]@K$;8 j?|}R?8??8<>+qpaM܅kSx$0Y7tәH!#̰I団~|G'5k7Ǘ?Tyu|yrt{ݛsq%#wёhO9Oݱw2 RLCUc@^:@,ᚆpQ4y@֋/GAӆ'd;HIs{0o$*b7ƴ{RRt3Gcvt+)0jw&K wco5#[K95[rtݱ%%U>zXLwmg&bΠyEeZ(:'sMkIz,,P0s{7q{IBT\S1kCEr8$oyƌiCgGmzW*|g0x }L1HF{FGo< Y8cutnKрt# 8P?zv";k62/ 掫v &⠇54!0juZyt(1Hd7x%KDy[؜&;1A:cc!bj@Wʘd/ Zli" I(+zܙ1Ä${%'ck o<1bǀw`!pqPO^c'gQ\0L~;*eMucz¶ť85j?Gd짥~=<;з/, ,ĝz%w]'^ҝL%uET ֟߿Gqۛ; \{{{ͽ=]ucN]DnX'`¶8qeIb]&h@bdg(r<79Qg`QkW[ZbnYH,~-^`I harNGB/UЃzUy枿|3/FaXXIR^q@w|bQh..8x$E_/%wQ]AmE*zb0d8gM v]X d zE}屻ub&9p~PwmF9ވ!f-e*/C')7 \tе<%xnT'C$Oz1N$sg^Җ3vhp)3L"I.ast:Bw1 [ws.2 &)ʱ|: ^̨^jdT_)A^jh$Иwh짐+s>fI 9qh"/NxѡP3=7#`z鉹\6[ֻhQ~SoJA;}/e|-VJI`y[_}WF؜mmbӖC\_EG`ElT?/N_< rQ,"52UC}tx@?8><:ԧ!퉗oѳwUmW-A!Ce}P P#w=SӫEd舿Xc!(Wud7.Q}T) >$6QY, s tƋz{"9(*jJ"]6a$-+ hp%ܿ?^Hm2YMؤuÕɥkb a)1h Y$EHh[=\ S; Y`bA~g1Rr ~ W>w~&:/!tv{zǶ}7F 6\O)(uᅽTݪ^z5i|Cl<ņ>\hYD}=6[N 78tZAޡ ۅכR|$7)duq{t$ Ne@RwQݤRrߩuX(_$O,YZ4Dڼx=pM5Zs |m*1^;I o DڶőaàˆUם-?")2vG*Ýzs9}חy!*p\tj2&YG>8mȷ* ל`q7҇RS.?-=ܗiN`2*wL՘ig);Дw0J7@2PF@MCP džNԹk tR@ҶĻ?^hi-J[C5˪l(" ji߅:K2OP9[X3ޑ?{N(䂎}aN";+Rb)xn=<=ܤyF90/GQּoy{5Yckܿ{8IRxF(l~6_o%{ܤ?;ݽߣ%o'uٿT ݏz}z}oZd}Ѿ'=>҃'Z?BE@+>O8so|~ ^Uoˤh{ZbY"+wdykwX!KwXzò>ڿXܟ%>c?"aݵ8 ϰ|?K`>__E|Ѿ|>?e%~}ugA7i9 //[_OÏn=!剟; 5{ss^xR!/h_4,eM `Q]Y ֟8tȞ-L;lDF_Y@GbfdiqȹA:!&aU20r-Hw8lc++v|$Xh_lҏop??:ۡ_;8&9 DTKQ ]kW T:taLeD|NjW%ω]i"YԆߑ_^u,JEg>2@12 "d(LiK3tH M!rjq6 1#Bm:C\#q$RFR-j+ILi%)J:()!^R6ŦGnohϖLJVJ<fMU5>AWI`-jB?QKm0ne^~rH8f`R3WFTey NV@֯f1T/uro R*?Fì$D,J])bB2>cq'fȉ{w[a~Yn޷½[ؾ_A_ c2._)$Hl@ܰ2v&ݾQKG_ p6X/RZ /4EM'^UY^|ZjzdJW4'J"t#t~0;2 L"A7j~cERV*: .G^\MeJ>V~/w]PP6w(w[`zɤ-UpZd(c0S,n 31̢"SRLMmۘ-|Ue .WX&Cv2P]St&et6O_ (#f{f2G@2@g&OSQm''TT«21ŒL5}6*4IA+^eLZ(׊ڕ.&j22$8 N4Pl*8 E*6V4mmǥx!˪_<ً^1IF 1?;Yo' '9Hv&tKTObd ѫwo.fЍ|O8GAkL٘ēiSzw { ?$vϯDb5QPI*9)U13`(3ͭ~}:hW)J)Cg;C 7D Y*ZKSg:agW*U'p* ;cZ]e#%P.e(\U;)I*|ښֿYv5%dL7 Si~{%x:vuD 8_h~!>~lKLfKe jj ѕSv _yj+\8tJG^=fj4˟]O{c4O lms>Rᄕ$0B"|5ي `vG oE;`p(we*uZhuQxP%Ijx~3 U6sK~?}4w?Gim߁?kmPh0f^`qYaOuA$'>??Txfem{ߋa| ĸ>]lZe!$XbGXYKi{z#qcA?[{- "yYnt=3Ylk?ή05ÍƞN 9[%gKTܨ?4Zf t֠,GmbcqLTa<Уmx9XkNorzu2_oC L?@t=&GYhy PF Va0-WRؕyV¤*NeUY"l~.L: 6gJ}RU䓪ʖߟĤ_. 0 %$e!̦e@R KWPtEݩ{M1%Qvkʣ0psKM0@<7i oV~rH&.]cҡ9_ʿQ#JOjP p Oz z$F0hCϦ=fd5=dZ! 4Zzx.ޝ]\$},8?z{\\#S6L'^xE-ûɐ_yOAO"btׄ Խe$J z{ kCz#ʴ_mӨ\OuWspLOx]u-":Ypoc5Qtr$nQSw 9~j~t&C!]鰭NmR1xx`h&(jyb;9_Q@_Ëf)EJ`D+uRqWICP}y;4??׌.|z׮o3:"oZ“^2(`gVsu8tcØˌJ7btk,OƢ z~r v`}fm8VJxzaH!/WJ{3CnȳT|(4*xp%P;7$r2y_/QS0R6= }R)ld_zfjZɮn/(|0]U|x,)RU󋇆2DLH}$ݡ jUFzL]+|*uGr>T_*jHV4՛47F:'B,+ !A׮xˇs\õi}h6(bH^L[:$Q SB??Do7*4 p2h[p%jjڑor(\FM \1l-sԘpf8[b6O[5<@VI{14ա)seG2U 3jR2,uX XQ9]x{+| 0jŸ{oJ՛Q'rC>-ZUx:ƪlfkA ƪcXQYv>C MOtX!ɴIEtFq}Q 8@m]~rsg2Hi@33ggڗ)Ti3ShL\`CW2X*ſ 9tA~){r5¢ijSJ˟B%!(S;GwvN TS?TQQQѸڠmhLgw)b*h?3kώVc;a꾨ߵoH@K.H :.ydwnPH-V܃tA!~* j8WIXT\5~lPS5đPOơx>kuRDPX<ۖ5HJF>&FΥCӆ\b(l ˪2zL*!lRQʠ3y%QC M} Z1հjZOk}uFb%>#1bNS6Fl02eTM}+k yF9T=ufKj_!~%ZIٳ 'n6olqwVG ]c*mH^+ѥCU /Mi@`VQf9& 4jG16CԺw@W ɱf\ n3z$XZfpQ@ZZ.cN\j8rƦx/>Nu\EDK%mMS])}Q\KPXrY}qS:L ץi|ʍWJi+ {]~TFb^RI^1}3ljz%5ɼ/n6imuSKD\Ï}ߒMJNaY,Kp_:Hq*hE<(Ϧ38-PZ&˔_R}6P!GބO 97mϟhzbbBTjjb@K?ĥzcXN.K&1HK1UeKz+ɏ8zqH=Pp1#K s+Z*EE[E'`_EzL,i⒟I)˱J6&ͩb(u } xܡS4=(Pz:i]GNSݖtnR2<@AO%-USfE]!pXv?wޥ+uHOFIݭM%7]痌.kŝ|br;M;;U,x6XbBfGoR,?/<Λ}e[b.`*73Py Ρ8q FG5Ht.NFX4{{N&b,HyL,!#ARj2[f1DъiVq[g[elJ0HMd#Jd86.r TE농1(`Hʼn7ZcOT:N&u['fejw$]l-;4=~N~ C72Tm;8;v" @RKʥ.*D1f¹ Kf]C P MP&BTX")/K:;Pb*IpxVK ׏L*%`r:8HWF]ҫOK-#^^E eo3 7:~s˯K~9:~dD!yjD8][&ݸ%lGoS05N nindnZ'IDo?{SK2g.Qq|`o WĂz?'UN@׷͇I.d@GGvIa7ŵr+0aoK,mn4=BB1#6ȃ~Ξ솉B}aoJĶ3|M(O%?0~i8?⡯HtSB439:B@|@bH}B%Dx`bgKc :e? dHZQˡjoA5I&n`CHw_ӞpKjVoL۔ ML|.Q Ѓtլw!{'zGx$U ӨǼ^*$AeA `'썼kI`M_'`fQ>P.e8ܿОAuH0x_D+=MH߇[+o9E (Y~{typxpyPǷ{*0ҕ[o `U&.٫{U!&a\]9%B?eR)VX5™#s[5CJ 0C$L^5Qᓺf_rqaHJJXvE1͵MaVB1fnF22ˆL\2:=oa[kkp/LRH˩Z%t? TT_ _ ' \ 8A+64\e: *0J@[;uW|2 *z8kMn77_ﻘ:{sƿ)`lnfk`U|_e7v3k>C/w=qѕghe}Xxʕ%%ìNF7vk(] aP ߊdIb]p> =`1GKK,:+-=FD[KF,Xn2/*~$y}|y N7r8P2L $s /Gsq1 f@c #/~]/]Rt %VB,tmLpDQAPNuم }u*$5܌0xa"=Q@QINGtiji C8;FxQ4sfsFSEcoRwaMctׅ>&] ܾ-v[E0M,Fthy* x̍ h` itiQGhKF:,K4Yeٸ=S0t:QhM"GCAg~11"VN9ϰY/0 <&&2  9 "ھb[!i)SA񴕑,˝" tBM!3<;c4PO I+)_09+6m9;d, Hl{Y薍%P_< rQ:yDjdȇ Er G'N./L8:?9x{tuSqvpqӷoUiH?пm_NK1##C rH+Uf}T#+H)R0FQ 6&\I777‰#uA/03NG&/VJٚG&.<RD2N<)M& Ժz2(DH c+#qFY44w@,TB"heh W>o39> Ó ؘoTq:DF@|OA /V͆׳IKf;rr=D+剢'7{۔o:~o*]"S|9+ۅ`vMU!5>t b] :eA1^8CpAHgNÆu/htYP_} :,/'n,-j>uR 5Zs |m*c:J%7 "mȰ2B j)YwO/HRi tl*Ýzs9}п慨qөȇR,Lɷ;6]58 s>y]jeXH| wq0%V^ |H ꚦ8J0u `(0<6t8Uo2`ɤ-'O:p EF- !T:%@d)f…-TTTN@xNHA#8@U5˝Bi4|2{N//*+/]^nFd=ܤy 'Xͤ {ow]c5[n޻ؾgtgPl޸?mJ?;[|jb61Eɦgoq/;le:|cɤۃWG[Go0ؽ{mK^Ŧ8R~]%&P:dz;s vD_Mz(rӆ:c>X䉂J|kXcqւ8kV8h%/{y+ן?>OM_Ux6֟gY֟gY֟gY֟gY֟gY֟gY֟? Ut././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/fixtures/twine-1.6.5-py2.py3-none-any.whl0000644000175100001770000005356314562147542022563 0ustar00runnerdockerPKPG8hb|t\v]AhM#.&}bh-ɰCe-8B):DmZ;CT+ Ru޽jԬUBA blan٧lwnn}pV'nn&./ Y$b!PQm j/jZ?QlZ4 "!'p$n +<q VK?S]@kH9NPE }+*t$FHEy.d8?q;rsEoa9hΡMflz9,.,+]G z8]k)q;bvx6φ0 rE+Q+[ʷ:P5]q:"ClG1{?;r0%WqبU9kIW!xSr ώ[ԫ3V R&t_ixNvO{x^>>.dV:'T_a}vʚqOYUAXʿBz6_ C뿊P"ůZ{d8e(œPKG.Gb twine/_installed.pyVmk0_q-UuP(lm ۺBm Z֑$/Ϳou-#;='yT",|>t kFRU2ן*Y^,*44pMeniؖVR+5RJ墜=0Ēsc56Q2w @vɌ8>OOy,VO2׫)^h/1^=&JߕvI$wbJ_(d'(x3K +JG-Pkn\# ˉ5̶;BҴhl1M]~-ՙvvJsa4g2Ǵ5 ̺TL[RaӠmrr5s\rةiF)Ʋ^xD'b^GQfo <;]ڱՄF8Z販m$PZsz$by{2ٿjX{H3jfq-C#pήp+띛v^9f~ӖPR'f$p`uA/fw¸%n\jMLQUPІGPKBM-Gbzoftwine/exceptions.pymSN0}W\ԥ i/)EC-jnrZg;  jsν>m+o) l%\gg3%+ubK~ϔQ1[' `tG1Ҟ:Ǡj2kƓTTiP%S/>3x) =ޛŢTD͢=>YWGղsdo'-lZJV-2Z{+T͜}/,[&er ̊)Z?n)l6_zuo ;V;_ΉWc~!BB^> rKYTӉG vȰ=H K+ g*fe+p+$W[|ۊ$IO( >b0,)Bʰc8# QJcгe;LzzX D gFx0 Y8T !ѲSd7d2p}j^VDj£iTAS|C:% lF;>ٜ@6Ve5E]ꮭM܅M 5!m=Wv*?ڰ`Cџ8PK&ZG[twine/package.pyXQo6~  1Gi2X֤- t]Q-%.Tߑe~yVTdNkʌO&GA-!'m r  d&g#k@OEӟ–dM抴d* AIMh0Uez# xכKEMhUpTs~zlj&\U_\]/N+e\n mKFȰS\sX]̈+JΤl*/P0TbA拈z/fh7wwyM^\_78zI.ߑח3&\#IC#!@V,CꢥQ;fRoDz9Zؚ)dT2Y &ijU+ M\(BWgڍg2CZ1VrFjuOzŒʲbK;Di,Wl'YE1oivw̗@F'f RiKV3s tW5Ԙ,kP4blUcwt`m7?Jb`xOICUiZ۹SB?m|c'stiHx#YQC:q=9N?ޫDpES]&d?Mlj'(%= Z2Ղ7Pǻd2EdVݖD'~z60RW髻PC#X :a^~ȸ Fsl1uJT_CęGg9GA )S ](i;arxsXS.Rԏ?[ ,s>F;=df{,1* YAVҿh•\# ћzWGgTqDi}}?N>`=D>+.bWm&ĉF}n[bA4T| U ,ݶJ|c#U[5Yۜ(΋$E+%?<~F6ݎ-u j#6ڗlDI*:\Z,qnFxKixC $žV#%;qHGjx{ycZ?{ti+c|CO<*ɦ0 uFr~W29VFWbPhMx]miϞ ۮauh]ގ*6Ӫ]1u&(4fBmfHKtOxMwE͌BZRI'ZMݸ}?Cߣ%TxFD)i>EUlvkm`<PKG|i twine/repository.pyVmk8_18˶)@^)b{*[>In6%[8}1yf}Vtm/0a9}R1ӑ{}c(5¨`=D:f~%[YBƶKFr5$\  {Xh>MfsMo'lJow0>2Q^(˟Hr[Bm #Ӓ|AS:Pʸ^L^ϸarTK Ҕ Ɨ[i)(ϰ~ɔG2Pp Cxn0^gs) R^0B6ƹzޗl~p:[wrr-Jcv;Vϸᅉm[Yl@ɴ{.KX Jձh9\rV|p-i*ʞw6 gy O}m6ueu P BRlVaJ)jTIc NUb^ #{pP@ ƃwFFR/`@oƀv^9.cmɮF{]Xe{G⪤.}<`=6xAmk)BX&bw !7!]>tEFhHbFFfzᦞWmm A3BC{}\p)ڛLے}hiYY}P 6d&a7-j¤K߲C-j';;CE 34"vEZ90*';0'hw '[gi6J̗22h./Yx]?}ױz_ujoBшvl3VmPKGhXRtwine/utils.pyWmo8_1P>ƪro_5qE ڦl#8 odr[ 3/|f8sodWb1kx+ `f,3YtĒ*V\pl?~%?B*} }R<'"e[B4G!9QRn\ba'ƪ )B|raJ3/- 97ƔpۥJNHM\\.^VCsAVB=mYZHl8I0X'efvLqDY mXT27P =Mg b|z>N'7Ӌ\]Û˷ӛ%OmÄjc~4RP5e@&AK%U+氖\\mh QrOǩ4ʔ|UR|>'R` -sstAl Œ@Pb)W| uOi̦~RUĨ(?ĺdJsy&N҅RR`޲R1P7v{nY'+ۅESUS6rHB[{(+bld:#}pI뙄:{H0v/5?GQɇw7W')5&c/E H1Zjbק3SZW[t%Rʣb%IHB;, rg BBSrN}GB[3Gx)0Rjl|b`[/e&JI_|,=G>R60jieE4dpU3Ɗ &kOZ1}azM~L2"q /cbuA5ƪi@u|s!Ύ̪ܐS;\GTq/rEH9!ݨ6~|BQQ;|w.4Bv1<(Y3Vgj#4?DG! S~G1),(DPlZ:Z|@peOG< ^2~H݈1L 稺> +ZdvE 9jr?s_z(+𴮉*;i #+03=qu}g9lzOf L,#_po6y}liye_[WbG+u_KMbS[Ӎg.A4&gAP#a/:} eΫ_EݚٵV_7\,284fSjuYaoiGb0vt9tX/0f2ncZϮ'pb$etMx.P`;H66:vnIwqiW{0w}/gs^xE[7̠`5#whVNw >J)\W޵0j"=icCyȇxc`0lcg2)R^^e{ 4S[W0օYww˛ƽz{/ ؜ո y~]G=F 4 ֶۿ`1BkښPKҼ[Gs twine/wheel.pyVr6}W`fD2onTܱdR@BMZR{wAR"Ջٳ,TzgDr٥UyG#F -dVƚ՜{0V(',$9D?!NUlwL** H!,E )hDŽdZRpn)>6j895.Z9}~zlbe o/'z;YJ sc\/Qa7L xi,F̪mdɄuF,+KT 0U\xΦ{1O#0]z`b:kjv9]LfzƳrӄn` GRkZՐ\,*^+=0 f-,Ң YJ;sTFY䕫 $ W18z=ю6B:4)~"U$p`xi9P9o~}Z)ٝ ?uɖlArS=1FU"ԑARB}5a4{Ң/h[%`KW-8q展&K/'x'gv}49u\it_t68tOfP&:6Muf~| HG -iSFyևJO~KȂ~ɗ%w~oVߟ|OwiT|B%{"  fR$ -)fMz]̔|/q D?%)c6m=oP?¨m^uw-Љ.o xMEumoPąQt<{9,; ‘BaʬF?a"M-ۆHqEr=/+ BDz!{Lb 7n 3KOU8> &]{*r3߅~EƓu$"1i]ް㳛*_zSۯ ? 8ٝT==!Xd?{.}P70Ж:fd1/CXl X7!V:F#nYS=n7Oo8{){F$DJȐX#mrÓߒ6et Iå}T=nMf p~~qh{l֕uIVms`"f`S#k-\TQ/PKK?EN/UXtwine/wininst.pymTn0+akд=)iN)PieHb)_^l`;;Y27$IW2$DZGUE +a+dp ) 0z:~ + x|d-B i &' bTԄn藇Oyy}07 jxAa<_9$|E|5KXϸ {qn$<O P;SqG6JqH\7@K}¬"'걒tOg1A#,'<:/IeP尒팪4 [S{5e=ætKevayFZ*7C[ 7.,G^Teht}5dz=6.0=NI[߷8+l[ SȗNjNjm5'bXs? Z,a"9jݲ$4sHW=ԺfVS@(&Bvg>#?[ vYc'[?\ ˦7X]SZ+ROHt$s7v{ze&Fzz,scQh엏݅<pN5kpLBh;0;1H}5ixPKuE}KI:twine/commands/__init__.pyuAo0 Drvr3V8@ɐe!HDͿd@a'rh80|kg<[@m0 < 0Drn4 q8GuRD=|ȂvGoHY0g&X\-\JJ^/8 0,EjS՛/{?Z1B߉lOh Q#j(;vDKG?LHU¼pWes}sվ԰j[}dz_e&9o>LISM}Ոzw5%JCGb7g}pGh>q 4}Te$+M@'$Kub PKG{c twine/commands/register.pyVnF}W h&vŅT_PHN (9%2bٛHJ[ 9gfgf nDixsGXdT.&g3xsl5J;yr+S ⢁7fC䗢'B؋jFhW>j n+Κzgil\fN0vW$eR򦤓E{) `Sa5ͶLjjRF͖zq30s i:2#$s[1|h#d*Нbk=sL=8Zym0S<_}ҭ!)ģy&!p9xd&q5lt'ŝ:1÷LWY:ƕlOgG&PPT`J6GJwuz0TK n{jgc|4xpФ\Q's}_EMc3W]? pwڄ)圐G@p "mKE-퐬vW+vi`3 -˲3a~gWVr+JF)+y!хb Ia]gQoh~U;w8.zlSnT8xa)Q'_;:Im~@ XGMwc}39;M{;7gQivPbL RyEhR8ݞ3aUO@x aHDO"z5hA܏_h^_?pſP[j*4,+J,_5NoPKG.:twine/commands/upload.pyn]_q@#i'CB^5 `ĈQSvfhY sB-e*8s#Njmۿ{ٰ{ӭVfr49_DK蚒+0k-+🿙ï\i!xBJJfD ;:͑а5\րh)8lY[6H$~$0f*^ӞlیYA3_../QX ֠;PXll R;#I֭F4\-SB%*Hh*@r~W x~u?G~ί..n_=\\8 uu~̈́lsH~R yI|$J:t T8TՁHHa'{JeUg:Ke-Q=Q'Ad*Dh "-]# Y+V$PVU˔Ṫ2:kYGV4:$Q"6(̐|iIedaߛjVe8qdH\(^VCaqL(8 ն&#;tf ɗjCiPvнՔ*4aQ|;Dm)! g#p)p|mu8OegR !xȬ^_,C2Q^i#PCO2*sD<<4^{Mբ呖T@Q虯 GcW?eyXD8+q/ paKז`hKk7:".3’ٓM8 TD g뎗54nwWA?قD N@ @9X X4&LV˄ԡgS,pW0ҩ8JlZ㶢ަ;N7t Lᜬ@YMg}o|k޸~NjYeT<4m燇[x2hvar(@>#tKB[%yWh +) 8o$ʬ9͝i+)m#KJlACΈ!\(0&s!Qj%lQK0lpOq +]xXsUuTnM*Y-j9$(Q{%*qYC` "iw׼n %|$ٍxz"g&'$NB* thds:~@(re4y k$2SQ>d۟<؆zb\™.2XUX~0B@XG{1Ad}±Aν +ꮌ^5oy=(cUqP,fcQ"@8Wd emB2OCu%:=1Ͷ籸|ܾIƍ)Aᳺ! `=:x9E?X#]k-d.]Hnt ב`WK=4t,?vܜ ѿz0^ EJI~t3D?h&-7$yN4Ǡ'n[,bPKGMDQ=%twine-1.6.5.dist-info/DESCRIPTION.rstXko_qHGkd5' IpDYS3߾!)vb,KsoLߐq4~K+ېnTMS1^&Ul\QPqp`9F&5c)UUe=yNWl- Ll鍦܊{0[W <3pԪve7GC5q&RVq'V.lkwTb4IByyU}`$AԲ UIF rv\F9-FʚN+qG\ :]-^<)KVĞYEDD$^El%•*IWA"Vфէ{#gCktk|V 70`|нCDBYmᇮk$UD=K-QJ2Em<0y ߛɲ -+"CM֐#uTnBMs+ +i'hSђ??/l6Is,Y5Oa-\@ؤՖ9qR|GCˋtq17;04}}a1]RKWهDCIM/׼'![`?ag  ܝD .~7Zdf{EZ+4,@5cXxDQ2C,8}kBqKwyDT2xo/>)F ]c᮰HB'k8Һ{1f1;(Fb1 xαiukwCx Zdux,j˷4kȻAmbͬb$0 ; }L&PtWt>Jd]ڡ Y+_ٿ?!s\Z'o?꣪bMJaPbou:(#-^5cqOX,ݏ=~ Dk@&{{X[ߵ1Q%24|JQ)fb>w`/7X\GĂVP&^]i٨*-0WCr8^IsČ솝C14aHw!]44s,馣@ykpR)Ġ?}w+!`XȎ_A0A[j p,&xŷLijQv/1',@xWwy] RI,^+S38i۔Irԧ 1IEbX4~w1ՒcEf {Bi]Kh*y”Zj*>.m-Jd_8pE*‹U7p܈@@8a73r4HVXhj6wQtXovtyW| i 9džk!k-xpCWio JtJ5COf M_6ɯPKGR\X&twine-1.6.5.dist-info/entry_points.txtN+I/N.,()*)KUUz񹉙yV +"ZY\ZIC )-OLTZPKG28Ȫ#twine-1.6.5.dist-info/metadata.jsonUkO1+}%E*%HDIDRNg~$D(wȧެeKp^ XϏQ2ug<Ss<{{55 |Q3P=~زG%"B.]UV8 ր^Jgt :_ 4;=e;}~mmpA(Eָd$ֵpkB΍RPjf,d8&u'[ɰ`dDh$qaVJ@W{&5VRJ}YQ,].I %shs##4K(Y,%`e,i$"b<3kYb˦fV=DN(v)tEՔ_JI zliLj?b<}|n6OYͥI;pht"uiV}LkRoO7i~@+QƏ O :xGsî£nQmf9Ww9:Kێ?cDiGR2pƅj6;pAA^-{?(Jh?-6h(}ݶXTDUoVN<{YOKiHחS%Zs)h|ŝS̽GEhw>ةժIxٌNy&L˷Re.TUƹ1yJ*83ju2e_LgXd{Xle+[R(?˫ӿ}:Gތ.T:MntiهF-{q6@h H vf-4Y#?Uq𣪊"(;`>}SwC߸n2|74Us&s! uw;[85٪OzVvbk[Rl?֊d{϶x%`Zz`&п6i{k|KG탗o^#F{6HvMr8 Z@^]NYyAw) .VV+ЅΪ0QT:k Lm./χ`A)JFBՎĔZi4>hJS,k<"8j D}￀?cMM {GNjTAMZ0ef.3з3Wah3.'4!#+3*Ac8%W1HȜ): >vvi Z!!c:D#bW~h])9FX +ΰUS i9;7ZDQH53!nlƍb-4TU +E3,oM,G/QjUeE;rzU;gV6'Q>kκ+jax:]顇G̨L-:%"8z9l6)B&MW~#=E6yl|Vj'\K ]#>xDsء+fn5X& (\ !ͮ ٵ*X6p+>ˆ&n}L>T>@lLޜ$Ȝ\Fͮ17G H{Yc( ĕ-DŚ*cC[z)Vʙv=#ʶ4L:=u-!:Y:ڂYo«ګϽ8yqLP%/5jx|qzxr}ܫp-d<'a}$ID1, h}4|_l\47z;ͼޫ]S&f:)$Z4fw[+j Ĭz[g{o62eO T{ 4. j+/kWۚAynDّ.x@wu|^.(8~!f$nXP*3LN8Ht ]K.]nJ/2w:RM~7xLJLFE~c\g+]LG ;ӣ #-;1tƤg`d 04zBh&|hF>}/AK=nBFm+'-GNU#yLߴx5O3!2F@䢉.C eo)@=y%Z/7G(U( ӗ~>fƁexn>+e=wK(BZk e˓NdwKnu+h #,+xM<׋an.?] #h!NI?[U7QiKE옦$REn4HjDUA.GN6X&p@vr_sME'M \W7يOo>Cz GN+0 M` (Z1>jA;7Q5yr!cA) ; c FRᬝ4xPйbǖ`o J5^.?f3jh*1vOW{%}{ $m߃PKG  twine-1.6.5.dist-info/RECORDuKϢ:$O..(" nH&J׏'oM7MoLEQ Ev" `x?yqLg0k]*|=Ms?QcEτ`P*\.*mNT(W ,mai~L cPUia²ڶHFl jo!1CaiU)zs[]{{+1&_]; c^KIq⢁vc`܆񪑖UJKyZFkj/?:hNSu3)捵 .A~Hf9|`RgڑpSq YLYJ ޙm7hjR)XȅUô}m۳ G&|Pj63(߱:_  Dd*/Oz!wA$xjՇcz]G1N=FԂ[WYVU}8w]c˞`E*0 9'H|'3=snta n뜠{wk_uWLlrΝ,u{$&5\B4/zϒݩϖRfsЍ60y{=q%GVgؾۖy2lH?$IfqgccDnԭvxO?m/ϭ3d%pqN/s P<G@N2)^}7كpzDqЏiE<2 1 7]_T]Q,%@f7,Fx~, ,y`1~/K_㋺u0t)uw s /=t><#Dk1p!Nk>߅&`wz,Y<|waq6wwva6,rx \렘$&N0vplT՗X;ۻoo%>0w,k_}gעJnG"7[;V}޳,!H>;;=|:Ϲ'q3 C 0pBo P 90n~7\禎LbV ' TbTYMExGn'.(?;0,r㘔Ls=qx3pI3`QPCY=00Z]@97a! =ҁmkh6{!B{I0f'!98>! .3x>̏ШrbP;ypd^ f=S 0+[E%N6lI[!xo,, g$^G4_VFf>z@ EE_Gkz0>|qjs6q( 'R7p^MPfK$>(Jz_AA4B6x>A=Ɠzq,h ߇:ȻA MLyq ɶiwC=JC<3,%{hp[78r T(p'5bxSNj_`)# ]-8_zkJ3wZb bE^q-iáٌ?z{ `(a;cca‹$dC 4X]Qw,y|qԨZYNލdgdVc^|x@^ggf RAMv`wE,1mɃ;H҇ p=7Ns-"nk~W. @jEI"Ϗx\s\f/0s``8b ' IR ^o?|_&!1OFo fT1彦?|dr#~E-F$@xKϹ7&Ƚ-juvmˌ IN}70p0"d,yƅz=:m8O7z&.K9C_Õϑ4 vClT'™' ܈Ȳ=r^y3t|6r~WŭYHs#T ܬh*ȐNjX} ?G#0mcQfȜ4FO7j P(RD6p,dƮ~q[г LilԓN Da4hg-&T:*@L?y{m1$a?q7UzݺCA):?T秶'RͲ \7%o }ӂQ\Cɕ@ľ*6,T$$5S+ eyw-a*=T_%5G +o[>p5C=h_Q4}%9ķ P9hh 7Dw}x)?)mK b#avn$tKjPӾ@USNN3wg6tSh]!Ӈ7G3!"эn}lFlRSh k;ڗI*Idu_ 0ÿ +u(bj' G/`Z_z8 c)|s$)@̜[~'&@6 粃bՁeP>ZQw4V )QMA|AV-Q5h!DrX?Aer?)VrY2E^B6? ?*5,fCcl"BI9 NUj4L|=T{uRHc&4#LIg%'ZWaCZ}m.LA 7 d،HnmX (JI8 h=ӑ)D˴s"o&IxeKxsd`BuDzwHb>Ee4AkϫwE-=@KUfD $7ZFWHji~lfox|h.{bڲpMDDUXh2)>𬋲7$UBdX/޾8x䚔KlЗhioțD׈-\kdwn ڰp&ۤۂH~q:HGxN:\o-!zd0 Cd^;e *vU{Q/kc̅L{N翦<,IB +Ͻ|SyQw$A*YxKJ$HCLaa'R F}>xޥE{|tG٬)l5U-IclRO7xNԦr%5J@ksHҚ|ŒTP4МV&¾q.ܿ?X,7]cXp@V4g#^kU^dkRX hummhV[mf4WMriՖ02' ;S7PMB(y7pSju; H$(d0o:5tb]nLXe]NBeFmMc[ :yIwJ.M'eB mds)q4Q'QR{:#mIh8 V( & i&HB8jKB,e82c#I&6H<@"G =uhl_<3ͽ ?7I#hOEAmq3/Z8L{w\@L-S5i^:e!"+-YiVv)>pT%DT fgUi")0̙JrS~PcdFV=?}s t\oH`h'!fٷ^ Rn8s|ALB!D%k<=2 6]E )U}JDۇ7z BɻY0gWjȊ`DC~䠵ɨMlg_VWb̨FJ6߬&~\|m؂g:ϵj,rIWb"WHо* 15dGV"zиjؖyW-WzeDz^adܱ, s3tgɤ#;#ɮ'hp-~'',닮ƱNZ=Yzw/^W,x;‡S积7/VT@0% CU5xytχurzqS$L\yxy*lwب+\gy0s} &hYؗLѧ\=ٲODbK4U'z'w0 Sy3xyO*JqY[MJ  ,]8*V/>v`{6t+h]*ZqCyM7`fQİ e'ǹRʈV>M7);Lde|ա{.Rug~nhx "Dw"yUpF%wQ>lӍC?zE% +ehڱoFF  Y<=QWrP+_/a돾d0nx;MC͗CY}YXK뢄%C%-Ɂ[v0DLT<{ zZ-e?]E4Evb#B֟0‘i]Y >s#44+lXkՒDlEKbKȕI=yE-E:*Z-9T9}^|V !e0犁>X>V tP$ _1 .!Ufz&{ q7)pG: WK v[0K?r q27K/seO&z?2pVw˝xxģAl0dF`"yĸMucHVa5uC/ʹW۲Ǔ FRF,EUi~( P> U IRQFˊqHݤ|JqY1ȝǁpkͧ&W-a7ZAZrT;]e+VxUc:V t`OV.vu|jݪ݊}\Az_"džN1 neh;߷I# }9o{wo`[o5ͽOl'ÄׄNtNm+]3hٷE2L(eQŠo!Mxh46׆ζ,_fuӂlZRPZ2[?h0nX~aZ.l䵾 [b≼ʋ€| EY[]a }dWbÎV,ʫGV)ID[EpJKjKRfnY(#!wð3[yUڵԪ]ѵn6|mlm-nJ#ws ԝAfocؼ !GPO[YC3Yi>7 bAϵ҈nU- # hnymFL9yU>CbzŲWjFSьF}kez6k h lVlSRX[ 6*D @.u4n:1-M`)c*h`˗B% l}~bKIdͦtո"~$Kepx*VEY7[ ™Z =)7ovl}xUȋx/ERX͋5xjX 9$6df\QqBi-$ ogys \$J@yXbG.cL3;f/ėHbui,OWUP$xW[@D'!{u(PLJ yoago~<^^Cɥ ujק/_ݎitmOJGGqv+ S|Cx%dn2z~۰A*i+B KZxLk!<̼RPfUr+HS:J5IB*C3Df@O:0ɛJ4(X.Hq\ Co^~_+⿶ۻu/+?3g7kֲbOƒkۛ ߸®.!o@]__u@|$ްóxqr\:9A^_}XVj["PAS;^^C7)v|iI J6C$:%ލbg2HFh=/:XAЀHi:D!Y+/.NYqr]PAykw 2!5Z~ 0n!6 "TQ҈8Q7dt΃ 6R:#,lKYWU5ҌTdA,j^am ADLa"{C8j. 3PDup;԰Ƶ$M<&#xxx(lR4:X5a>`2M)W(0~*us*~zDr 9虈i&!KOQifǃ#M o 7`Ddli.Px (Y\Qr4$b`FۆZҹ0ՊС1JbJ`8b1-;FEJ'Ns 5rCCÕXrhi 5FfIzպ_$=hᨡ487 ~a.<녨z32d 23@T*:Sx Ax|v)Խ'%CTD#g`cX\X.$ 9+aܐT10!יphZ-CN7u'&Ŕ{%n c CPf!oFID'0-~@i;LC)Gy;Y?%drVFg Lo*IVRMDc7A&9Q&$pɢ9\ K f<8ƬIS/^1$1RjmI~<"MhOTcz"SC"$*z]tlqPl?\8ULxB`hZ7Q/p$ÐĔt< ziKUJj6h@dQ9\pP)05o˲jL6jDfNѲa4?DmXZuZT 4r*8kQ@͂W,>&%3f#$ Qwɛh{Oˌdÿ)\s/YB.S0U8:BFA 5)ɍLƳ5ȍU$t}JKIBjCO I(]+)+{ɇ3j\5mnX1B]xɇ3#shjhı-Lhp _L\%՘ZY?f0K =Rƾꏔ{@ -Y~3 :>G R"|āT&@v? 7U>Ơ4]/ sA*MS#!rǘǏd}~;SA ; ]ZLq֡2OXےcC@zO \!4Je}I ֆHs{Pw8$Gs# "x߄Bz%YGeXV%L;{6&& Gq4/X1$E5iHSHEtzrS k%j,F< rp.+s'.㋣sqrf?_6d.\1T#&14̆ 4GBQM..6ǘ÷ k7G?:8><3l ݛ3qwF>ѐ`Ϡ;OsdCј9Ri )orEݴ; HzF[禗#;JzN& y2u@*'5w`c(Sb{%ŒNΡaz.[/j: EoWΦǿ(/AH=3?ub7/80J,,$)aRꢌM,.iDy?Ys$ށ(vKǎ$إVj®' n =PKP4T\t+@5ҽL&p 0*뉞%郮!@|A=v R,'m=/Q [vieNOJ«[%jAO>#֩ltD!-%B(f Y*E:%;Ս|!͵^c9M"Am3Ԍf ܲΙr+ʹ&_fcJ.ujddiCa9JExYrF`IWE0icJU3ʶO7.,u{u9Rxzge6p <&\4 ׆N>~Ds2Dn۶i1"}o|],e ʄskN2o1I"K'g'G'g?㋣ z4{jgo/-qCs&Nߟĺ-^ke8AXGјSu rv#,ԇ۞>M-q3%S=q_n$C]%# GI|0"Y鍺M\7^oäl.EN ޜ%-]vcusZa7:Ew|j.ȫ]&HA.RBᝁ0[P &; t$ѝaP5WJa!h&-WGo kiU6@^K}]\y -zGg/Vw `n7t` @B(x@}OAh 6Snv􃗼9{ҮaSSoC\-ϴJG}69ӌ҅L~%S+DR ?lVQH ԅPsMiC>g &@35;sqLLEXKAV# V~nIӽf[3ؑ7H5r0[kBMF|SԖ;@m["8Z(~U\JcbI@}롱O<#w| 湨#5="|(p"ؗ*G*w*&t:I<ÓXِԥHHOS\hT[03bif\RGzgL%) B59w#zC47 L &+C+Е~dVxT7cO=d*Uczj*:W\X/Tgn!)7@О}hyOQ)ANͽn@@) =ւ7ҖOiG;`tG  ҈n DL",Yxa8 XGۄr¬~%ӛ1n)w2CD9]royV@I/^on, uQ8 (Yi${Q־oy{5Yckܿ{عg#b'/4v3,g8G%翛{;_;{;[mhCKf9'R~wun,+sͥ2ֲs64Yf{\*?[j9ϵ,K_s7&ϥ>yΡOmM_ϗzd>GG87y'}΅><@ts<;XVz~cl ?|2/QY>|e9QMn9/|1EX|H^1/zsc?W_e>H빴恇D}Ѳ5 ʾ7ҿ@ Ceq_Xam^y,lm/T$&7I*3X<m{L50M{>)4BͬF%jD[ZP\d@ dK≾'.oF)S|K~ yյw], }#X1*:)fҐ&O>5 g:w *}ul;Ƴ cH=1bAɇsIJԯE+3jTJWS)]dn%{F-w &>tڗ♨bΖ(Pu ޠY:OTB9bxv\P_*(5wuo^&0Y,ܠVIq+uMõ,ϳUsWӐppJHm 7wN%Uc^cmNΝ˒έ8uV;mܽ$9ܳ{ {uv}+ܯRWOm=uCπ\XNjŖGqEIB_SHx1n ]]cpT**"ۅ;hd*b%&_ ;33-nv4D~?hXuH'ArR\ͯZ|?6io7=(߿x,*f d}f]oY&0RT%af92GtXN2tt,YGwAo.P|H?{{:27δ?t 0pΘќ *"Ƌ(Vd+uʳRRBi L6pUR`sن@c&.M$ C7A^MbV*ς9#7^RO)FNrepA` ^8"\oa.nϒucAǯ.ɪN)}=v(U QM6:?0Qe6:n%xc) )jҤq@>O+6UKjj]8:WyFMT pTijU3czʃN"#L 9UdzqpZ( # ',.tj8BTIddMd]d:]}&?!>ɮ釀q2tHR&4w WJ|j=3b_{Eʒf͚b>DO\A͝ݽSaniv.&B7({wtƆ lSa5SځZqjҰo*xH3K ~v~]5' }j8`kXΞƏ ՜*p#'zvatGYW y&?Teƥnwj\><;}sp>ePø6ookZ{;ko-=WYyXɺ7u~OIu/|¶l}xK,%؛٢ n įb d8%$;BgL3K:,SKKGQBǕNmPKê]T6y d;Wlgi(ʎRh\znS;L}9{#/Cw.tE&q֪}r.&^֋^%.aR=dI њwgasO}mG*Pv?! D#`@?djq{ gjAX1.Wq7ѕZ2 =By#?B/b+ïLl-nz1hhf&c)TRdtc/X}p2C]>\ԥ:s>!ghoP+8J"]!\Gų ?~!Ruq}7(`;.ha,L,l"<)L"E(z̮gNUZfƗ&^?;YH(v_:xkFKUb5+T˕Oф>&ӬCiԼ3+[>x?KU ^,ӆo N¨ WCION#`Cي;ITi\6];*X}R*,DO)!>~'-<&NooDu١+…6X*X]zztZATU.2K'jqt꾓}F HHGZu{,nj>d<2+w A@_5٭ 9嚚R/I-R(`*izZx  s5elhxkc Ư{@K9iKmu%7w6wZeo{{}oVo4dޮw6)hsSq09~Ҽ%$ᑘdtsG~oM9sa863 ɬ @6VXyC6#?-=- ]aʃb#6*UGV*~ Õkڠ uCv H;>}c<2)DWf z@X7=Vç5,8p0bZ w1ճ(+>eQT'N2kqBg[_fym`S° R+"̛WE|OQA*UuX: D~{#ot)1G_(:ףW;;~쿭.0n1s^\AVea9Ȁ:Tp>\z/Akto##j #g4f 2a@ ntrtG!a#'h>"ЈLAa(rȝEp>g3޸Z4J;&3g 7d<;{cxxW:Ēxp+Q\+ < J I0CY 3e@a[{6u[Ai <41 ]Vd!mEX(@J6b* kL) !ZWN`)Me5 [tV$lD9ZNMm}yhVQvdɕfSk8;+7*n)uTbQWDd Lad*!|xˉKktqc0;==;-pvY#Hˎv3p3dotihň CL-$X*kwkEWh`wݭݵZG;%|n>=Ϛ?:ϳ6̼ Q1112 9AR^U0 ^#]͗%k8m/ާthL=iV'đ/Ӵ.EݒwfueKhZM#f)V7]ZLhGȯiSBYKΈIV=6juÚbRw7,;asPEL;Wx@}X R1ǘ2J Z:\6s14Jjs~t؍aӡF|&߽trMOojW~m!JDR<hbUA"mZ_Enw3^&<.P_mJƉPV7 BӹO3Y"l#L8s?QE65{&3VpM6)*Oիz\֜3:BlxCbdawt>Kkr{(ܧw/Yi>4 -7+in&e`wjW" DIWlҸ+'/y}#o0.M2U~;bcY$^Lȳ*DRSKFZ҉@ 2q~5k,%P(RBIj7ʯ@]rvoo=~xh9@V-t[{kVY颲hۿ,}rT1`Q(߼@Gg[1&_ y*N]'X)ŭ*dlQ.eJ㛎hBj(6l6H0п^eԹXt:oP6 ,`.L +yn\HzàB\qLpDY]8 bKO@b]+MhQ+y\}"B((W+jkY={O9OV~9SIքԫ Uč5L̇1ZZrYAZ Ϊ6㥱MyUrKv|[Kk|0ںl->˛75UnhPHXm{-WOI8uAs"IN]ޥcםFs ~\$#݉sH]vaOTT?1@C540"p!ޛ(7\܄-XcӼ|{͸ҊP-a݈{CnAᑃ6SJ*SGJ'aS-`2%#LMAZX?ș8dB}1(Xzp>Xa܍Y7k}\ ɑb;! z`FԁM -E[aLQDg) n@lEEσ"~v h2%(.^|$/|Aj? iϭ͝[[3Qs;)W*Cve68QH*9辱Qҭ^ӓ UH8#Ş*nym;n& KOI Oߝ$zנE' l =x%a/ $/c,/ƿ̇<h#!SԲ(f5cgC"V1pudηi7§ڢUS&}\Ƭ@5ePKj&٘xI%o3h7S&h2C`)@]].OPZ9WvP0qJ<0gl/ٓƏaW4[XL?EM gyF56ׇL,S]@%Sè. UE ~G|]IM$=Q%u<->:ɦ)FD.Ũ aCo3 031o\򠳓cU.}Rh8[UIY1o[2İD X3?\CԳ),xP%.%LL_buMˣs*,|FyXjmMzF>2亘+&i9[%v$TULFRw .2S#t352U@8jz"B5~l䢶 ᳢L 3 [2m z-qC$R&U{ V\W8pHƞ[5ck V<ٞK5Zql *#) ;Vj+ 'TP!I4}\ 73Lfs4OS{<7TG>n*;NUl H[w]խr˫эj$to};:aeyAMO<#+U!,ßޟ<^tO.\\>T2<[_ʡA_͂Sz#gb X/gsci壂ҫ'9;]f2!_Cpo3 " Kw¤I7u Q ͫRxW< džBYn9zobeFWTLԨ! A %CluB/OA`45*ρ+ldI`,=v%u_fg +uUo+*E( oXk*-oUH7\lXF\Dw4l*9Il{Y5s2:ot}c y9qޝ}sJ̯[!ؒu$[*{KlgT:p5t{Dq⻔0cg&'z)RVp`H'<ɧCff.y3RS@2C.;j4$5\CRG"m2Ҭ x MK!-6H"(}P~VpBSZ2\?6qtu,7J[գ=~e)Ko8fMfLvBGj].Prg䀓%d( N% Uw_ȍ}}t""}"[bkVǾI Gq#*d}V'oxTpkR_}ьw' -PEFՆD=W&S!{^l:ZoXuˌ*@T%DŽFSK~_ENk[{k.F:/ތG'M{녅 ޚ72/Yڼvٷa<>xl~}x{pqqvmw7)n{ym}z^*<]r8\fh3~@H7ךt^l/x_?ޛnSC ;<0u5sF F(E>Ue]ܕ7Qw2i.`KdQ}-O/2`S6aS_M%"bZ:umAae0*A6V!Kl#U 2wAzX5Dw,y3Oe(B6k9*ZI-Bm>7[ hˬj$kJ26-z@/h3FácLV^<8ˬ#`/A7jj0ӵPoƸ,V-C2-/,1 ¾(v6?B/ e$ՄŲ0~Uæ1мfp\J عYRVPXݪyu.F5Aǒyg}l0w:H9Sf4>8qQa<sV_zi[-jgow}ϿOAZjxUgTy6SCbH n\{@W`[r#,*d .6+U K\I񘴻J-h:D8k z:k*xT2%MqB[ۛ_{Ż"n0X|&?>kFKֿ@ם<|er[/0AeKZ|iqZ} W% ]wU/- ^UtqnDf])粣׿?^NC7^-e+уe"9"z!t'GLK2:"/I-ț\Zé؊NhEMd}Q8mpZFѤ(1zTBKz,tV9X/5v G_.8{,9Ea==|l](^.vbEj@X .&b#H&nI#Bs.#ĥ4ӯc ToL9LƨCP `ib *D:.f@LԒ2FsH.`T=Dq=`I8*"óҟ3jۻ0)h8wͶg"J~#72WuȇfxSvG,7{Cl#f!fnPޛJQ>F#whK9DMoγyވn1@LPa`t()!snK7`fx9bڹM6,&%Z+bbĤ+;63i]rMm[U2SNxSf# 2Q2ܾb^)j)cs)2{)*^LԊYOQzfUM%<{ħ{vR2L Q)È4$rеg^4H4#B"4y XW| } C1\>&]K2 Eԡ^o}r-mˑ H./IxRJɬc#N]7P+4Ervpdۙp ⼸[Ƞ/ *$} *%{mSf;L<)ϟ̛ /b3[xreq'Y_0׎a9^Ii:XmK8O"#ElZ_ܘkN2o1I"K'=N?8zyx|qtA[h95ú[h4_۶/IfNj?2& 䁽Sv_n;5Ǯ[pv,l̐Ay`;7S7F¡-Ӯ0i3.>/4;#wcuMm`%)wt5aZU{%.RBᝁg<; *ػ|tg;[:zsXXK^{?y |עwtBkun7=G53ۿxi{ #o;X4[7;K^=[i0)=Ô;W3 Rfw0 Co[#GN07B(KX G$eUC1lLB9Z@]o 5$8,"SAbp%hC1L'n)1H"QPŔJs;MX_e+q^-3Z#w0[kBMF,vF;@m["8Z(ƼP^:xw\n}*uqACcxF!ߑ` 5PXKCA~yVt;6'tx&J%u)+e*WQ)H\CF#WΈ}OHl7% BdwE$,T*47 L &+YaH+52C[!P"$<'ʞY2 *1I}Ύ̙C#&QvHqEt24|Ә/.N(S-+{J2" ,|*tĻO[/t~h :( (DL0LYuJ%fvM\SE3ՏQ51n)w;4QI˽u'闿Z%;/ߟ{7^0doxaxEi#vfDI^[޽w{غoy}c5vYh:]xМΰXUOG[||MK%m{xTYզc,.RX ~+??ywm뿵ίz-z{p|C;^`4Fn@pLuQtiMw.ٰ,YXd=%'*"}i&s2ιLža' u|pm\H)Ǜ ̄tbqy5Mkߖ|=[wpnqV wI8/7_ +8XJ隇&)ۼokk-????????q0././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/fixtures/twine-3.3.0-py3.9.egg0000644000175100001770000020055214562147542020431 0ustar00runnerdockerPKh[R]@g EGG-INFO/PKG-INFOVMo6Wa/m+tM,4ۛM# E!%[M-P 3Kt'hÕL-!!v%.hʒ}B&JH-~"*#r-C2I6˜T4}9bJW$qE(0xtltBJRҺ,JFd|˙JZ7*m1 ~qtSͿF F O Y*S8ʹ-&JUW Ti/|@ƂZ0$"ȗz6PuU{&#`fbMM-ƇBǣ)Tʆ]"K_ܼ#,Cv<66/T`[go`T5s|xej:td0Ю~KںP`8m;Xq4*U R6H@:>SވpCh#) (VQ+? =iY{mYo,NA@Í$fo,^EJ(ʌW2t ̆Kcu;oj3!t"7D.GAm} %A0M`JX;rArn"X`g.C"1QS1R69m˸{ lA 4bUǁE<–N n`N#D尫I% MTa |ِ b drYjsIPm6!~.6ԄM1Ƶo5D+U6{- g+ %Wh#ˏd ՝?0asߨ7!')bɅ20+^qNh'Խ6}wJSv.*l)Nv` ~TDor2̧׫d>O>G%k[hp:4q  (#Wq(xLM϶}y\U^߯pp*;Cܘg[*s e[g74 沖y|>?K2dje Uj/'!sMһukRgs.J?/llwgzܔPISOx|xI PKh[R3KOEGG-INFO/SOURCES.txt[s0/^&fRfbBRŶ+ɋAYS%0u  `h %tRsLwZ* x­2HXU'ͯ:yʯ[wh\_ab PaEs0ӌPS{]iwoci+Ʃ=aЧ򢷹dʊ?PK QJAItwine/__main__.py}Tmk0_qs$:m-MiXHF쮔9;[$ ;%/6Cuz|iXh5\q1D ye6R|vFgkM9#G(4kT`6E '*ͥ+z[n 1TU B#Qp 1O-,O9Bͦ%⩥+*>koh8,cROd6(=Mo&srNjkHQkP\UrILY RKҞVl")BbYsm_ĩN%| ww؟xxL'>,pNbN;ϟt~;$|˕O"0D@,A:LjZ%#ұG ՚ nڸFxhjA7!QQ u#( x|#X"0 \)OE{L/9KTVPZp76RAaV&X̿W_K? Bsuss<j=}uÎ0J{Cpi`SlLm%wY.3P^ٶN+oSOaQ쑯Z8FxNKtn%|L{q^kB{@|\UeH禹ݜjp7 8[MoBAc826J|ՑPKjg[RGA] twine/auth.pyVMk0Wb%)rܖExWԖ$֔dKfsHX_o޼6- + 0Q k^UUvx Jh\:$A[E9=-\B$Ј#đ2bȯraSX2KW3a`APdGyNeߎcʨ823'I]&DhJl7-͑ѻOfyة`at<  JZeJ#i' rD@φeCԞbϹ$QQKm^@-䛱cZB ,4"$臠\x+xK) hŸ zWjUQXj{VM+*eGǛCZy'Q\348?D2:W%Irat34 [/ /<"'kksʹ^=sv﫵u^*\*:M:'!ц}\!G#|nsi(bcΜODaK fxv w8];g V 8Q3{\9߀}RF%GƼ^sA8k \,O|_c&o27{ ;1$@ݗ.'_?{Y;z?]cLs%&tЀcuߜ2ZQ.pjƖ D]FÕ)-)t!S¶ISyvRO_PKh[Rt twine/cli.pyVQo6~ׯ8( ҵo.6\ x9)'lf[Bc {ZUaڧiABGțժlGxm2f04v^j:OCbDƀo T"29{$ۀJdґhn(c0re7L#X{ԩw^1$YtƄyn 'dMSYzi&/l~R( ~W' װJ9_eJR>T(57NIC Bx-~燢ImR1m0XiY*&b5mLaFUtk{MoN4֜3+e m~+}.;Ʋ"}Eɜhh0AP nI MJFC[SLJ-5RB"9ܓs|܄d*J.ALPy6c0DPQ#Lndxi˭Ȑ9G5{xJ^]=tysIU7{OfSԚIcb;){LMwnzIh7ing|nR#OzG:a?$a_F&Wl^kxX;1O1M3:tyntvkϞ?<&lj[oxwF~HŠ"coa[@xh:݋ihiw/>t;F_^NiODVqrJj2k*;*R~WRcfu=5Vjd3oIcUclkIt3>#hԆף7)wiӿ'q.r?1L\?s3{2vTQԞ>yPKxPi񮂢Ctwine/exceptions.pyWmo6_qP?+/6MRXXhDj$b+/n(Y${9&rI^(tF6WF;B9FiGQ4xA'Z=~Ï4^FcѦ%f;߫DjPTZkIR$Ӭi| yC,E`ak**ĖT9 Jq ̕Љ1ħƄI`o}$|to7M,8wӓhKK<*eL`q ؐ$2+ X19a%y+T o@h"t1oK8'lAs:9N>I"Op#oKRqe [H25 WDTtVLRfn )-s5BByQQP`0HrK&YKa4z3 |͌D(N8yI_þ۝ jYo@.|$QOEL~FU b73=V8 G 8_,M$VEB"sgk4Jc:#Q.Wl3p*R(-?m_ZXFl? a oײیա\H6ix)a|)oKe7N;ݫBÂEuS~賎⿍n?wn/MwqU>>* @/ةu6=fvhS(&rQ⨿Wm4`n,Wwz;zg-!57*m;$1Uf݀-+fX83u%K9o)mRjgeh]aKޥ9Dۋ;_7o< EJM u >TqE+h=X hP]ICgy~YT} F;yM\?OAN815ͪ)zS<1srd[L/_\kjK2}OrX\P훘~Aq=ݏBׅH>¾ 9ަvALq塍vr vUhQw\ #o8#^|`v UQ;2Em?m%'!$x{MbrWi :*XF8tujtHXF*]G\*lq iRSOoa 7_1>3ʤg7;1[;OC'Y>Xd-sڎF33zuY5.0k&M"wO|d95 }Ŏt"zEAOH҉ai(0cD씢UF'QV7b'{ǵ?PKzg[Rh%, $twine/package.pyZoۺSPuWo 6Ϋ6 u]D\dQOEQDRRF:;"|.WɌf@**c18kbQR#Te㣣nR4(|zhM2)I\ LL1;" BWR ]KDe) eұ `+0sp:'y@ޜg14~wysM>~xzq=Glv=srzkvq6& ˰@ Y3X Y̗<ՖY{VdYwR|KZ*JKp뉢$k*)_W.̓Iny!b9XbC}+3crA7,)xet ecn2*ŶgK1kxRǡB5ci}d9+ؘm@,pc#558D߱3^@)ijuj~ǭy5]c|ZH ,ܭPK 4zP +HaA,ςmv^%r[{SË}-ŝ1DBjkX|r$®% c0$l(w ea4#Ofȟ1g d RGZblMQUo|/bu~Dt,dԂ :257GƗ{9j[y] ٧d쭣`Pjs:[&7 uUz4Ix ֡c %6OpfbǴI [M( W X[_(=D[Uj6p1я '-Deqfi;in8Z(bFgKޙ&@ _Ifob[*r1'01vYO}}cF&B"o_#%p[r+aPgaH5KH=,oj}mg*J3724#"KdP/BY`k,r jPI -0Ʃ4R9#X$^Z%/ Q uVG{T+yLC:&دc #I0=1MrpNf4aMf߹hkWg=҄0kC;uo:af]&J-OWDz{=2.DyȤc} xshm00JJ^V}kʅiPۖxE_$6WMl,47'd/`&/5o=ox  Yg02;{i!v"ġz_tվ^|}nXuѫZi wwxU/&'ǯTXy5~e4%4I.p X{cd*V<JMt?3TɆ΀'|n*%TE5rDXB׭dug?3 ;!Qq@gWlD°S釳MvU뷺֑M1+TV +;%{ZVw]0=FՇGj'{z+'3kUF#A|qOf MmK rV?0*I@[xnZlGٞoaq,@zV+DcrQiغLؑٺG,s6 - ?/q/Ôn =Ex6=?y9?~LGcOJ7>U?`u|[g~ǔ+1濚jlX?;4PKjPtwine/py.typedPK4RTb% A$twine/repository.pyZmoF_?jeMQDia+ȕ5eK+~3\rԸ;y}ff#Vdۜ8;E.i H GX*YL4f9QL3W2"b"%+?VdM$ .ɒ'):K8M#F6\=mJ&\ E}WKP~T*fP-i ib˷ H? 쯂bKhDt"&tCDN*g ɹjDX pT9_a)+(\yn<>]8'ӫ\ߒW.WpL>/ލ;6k $G vXC0ɌE|#+]tJ<<uH5I %kw:JO$b2k-oO툼 3dL "K "*B0d:TB$ (w' _`dOX)hL3\IE `HmxJ%8{BkRiϟo݅z0gWdBv*[Ry#E"U >~|Zu6 zjgwO@x f.)X_8ԆûoIJw/) O7 Bmb+X1A0k`%&+741 3|b$ESdrD1d_ȕH!ďy51p AÓ *p9" 2K K0FEcYV4)9ȉ1D1Ɛf> \ߨm } 92;D >;Lz2!6c0z&z6rj @9HhfCϘ, {ЅCHr$Ҡؤr+ʣz,xHbtnޣ%&#!A<8 5[ 8 ϯ]k8Pz+%_*e{ݽ=j}=$|/{7E-d7RKr՜_uC|y;~112jt>vxd>'Sv 5}ba !t.@-RkB@ #7_"U0r7-R e:Z@s ',4&qb@?5e*9MSQ(S4c]Mֿ`?mv&\FJ!5Q8fo\QO|QہXjE#O-l aX꺳O5y6 6GJui~~|ՓWX^AۛF/k*Ch0)#Fo B E[d1 WS黒)'5ߢZͮC5BtT,Q"$=סlAP~ۡ` zP{A,CnB'{ kL$ s_gD'i k6h^)uHP̰㒧 f;"~G!ưŶ- [߯6EUAQ@T[Ne%Sanl<۲()D_"ʖ0 YzR%e%m-KVbh{9Sbpc.o-Ed2fV$ ՞ vycI1d(JD"I 2[Av[jP/@ۻmW+L4vJ`Xρ1"Q<63D2[_Aj 㤔98M~>sޚ+9ضd~ۻ\9W %Z9bBNŖ/! f`h lֆ1w:"&; n>ިKM½AdwR꣖e oI-dqqJƝ nz~uOMSQ煮S'Vj>+Þ w^\M7=+h&`b̥a~K]2vr"2*|D{AR^d? mזG^2A}V[0CqZAˠ~!Bu)G? ~BUt %6jG@>춀VbϞoc^m~m(oy/M*m ,2qN kGIkYSzOyРsop߲golD#JG^F+#{Iq@|`p _rǑwҶY*Ǩe0E 2Oz|UC}^@L콉i2-L}lSwkI 3_՚>?0(.Hvh$4gf{sHĂX9tDQh@X}y75"+i1jj%t6xY>vnK0wkCzɯ6Ws c*x;ޣ3Nm?EUJw'&#|t☱e bs_ƈa [=hd>0uZ=B{Ϯy/̊Ѯ!Wf|лu6֖΢6B{ ʠJVl;g?m{jk>P|vswX9A[O9cqїu.}]n='5_rl8,ہSEWVs\v{r*+uRt0gԙVm4f_u2TaUYWΑ˦&׀j1c@}|_X2_bVE|57p<ţJh x$ƣd8&PKUJQss 41twine/settings.pyZ[o6~ϯ T-P‹uϬ'= E2h#Z߾琢$Ì% yGF"*q# VT7CgP˨>!/*7 v(up4=4Đ\g=M$Rq2ƶA2.cűc0Wq"΢\W\o" lٔZ,px -/-5R 5=7K#P>O#T4U1EЦȎp@kgMAlhleM£dGS3u-rpS==|wO}n`IBёX}|>fl&wnڟ9c!xLae; Xǥ ~ XVO@SR4Rc05c!lyσ |MD8 %PL4b; _2Ј:"9ElezLbeow`$@naIzq9t$#<e-;cI7k;-GLC; Gi/v; \HPBDIUwwtwwAz *jlxe_X+ sib tC*oa'n>s+!ܧ>v$aWVIb/P 1ˠs Z;ۑ?cgmjMxq^r!.!/fr^|ɡ d#:Hw_gZ+LV%H$}UqAC3#dVo 8H[|XANT D+Ns,t2&QES!{BAHv20f"Sea$fkԡc]Lle{$ka9jch bB "KxBÙ뫕kF~3hIԤmsI_<m3,3A]>?ձ,U*Skr ̴ꊾoMM}?fsȤr&ǟjUE;#Ͱ\`9e$:2I!H&xB[ŧ,uTt2Y4oNeb7z':b!'B<I.s6KAKe-wN.ƌqQ_Cx 0 aЭOTw!PiZ$Q洠V \Չm:;;ٳ7Z_oW{Ve AT/_^-Zvv\$Y=d|[W'&Bb ˦ŸbFtYԲ.ii,%n6Aٔ;ꍄ7lUh:k0KbSyi w2K&vj^~n,2I},ɀON)x@MCCK7)DKID,zlʻlZoLgM6hR'a u:'-%Cl%S,w.xb.@**D(p-Я֮fÛ7j+=NRl4mlcaOWZӎh Lr~U>j^?Ћ!K١^VASNo;![Vw u]w&u D ]k*7.MhPK4Rp(twine/utils.pyZmSF_13|E &,ʒ2(=3H6鞞_W$/6Z-*4Ϣt!:I+^2+BBjQ=Hq\D17cԥ3&<CZWk6"+Q,T)J!?DzDTEY,ZUeϖE>"_'"#CU`^KzyU󓳋}HoTZisQahh-r-xW$ZJe˱(ZGZBVhʉ (8wc|Tt|uu|1=?W|z~yooXH υ&!")ZʎIn* D8WQ +U%KTTUdP^.i*cje2{Y\y,Jry~ z$:_jSq(MXcq*NSW¿/ $Jɯ09vNS5Y4́'$GU#N k52ՃZ}TVA ע7j3R82ӳ7麟˫.pЯ4aUeo%n^^Ny&24*QJ*I9,yCT1BJx7pRYYu&*qg  E+IhvY=e łs׈Z%Ne!RK-[黱(\8'Y!v>pȐ:m04Y#x-e5#B ,RUޜ @4n0 GBp|φ3B&q!ƄNFPHn2y%`̨KZ75yC+gD.N/~l#8R ulOV8 쬒uJd!J2!F}mؚ)$$Bt Ғ(#?[P1RBۀE@ ^,Z3yw:vu9R>y ƂE~fD@v2JwFȚG.TIah,ɇIpSx{g`D$A{ocOqI*વpe0<"c<D2*&MG!8Dgāq =D,gGJ%;.Xfp/s Kj詂InȠ-&|4)aV'[IaB ;sCF4ZѤ%Jw 9jVQV#7nJqst 0x'۠uрNڭe:n긷bk^u'm﹟MXGOE} ̞fU-Q>I୳Aef,OCE#]MF':j`&82le(. z{BTԴXG^dJ*0 M W.I%VOxhû0h:; efWwzuF:vz` ݩzakAO(DZ!uS@1#BŘ O(eyxoZ h`,lclfSk$tv8y.u{ B'n/{XICȏ>ֹ͙neY4@D{">{;2m¦`ŞdAB<&Z)\LMfn: H?D(Y!LjlLqH*Gz"zoDsfKב Б#'0ykLLQ@+MUoSmhHϖuN~qtfx"a&4푶oC #n4ؽܬc#)}VCDCtd+ËGpXM=Odl;C|{$^ll5xؼH0ЛYF rW{GkO $| yNynB@ I dƚMbjð "-Bs+D )X8|0 !)hHHYդf/eEeΰ#=mFZ3a:3tmӒ"(79m.!d‚xJdˊfnA!s5sR!ec#B=ZbhXIxM6{sPLi@%%}N.^>; Ԩ!j6vO6yD]0DJFFR\x,_! KYhY*VjI![W>y^v*C-/ m"sℿ ;(V}dCl׉1vaԵuEL4[~z}wFٲ{(N(SvrӫƬZY3@ap +dh-ZUgmr`5/sV,ѨR%\ -O لoQعr+V)szˤ|(ߤmwLm@i;s:i:{y\K /7y#Me7%-Orj/trb^yBW8lt &Gc{eU& a{;}Qf}O#c#Ho< nX6uwOwfv by5ҧe"&rK;WF-^lEL:˚T oTfSosm[YcK_y - 1JU-zL[ *wNMc:S3{ʆuG lOϧޱcvϾJD.@~|=vD3T*݅9EW*JۑGGLvd8(OpF)VQ<#+4.7!ʖ86 x2:Sf62My8xhihkoa svW_}ZlR`~cf0{>QGtZ'Z'U'5r$F@E˧..4lcɏqCz>|ƈ/+L' /6]H_ ]fL?1oa.?aBH޻x\r~d;lǍJFj #NM`I Va [vEpv߄o'#y.f(XMjj$Em yo{ޑ;#|"IK,߳SUY<mJ1[  $2l4PKUJQ  twine/wheel.pyVr6}W`fle8v:+Mc)ɤˁHBC Zb/]"RrEvbG**-%O.~$J Z.E{B w[M3vtH13,I`8}7yXNGf Z= rm±MxJHHa(0(F}k(^su Z0}]xѣⵞ]NBq>T>IbK]uqjm[2pJ'QٞB/v؝5 )^QPpLzh+͝M䴧4furz)a!-P,-8`l>(J[EJpycd2q4wPKxP9 twine/wininst.pyT[o0~ϯ,&9zⵂ۞@ZWEnfmlwM9'M\*~Hl*qnl Δ$V-iE6(Eytn:i|pjGV#\Vl({}H)q|<53s5/]?e8|'.+QTܰ!428ˈdN0C0/ \о$ "A$p-cn6GZ+L݃jl DJ bFg댕.}X45 ` XtRtF@`$N\Nӄ ]^ ^[gѦ9;)M?ycvV֝qes1&?bMe8'$rГVBeaOwMUSrXZ&xZ/ϴ+U_<قk8PKh[R1)twine/__pycache__/__init__.cpython-39.pyc]n0).[.H'|IIQڹK6-,m6[4(*=tːh>O7:З(y$瀴, ӫ"~F- C5bA0Bk񧐅,zşc3DFM\'R>/|Y-c)0f{%KޢarBz]P %3{M+ vv MP!9R̹jgJBz1blߝzba+X%wuTVsj|>0|v_.kZZ^("Eңm ]r8[wp 1'.l7 j].6V)!>k"/K ~MfLvйnF\]Nr9W8!A@;^7u{$E8ggafPKh[R`Kxm<)twine/__pycache__/__main__.cpython-39.pyc]TKoEΌwM!F<eAE!DdY(!&3 7x\ ds/#WN 9fcdF]U]S' "_l˯|B!/=gd$Әg%xJ'bq/үzq&@31C( ,"د~==Q{d쎩w9́E0U͛t^@uKNeϠDYoH V%K{_ႝ"ZrЭ|*:H^'_C<[}IoC:R1<{Σ _} cW: Нۣtt9iT_ ܴM7sD]3eq_On4*M -UnΌLL9q`m"otfi# JgDHak59K(7y qTAfV|*Ra6cYi%/~>mؒ ̼o8#X^xWłNv!`̰PuxSǒ*q"sV17"Tqxo7ٗwvv\;l+ߊ^a!^uohFη6]$xbk*}z[u>U2QYKunh%ϧPܒշR:rUY\WW#/PKh[RN%twine/__pycache__/auth.cpython-39.pycWoE]4MQU MRUB( "U,e$ֻfvQʁ W/. zMPvfg<SSUG!c~o/'Fˮԣ wD9y͋8ω:jʂ LIPH'.YCS:F9Ir ǣZFA!2fr !ka :*c+؅cU7vIwQ#.*ÇD\8i $x NsdN֦?| fؖ8.q [&kd)lYFKf711AN}1~tMH?yH31 Z9QIJs2jQ`>PbIdrI>?C^N [W {(~Q X vʎJKP%r^6!;a{T͉AعȾaB,eҭ{< ;6Q%7`^SQs^xJ/{̾eq52Ȫ.XQ/!.|r.x[)'e"u^~iw;1Ia~jL'qH>YFYZg6.ѝGYOEUl'5~DRCdyU#Do!G«yyed exCI4xuB8-5n$^&v; }r@LНWtq{NX8S3 Jjb$_1%Bj; \ueB_ ޯR\9QN Pnq,Yp_Cba|1Y3!tbۛf=@62 ݥ?AG3|{xvI_v؝1px0@$/qP2ڋrgP`?л09uܮP ZDp=!@WqGj+)L7.Q&03]9,G0)< 3mpws9gg ᚧOx*pQ`"4pa?-hl^N1-WY.+K$grg郧؝;cwϭ8M1)[.ADy^c5?(M7OW/7MRd_P>g4#N?R3oXI!fX'=zޚR[2, G@ ~lOfP7oa}#C&͙vi]`.!T PKh[R;f$twine/__pycache__/cli.cpython-39.pycmUoEo;kǡmH )P)F]P)PE R%8]w2;.k µ^9q֪-4 |y﷿sPU <v痫B&Њ?cјxh&%'g$gYڞ9VU7`Mj^N'ěg :4-f>ט짦7 + 9eV}jxxC Ljpaۤ4JnzPPwQ!K~&-~2c^,_.8,IM4"%4gӠоA/C'I!#:LcS2!dl6p`<жE"2$?#:=8gl?kA!}Ϫ~If~ӛZ_onq< |Cp/1Xgd^0#B-ԅE!/ʅN<ϳ(ʴ0E\uq!KUSО1M=.-e i+kJmׄbm6Y,j;Yl ;<*=4+//+<pl?梈T,h'c̩?O,Br+1 34x#?J4Usz2da*%Y,MUШ ȺJ5%`*74|'&$m,;1_Qkd}%(nЇ"P@C=`fŒmfFy漪ҁ</wElPnLn qkWy6UD5 (./3uN2|9▢E]jP +RKƑ2xeYu/Jj.VQ|&v(*JKءCA*S,U^HQV7PPZAT3YK2֏:s~c7ڵ/WPKh[R+twine/__pycache__/exceptions.cpython-39.pycWoE׎㤩ipMf 4mQ$j4"8tƱp‘C{ _/DyovIȓ}3;o~>~:U<9q|՟0F czݰA2d gAα΁| g3 y]x.<, 8Da/BE$gdCt4lX` YYflojO5vݻTaUԄ$4;Ր6y+VM'է j Z}c "?`-+6'G.^.*zl);; cCWϥW~9c eM 8Ϭ<ˁAg䞾52h_"2&mKֈnP '⒄3E[,D&XLnӈ+ 5f6JEP&_?ZO_"A15zR.jlmICxCb4|ťC$m(uE2h |ܐpV (C&3Mb`IBv zFK#zr3f===ͲŜzeY\gv ՚7[/b0xwHGDDH5}Q\ܭp]p\wK[By hR=^$2m ekR4TM]m.Zekؽx01h!a-N MJK9?yS`[f*:@D!DZS-)}'Ph&QH67&p!5ܑ0s9>6.qIW;+??-I 9%ӝ&o1ZL]!넂Ma^Lr53qX(|hMىqgss/"DkDPɥ 2lj %uubȍnrD.RjڤhL/Ym3.Y|ha(I)sNuC)!'f5c*)mPk [r9ֆ3^ArӯK\E[4aޑrpxDLה-'e!gߔWH,SݘW; yJN Zku&Lݵ9VCem.Vm_oj_0 [[+tÄe2uTB辰KǠ 7IPώýHKd tpBpS p +0|7IK5^Bv-ǡ`ztgtI+2 Gt`G]; WB2 -6.'.\K~ĚКa@ FPqd qI\Z'2Uiޛ6[4}OMR.b2ןr\J0e g*p<2ix /22mĦ cQC{_xwPBuN/͖K3ͧPKh[RV(twine/__pycache__/package.cpython-39.pycY͓uGk0A65wH%9VڢK+FKbLb Ɖv}uySr-UrɇT-'U吪$;5RSЍ~^}[R)I_^} Z/@D'YXD\eKWR*{jOn齂U+Z^*kF\VWUS_jAnՉjYVαrwU-PiIfuk3Ҋ%Hk-#VdkFUZQ{uu: Y=CɦԘ׻ێ҆cΠxxfNiw}i` a=f̃N~5dE0LRlRLI|e$$U =V=!CF3rx0Sg@gLa*SV$u'(%TשBGU`3>ܚ}H#V`W BN}[ef7A* *yBe:OěbSN%3a7][akB;%FEu"'v3FѰh>Me@Fd$RD2"8RG2PmG*HjDŃHH$藡f EZj#Gv~P?Ci0*t`~ p=E.Q .o>:a ["ah;1ZNBP^E=C2dw:6ga0m&bxVQ{|vcI8,y t|0H!u; TYї;Z24×_ r9[c:5İmȶ/B%tR%z2t 4^fxCrLJ#H: _w]uDrH#eHB[{*Em"/tJB8(v.Jt߸ !i #8qW?AQ?݂4;x 0aXh-2Bai-}s)74Q^1/}*C{N|w49wC]c=5-ۂMIy6Iƭ;;w76ohnzab0\0X:q|&΍)i"} 2X|?4RAobb0PJ '(adiT8-m`”/ y ȟZ)uR"2ΪےxU 8jOVxeZLB?Gw No_eUA} ̀-AVV*حC; aEm @; hB;̈́v(* )F2J#TSh?7"0\qz==fDw[xRN69~qx<<&ea"=H6|p *?i{h $e 6ܳArΞ ձϣ}'CrA+ B ]֎!DFKct\ߐgCS1%o7< jۨs?sdRGH"c$א| &2U$בNbQoׄ? a8 "_Gr  7d[Hn#S$o#Px_N5I>Ƅ_D~g)i> %sa"8vO$O35i ܀0"%] A']rB^=UᷧɯV _0k_37si̶㛁 3J2o54p|M@9[r;»B"s|[f#\!|Y&3Ӏ>pq`hb ONͱ#r*OdږG y Ɛ_> ?(އ~Fl v *hoqyu ڎ:UQH>Wj3HE :[88*~oqUu9**jg}Ldl6`'' yG"P:hC&K|Et"i|EJ߆3Ȓ׿(,7$ w&ڃȁ\tއ@f ik[$3 Lxh [^KfM]b )<Sm >k[C$:BVxs:MШR8hAG۬cyɬ8`Ie-gYV$zoukkTR2BHA 2 1:-Li ̔1ylЁm8^6@媰\{ J88퀊쌣'73U0UgtOif0CaY8ȿ*`I)K2O}IW#9):S}DU'ItǟC>$%AĆ{m;>#"}@~! %eW*(@Z*Se_pjyZ3a\FV>Z)9DiD53*4&st 0zJ2jbH`Tt.sܱM8xpF+ă>d. wL  ^ Z \7!7 :n)?Շ90$Fre ~oN8?d-MB<]!jM?%7j-8!`q@/-/fzhn|x5B ƬQL#Ҍ􉤀Wm ]LJ¨3I4ņb*D8hx- u +ㅩ:Ȥ ceJ'Ÿ'q򴈽`3@)߿ %!'\d~9_$K;P4>7F2 I%;&T5ݵw{n(Hv ƬWr H2Hq#h-iCaeꚯWǀ:PoH ?tR ~s#RM~X ?zn9 bMК,wX%a68%pnJ"07.`# gw}R#M Yf.&WzJpQ>$9b 3FvdWqǾ~[ֺ1IU鵨s~%0 UqT5sf=)T\ ?@*}Rpd1ݯ>=ڴ൧$ \;.lt0\DOQ4rZy4fAIiІs4=ZC)XXƲ< `^8*D>/@^꜖!8gD-JG>d""=XD:GE{@,MZS6xI+*s[pXMQ|R @ʲt]Sf(L˘!ߺ!zT% PKh[RZI+twine/__pycache__/repository.cpython-39.pycYMlu!9jk`ZXۂXZ`"q-c՜gzT]C i8Xq ik>O9)@NM{Mw*y##?EEgğo7La$T8~iH[4Gk2e:frV)o)g~n:N97ï&I(3A)f9gXf3Ê,+9slę}FخR:硤ơd|:o2r_NVDN[AM$ u#텉z~ռXTIbylwc^ocj4: 7mOt-IovCN|mq :b#j/D;?j"dY3 ZM8a XT;iu59v2 T킽I.܏_҄v I3gW>+M20.G{E)w1sw 9r%_5_S23p:Ә;oVvcC*K ̖%EEV+A:Z0ޘ_Ο6!oJ8,)v&9ælB챲96es,^ vpUek{J+-(=A)h6rG~ %o2 ^9ye 7zdvBA%VNj'g8v2/| Ⱦ2U٣.xK}9H_\dN۔Amt}ʗXTŀӡ̟XmM0un)ߟXgwS< Pa&J{3^_\wAl',OϕiDY9UK*2ǍČa늍FQ2㶼M=w7"EFDuOcP>3dʞuTݧ;2*'$Qka[ZD9[2!j뵙AsS(AvSK͛@6 ڵ|*;{ՂnD %*VUMu( O%.Ψ(5{NmjdhI^){d_A6z_yI QcF15 ZٙCjH 9>"QWAWGt [J|Yо*`Z}(Kl~\cbeZV5|IbPCdܻu]YsWnX[[^M }_tI vRpc(LM;%o0NBٛ.D88p~+G;GHL2]oYXͫAtOrCCeJ%EI0KP߆WiۧMžpq\擈bNIGH_4!BI3 ݧ"ra}oTT{cR`R]0=x0iJ~5Uk,ٴ@椾ִ 5`^cYw[H{/\jXೣ]~O}UICھNmGo@%Q 뼄! bTVBn_ m.07Fp|fVGܱZH H9 ,- > /2ZQ-8*+\z֙w81XbzB|1p) >cȘA@|y%Cfk0JvMTְU, ɛQqLAU^=7*of kЛYi*oͲ!F!F4ՆPJ' Kvj7WPIUꗌ6 ꐑ69bdi} { ZT&vNXlTh_ok_WzD%5qLf<0rk8B*fJ."ເCؼ`g 2\<?BnzcVI]A^k6-RB":A%WەAUKCs%zE/HƆqB|$01!dZG.r!F_5pEGl>%iFѠ!H{/o.6_JY@3Ƌ5T}2WgfUcJWỌB^U!@^Dg̮xPnͦLpٖYs0f[ pdΐ+} F'MV.:;( 0?VmD^WWzX6[g=`9иQË^yMe+"]zU6x1o|н%7-';oGث_۹7%ΟX9Ȟ[HHCPlѥL,y#;ɬ b7o JX  "2(#L;?z~WyYf>!OlVAEA! 2E=ص䖲WW-!L[0<=HaBe_Lj@pz6nׄ\|6U-?A񧊼 W5^GZ0sJkPzpyCi.LCї}L!SgNwN KHBJK jSyo T+"sc!cEPitt0+ǸeH/dLIydxû>/MZILM\|}KkG.%X;n3s$ACrYcDO|Q|*C pPM-7P(#,ZY)J$Cmu?PKh[Rrn^()twine/__pycache__/settings.cpython-39.pyc:]sVv(#cI[5i'uDdVeWMi$y= ^Q@{Jɴٗ}xִ/_kۤ Eٛd+/q=|Ye ??FTɐLG6d U j|`:E)%iN3aLYզ,*us-'ZHz.>zxz^`;~kN$t dIGB]x<+jj4G47/ ZK~2@)!%R*oiȊW2ɯQ)>:A+DǦW1 4ޟ%5UZW 9NәY= s|:gDaؚ*_1Žr?_eX7_WZ?wPR9$6cTz,5ŮŬy]Lgo9V0R8ծkAudrd`JHsPZOٳ(~TGP{;zrE \9By;2 񧠒HIL?HgQTtXcI9L }Ӆ}?P}!>k6D_OEm@ cpσ@̃tnLѴwb~ݳA?`QO ?MNc{vrBݖ#BujvuP{]&93,fCm1 kҖs40 spv{ ]dzp,a-;q~Cevb6pu׏W:'Q1.ppT:6IfIx7sc1+Pn +nK cXC7GEH,N)J]@:Y@booolF?X-^VOwnߤMnoyw"|\fCk>GY._Y|"W`]8 BMfa{0&:^29P1VʦܬX!TP(NfNodv?Lko‹l1n^Koߗ9:r/'"I 7$-%0PrЄ' jSlMpwnK E.]$V =eGv"!@ھ5`ZSOM] SLCͻY/sL}78.}0ގQAMcF9d?wiG̔c$LAc5ܨ; Nǻ}_\̱(ƹػx_ _(} =-a2<2<] ~I,,!ڶ}{ ƪ7ke@U).U6 rQ"obfW'~@` ;Vע“jX֫ y)pGAZ9urYkJo J oɂoXDŷɿbGX2#Obi Ʀ\<6K!sH]d>'8º߃3Jb]' 3sr#sf`Lao1 re!K!VObz_cw)|^ݠmqx=NFYX>ɘz\17V6W6IBF$" \B)Xbj npi +9\'Up2ۀH!Í6c7W6KVƒBɟ^H8i?R2,ٔ2C AlG*(Z(ɮxJU6H(yD/T|)ExZ q Jxܸ&]XumlyzQ#:DbTcb|XvA:8:qPtQG%?,,񵚂ߎ *@Ay_]) ,h'_ vipq\์/3/][-ե٠I&\@Fl;.yY51XS*gTU˄F"5JG;Cr\XUGi n]/M_ +3$^ 4 &r]G"uG"E>^$)*>F-"w=GUb.*GfGP%(5&owpC { MlևEs`m_#p8sk*J¿3|s|s9Es=[^Ų|W`Nqjl y lnas+$m )ݔrr!q)ظGŻ:nAe9G?PKh[Rt:Y&twine/__pycache__/utils.cpython-39.pycYMpGv? @(cڒ)"(Z*ʫE)$eGqFCt z)V.)2J!ܲUy9mUnim+u@Bzz^RA{i9!$4F%򩻺|!kʧZi˧̵i|ZD[%Ԛt' i4Ze,&䏸GkڝϣQxZcq7vO;')^f>{#]X;:+?+{M>7]fU6 ?ͦvLVfG56⾍(8ƎCw~s|z<F1)vfمr|Cp%P"ٙeo<Q[Bw({|{ywqA9vJg%UFVJwJ.5Թ7Zx4[A-IM:I~3uVk<PT!Ͷ/b;?q3$xa~nF>|7Zwwv qreP*zjd=O:"$@4~ I=m( %k=%֮ k+}U:>u0G4=]C0Ό[ڗtېmr}4e7jOIq3NL&&#U_Ŀ%SPV-6sj%-}oͨ9~ߓr|:..@hB\~NۏH̃8>EԢiqIf+!O&q2sͨqNwsmWVd pa֢vSXqhZ'h5kQtsO?`hYn< i+ؖ fۤw n5hh_hMp!$Z\Cfπ=W PcζS=,%{ ̀ :`L둠@g4)ՎBG\MMԒڪFq;y'ā@!THs^>t j烼[ ݣ8k #{Q=u-uD ^㖗ol!|<\,O}/qR(]0'AQD |e+12MLR~%~@17-DaUx1Fbfuq2vchɄgD'3>2ȠZ`кb.:j $9,*D'l 2M.!ۗȪZw8For:z@ Hp@n@ `d= ۼԃ\Q<|](N0,Am|ʻBm5б 7GwX1ŵ!}R7q d(^Pg6cRljwtF؛ (z˩wd*! :w> l`&~G$E^pAiѽեwWDAңM(f_ \[K*uo_O`g iA`"Mq?(3dk#0 $cXnQ鍋  K_jp7zFr%`c7,U`Ag!=,qz0HƥTBl Rebr Qޛ8{CP`W._R{nJc4 1eu IJWa!*}XրdTq5D % NT> rԓPLAӉV1zUwQ~H0Ϸ VsI4Kj 3'n&l:YKrDkt(p &K"3oz:=VRFj 7ԧĻ ?]\ E?77"8p!zr'O:184%+jtkwx[^X:2{1OM8x{I|́I g̎?!]B*M7oLS~e廮k~etĖ\?E<<)mq)dDE"^s,3Ļ*g·\އ謏KG-n [Y xVG3m0K.S0cCvQ p4y/W: ,ڧ2h(G8)TeGjùGoj?&GȰXšz'SusBF [tIV2KNkQnD2{e,$67bk݁  ,)_ItG`h_x  cGd.6ȲϤU 5ڨLYz'* n׎ˊʈ `fUPCya*\Hi0TO9`&De cG!cEr3^kx-ꌸ+p[sx߱%a,`(CS[5-)L1)1YcbrS_ֆ{zPI~bd^Xc`F$_b\|UԪ[X &HI6) ;oN),sFKlj :g7WW7dbfl̷ay_i 6 zӯs0M}GW8EӬO ( _{ߢ{(q~KԼd()z-թ~d/9[ŏ7 SS* aAo@TChr0_G.jeТq$:cfF,#9T2{uqנ汇%wT}@?q0nM͝3J[;}?SƋ%H=qJt ].`Y JSbM1]#>`5joeb7abdc>gjiH'\Alp=٨_meKdzɤzA=vͶOf{2.F !U~gs~']V_(UU.@2XSBaUAY#YL{ۀaC`X~ƅn qE\79$߫m2kkkPAðDEsuY;Y`5'ŸE0ԍ0d|(ndH(ecPT܇q '_N=dt -"Iᣙ. .o;U6rD:J2j/Ls6:FO676?ow^;v6O61~>s]T~ZVe̫m3e˦̲n{PKh[R%(twine/__pycache__/wininst.cpython-39.pycuUoEiNCpNU( HD*DZ)V)gl̮{\h$Ƚr[^'mZٙ{7^}_,?ζ piDɓzT?"gz&wFdlֹ9=s)t\1w sD#콹Wt4F*~IDx2cbuV\iH}`5>rmՆjyURY UaS g np,a [Ϩj>#\vy*r87ra$'BO\`Uy_? %#:">11n6 ΉA.NtXD=9x#ElN Z_i ߾,vv0BN}h?pNBm)v9I o;+6eY}!'c`ǎ!AD.aFtlpƆ2x,-fT禐|,/zA$#~dݫhOӝTaY jԥ&+Η+mbz,^m ʡF6rFȁ1.'DG.#=D hM?>i2[!EDe(6F6Po׳ dDNWȈk ؔ"`KhV@vvCD _i߶WDZ{6t#Aqč鬍n";q8Y<6qv1Hg 4!(lD6lVw}ֽ;_e+b4 L3v]`v^*7hg%u.֋Y l@ vlLu/lw> gZ,ʪ.*gꢚK2e 31̰jI]? {0Str06z++k{LY"s:QfEH݇.,i$2:,uN e"Iyޟhbni/gUZ.[4MjgO^yzb+]dvOGx#0WU%a^~M/puY$2J[Jl4=7R!$D^z N yëjPKxPh&twine/commands/__init__.pyTmk0_q$&/Av̬$+gGLT5PK4RX twine/commands/check.pyX{s6_COg&5ז{G>"!e5w(R$o;ϋLf߿9]J'dQ]%ȔXEB2!;]H\$p7pX%[5rJ%ESbd<tʮdN=^q)K(-Y^ .`x1~1ⱐ?(E<7 B< ,.y,X?9r("&DsM+{F-K͸ .8zmVգz%yњ|@5`$=6*PzlZ)-Jjѳ:uhi.P7Oᶽ y$9h>vDID"IPyAFN4Aύ/\ R Z4 P4/ac*8,[$ -[ QqsAdul*OJ Ne)UymUwڼ\3­3)Gasu'; VO/ƞ&1OIV)Ӷ-e[?_oޢ;ů:7_Oû 9ߞw7k:FIJf김1WCiJC~iySeҖ5J_uY]$hMqh$x+!ĪBR)KFM(C9$4+'RB3!Rĥi%>:>WӾ@÷kF/Z`w'˩*r-;m;s{Xwx8uhn˲&cf9 wpI^x4{L]2nLGM..%]d:`RN$l4zlpտ o<q(pPLGsi3|YsYe)3Q;.3sT1έ"lF,RmS@U&ᅘE<FF#%%dy}6 K }f/0T^_:鋋=&)1_ofN3r/2r7KN]?q5c.TAGQ:N=gEyX6"-z3wᒧpSE+{v|liV9JRشRvjjwݨ q䯢ѯVS4f85 %=b`& g2#m*3ߧ tiôx|5wRPKUJQpr0twine/commands/register.py}Un8}W ڀʴ% /8nQH#DjIQwH],_~)̙3_wZ䅅O>1J$*.^$( 5aY~zK6BI>9)I;@w A(-ڂ. B+l r_S6n=\]^m˸gʔ/\׷{b6BS;5I+Q,y J5*G G`Tf[PRa=PjF OH+9\n ބro"?}~ߖrna )~X,WKI' ovpb TGԘL$#'jI@UPJQ ˭s (k ʰ"ȴjݛInlЛ[!qv࢝1R%8 ǭ3+ Ō STԳFWN6/X)WGcJﶍ.a_'l(9ܿ _BY }k#G'`J;E}0='{1~*4>GwZOϊo}k%]%ܹ++yC.ABi;udI Ntr0$bJK!#κwAK4~ 9鰖ёisX]ӊy [魡l[8s=SkTMrS˖:o22\|v5 h-p%⹻`\~?c{ 'C8;tOҵ9$x{a ZSe]K(m!Z:O}s@Rn9s< 3_zO5C&} e降gLӭHzr3@*1&ĆPKUJQQtwine/commands/upload.pyXs۸_aJ &y_Xen\"! d0%e<8^wMW/oU[ sQkQ.bf#esof7lj:;g)$* MǶ|ưN e%xEkYlJ:l'ƊL2ݳhVŷuLǸ1nq4kTyV9*}ZOu%fJ z V|xLC`wJY3q%(R `+^r rq_o?ݳϗww7W v{޼_7gLN#ZER EA[1n ݊\e`eUVI xTr+ 7Hly2JjFg-7Z5[f-Wenf7LrFOExP>qD(|!'(0SL:#+"akV eͷbN&B"eV / ?xB1Q'X\l4,pgݗ_H){wK<"&ۘqLj 3{NC-s'˼)5AQnJU9qw >%Ӭjv0>2ɜ~B7*ղRu}f6G;!K%Bow$.Eׄlح#JJz{ROl4-s-R љ-Ej9 168cS9e gg F,o^݉4jn-OM$ɷi2  # Z2^X˸|^Vuuԓ=#nhx~A],qjN}/ B݈U4eBSd'{PFiû@S+l 0*2\d4ɸΓ7)O}&GϨˣm@Ye|٩!Y 3YjK!4o,(ytP@k0dSY>| iDa6 5\ %yZ P:Ztl@Tϐd'j c0\0JqGiyy7nm搸UCyy13%ă#^XK>! UC#2bğ-OKQ EOq~ ƒNCA*h*}`Z7~zS%w{YER/ C6;h:G @@'kҬk^cA@l:ckeX(Љ4r @uؤb#vϬ#Wb9mT&vʪhLndvv jՖB` ݈A'ʹ7XرDCxmbJ`F#epG{Aqdžk#rcvCQ=N琸NC$~H&k@8&ǫ?.;\w<Ծ[5:YFRL8[?,dcM]&<Ԧc#2_(ܨ2\ۍF3wq sU!A`W{N-없1}*R}԰᷑R MHO ~Gc l]BP>X^:urL;! ;,$C4פEFQ }Vj뾅&thxfxeXd3`˖".h~;?UWЦOͼYiIX[q > 2H#}ľntCѶa8P6Z*M<1gRpPKh[Rj 2twine/commands/__pycache__/__init__.cpython-39.pycSOAn*ĚX -& al;CBp?GN^kꉳ7.R &n2ovμ}NN <߯}{{$1`HB >`fpS~!0mK%W= S] _eOAG]uezQ㸾8u [/m*Tإ15!퓌=Gl;* gJ fB)c7!dd.PJ8c!Ֆ E2>@e%<$Eu/ۍåƓegQSzt腲ٍRMB/uFK+=QZUF Ѓ=9=?sonVdǛ~JV8(T ~_ 0*nQȚT֪Zs5Y5 ˵:cXvN-b[Z qw\4hO+;%_iW՝ MS{LfT3|p[|vVᗶ^f{k;=7YXֲWc]FJmߨFMZq[k$~^sIV+t.mSkZ#<~}SqVd㋧,릭܎t ._vg{v6<&=4ײw7+F\lic6wMzTspM1\e~:khud菠  K}E4P5i fB_>Zl zTW囉~*0O"6< E ɡ)& ʭT(݈I&p"&aݛ' dsO[Q|чއv{.΍2歧~ny^ ϻנbŷ_Pͩ\CCLk3F19$L١CR2:=$CmZCŒuf6+XFxx+w`5GV'Ef嶷 Unm1C'V$:~m{{AA C۫nx ༟RQLaq_m2ѨAH 0~ЅR%({hqw͓g~bwMσ{޽S唚hy*~q[Vw}sx*:Xx:1.ezk+9n_˸ EF>X)oPpw&١aЁiCP艖1dY$h_.dĠ90k8X2+C:3Y\3kW3[|;"H@?ߑA)+bLݢr: ixAVIm}4$/1u8Ϝx /tq~h [wam''q]VPicaöFz;$ZUQжni-C'llNux3?=dxA"4, dj"6*čEp\FZY\?8TAEO%E g^GYr;k{rN-1(,Jt}$Pj~7($Q fPqi:B"+E485z(0G^Z9$ IW32Z>`|.GLVp4Q_(ȗLόx`̊d؁Dpt$ILX l(zffTxw.e9ǖń{M I5/*l?vܾ6x[#D%\"S>@Bh9$1{KIi 6 m|~+'Yڐ ЁtèBdN:ՈDB+iEiw%w6SsҢn|؉BD"vS@pw~Ȝn8OsSD]g}}nqŔ_:Ssa}N68?%7 tɉ]B-̻̓-?tBw'^nr$>)XRM$#ŮaHv[aS \[3tm4(y 4Ҁ`Ub@A*PL$3ri@i*e5_UfժAfŜ)h{@8$|K..>U0TiIlFZODm7\C spZ7~ 9ܗChջL/rmԺy'^"|9X rFc'QF1WcΫwEk"/y, -*/ 7QŒtrpKf3:/ŗh G&1Bia a٪xXzLBD7C{XhC G]9(!Pg.(!kUc00 kwWW_?X\Y]i,cɨ= x{؃v-񂂉;ԢeRVQ; jYTW)C|PKh[R%2twine/commands/__pycache__/register.cpython-39.pyc]TKoF.Qi顬tmѢAa$7^ w-Q*~>$tO_kO tv%iHp837nuׯ>>dXg'Yك Acck8cC~*6$JIyk]J49ZX}#m2h&\eڴBx~D{53]+}{oAޕiDꑎ-7%4_P/ݞv-]n-=A۲tgq }Ă=o,Z &B^\{n I}Zo)\)?TTT][90L]cmQ.bll'j>50X(Cj'&ty0ɶNV X+fLg]Ȗ׆A0S)JhM6A> l Y!y'k@9ZΛʱ2фSƥʶ%-0yM\AnYNr&[ ӓ ټ3J u~ૃ/FG|<곢C[+nrbܳ8ڷBۨbA%!1Q0ߒ[&$젗0 !s`U0_+_@@oEx9쬢q7xںkEǗ=Npåҡ%-V9g''?k)Ʀ͸evӝ偝rjǔM|D9dn\rMt"+<Ґ p25r<8veMt'7sPCMrYe Õ'aY۽] s(*iQ 1~ΊC0LPǗl-u]=K_Ћ?'Yf6U%Xb;Yұ%] 6om eIb ۿPKh[R ;0twine/commands/__pycache__/upload.cpython-39.pyc}W͏r8}ko9*qv6050Yq¬ȕG;UG5ENM@{Azc{z0q4l]||gտ3? ʯFP|I@&|iV`;C9n׌+}/ATuۂv- Fm+ 16) ք[;#6 8',(!8Wa0e8 S&/eO)_ OzeFQ9PwTHv~?ڞ8ꈡa9 'BhE5RQXJaa4l# j$s)6}#2hcf􌉩HQ9Ƅ>!>}HMd1serWˬz8!'Dz0x5}O}&Z 7rđL #zո0Ə}^,C{)ic}>:nva'I޾ryݷ~v}]@g;IwyZW P^2s-N:a^ߙODגB>~(Utvxtz_C1hPv'"wzJV<27q˖xdYGJiQꒉo`rFm$Mǥm}Gs6#/8 Kv8+vWYٯa)1*eȔfFF{)cC3sje$cSJYg˛ @qQ*SVVORi} x,oSB 5#=Π -ku<)?gwX5{gL}s7|L"uo5LmE&v2ujbThhr9fPna}e7Z= yQ$N] XV6.7"@?(,i+ ^[4g[\5o.Y#:Z#F6bԻ^j3Pi&&)S#T97 T1Y4N~vf"0NXf&0{ vƧ8ܝӫ.hW5f)_);sQ׽!j(TJ.Uʴ[M>$j-91ktCAg,ji=٠˼'DnɼȇUQr(wzHսs 䏠P&),W #fKz11 2FԐIaCjPHqȊ(/r|VF: PF@eMKKs Pezy,X/2JgyJ.ZARHѠѬ{C(*QCMs.x*dX/VH lҀ5)bx0ͭN Z*q sҠr8i[%3O+%t>4Y'c'>?㾉"mu!m7B6s!C!{4^)lZsG|]RcZ'<ƫ epu5H_*S䃔 -ns p#'Tσ mS@W9@H޸X~xL:[9Cۻ r[ ,ߞ֎q: X}? }H*#ZF@{hЉG ~^ {3ހ5(؇WR][^xQdw6vt;EpQ.&d\ k(7Nz ~-ӧb= yP_$]b1>y&51}g{r5o_`z$&yIo$2Ĝ)#f Gh}/r[Wsxmh叵~5_ˣ ^t{Q,7lA]Z#GV PKh[R]@g EGG-INFO/PKG-INFOPKh[R3KOoEGG-INFO/SOURCES.txtPKh[R2EGG-INFO/dependency_links.txtPKh[R$ɐ6c*EGG-INFO/entry_points.txtPKh[R2EGG-INFO/not-zip-safePKh[RNJHEGG-INFO/requires.txtPKh[REGG-INFO/top_level.txtPKh[R6Y twine/__init__.pyPK QJAI twine/__main__.pyPKxP=c twine/_installed.pyPKjg[RGA] $twine/auth.pyPKh[Rt twine/cli.pyPKxPi񮂢Ctwine/exceptions.pyPKzg[Rh%, $twine/package.pyPKjPK*twine/py.typedPK4RTb% A$y*twine/repository.pyPKUJQss 416twine/settings.pyPK4Rp(Ctwine/utils.pyPKUJQ  Rtwine/wheel.pyPKxP9 IWtwine/wininst.pyPKh[R1)!Ztwine/__pycache__/__init__.cpython-39.pycPKh[R`Kxm<)0\twine/__pycache__/__main__.cpython-39.pycPKh[R"%+_twine/__pycache__/_installed.cpython-39.pycPKh[RN%ctwine/__pycache__/auth.cpython-39.pycPKh[R;f$jtwine/__pycache__/cli.cpython-39.pycPKh[R+otwine/__pycache__/exceptions.cpython-39.pycPKh[RV(vtwine/__pycache__/package.cpython-39.pycPKh[RZI+twine/__pycache__/repository.cpython-39.pycPKh[Rrn^()dtwine/__pycache__/settings.cpython-39.pycPKh[Rt:Y&*twine/__pycache__/utils.cpython-39.pycPKh[R(Λ1 &-twine/__pycache__/wheel.cpython-39.pycPKh[R%(Etwine/__pycache__/wininst.cpython-39.pycPKxPh&twine/commands/__init__.pyPK4RX twine/commands/check.pyPKUJQpr0Btwine/commands/register.pyPKUJQQ#twine/commands/upload.pyPKh[Rj 2twine/commands/__pycache__/__init__.cpython-39.pycPKh[R  -/Wtwine/commands/__pycache__/check.cpython-39.pycPKh[R%2twine/commands/__pycache__/register.cpython-39.pycPKh[R ;0twine/commands/__pycache__/upload.cpython-39.pycPK(( ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/helpers.py0000644000175100001770000000301714562147542015440 0ustar00runnerdocker# Copyright 2016 Ian Cordasco # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Test functions useful across twine's tests.""" import contextlib import os import pathlib TESTS_DIR = pathlib.Path(__file__).parent SDIST_FIXTURE = os.path.join(TESTS_DIR, "fixtures/twine-1.5.0.tar.gz") WHEEL_FIXTURE = os.path.join(TESTS_DIR, "fixtures/twine-1.5.0-py2.py3-none-any.whl") NEW_SDIST_FIXTURE = os.path.join(TESTS_DIR, "fixtures/twine-1.6.5.tar.gz") NEW_WHEEL_FIXTURE = os.path.join(TESTS_DIR, "fixtures/twine-1.6.5-py2.py3-none-any.whl") @contextlib.contextmanager def set_env(**environ): """Set the process environment variables temporarily. >>> with set_env(PLUGINS_DIR=u'test/plugins'): ... "PLUGINS_DIR" in os.environ True >>> "PLUGINS_DIR" in os.environ False :param environ: Environment variables to set :type environ: dict[str, unicode] """ old_environ = dict(os.environ) os.environ.update(environ) try: yield finally: os.environ.clear() os.environ.update(old_environ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/test_auth.py0000644000175100001770000001734314562147542016005 0ustar00runnerdockerimport getpass import logging import re import pytest from twine import auth from twine import exceptions from twine import utils @pytest.fixture def config() -> utils.RepositoryConfig: return dict(repository="system") def test_get_username_keyring_defers_to_prompt(monkeypatch, entered_username, config): class MockKeyring: @staticmethod def get_credential(system, user): return None monkeypatch.setattr(auth, "keyring", MockKeyring) username = auth.Resolver(config, auth.CredentialInput()).username assert username == "entered user" def test_get_password_keyring_defers_to_prompt(monkeypatch, entered_password, config): class MockKeyring: @staticmethod def get_password(system, user): return None monkeypatch.setattr(auth, "keyring", MockKeyring) pw = auth.Resolver(config, auth.CredentialInput("user")).password assert pw == "entered pw" def test_no_password_defers_to_prompt(monkeypatch, entered_password, config): config.update(password=None) pw = auth.Resolver(config, auth.CredentialInput("user")).password assert pw == "entered pw" def test_empty_password_bypasses_prompt(monkeypatch, entered_password, config): config.update(password="") pw = auth.Resolver(config, auth.CredentialInput("user")).password assert pw == "" def test_no_username_non_interactive_aborts(config): with pytest.raises(exceptions.NonInteractive): auth.Private(config, auth.CredentialInput()).username def test_no_password_non_interactive_aborts(config): with pytest.raises(exceptions.NonInteractive): auth.Private(config, auth.CredentialInput("user")).password def test_get_username_and_password_keyring_overrides_prompt( monkeypatch, config, caplog ): caplog.set_level(logging.INFO, "twine") class MockKeyring: @staticmethod def get_credential(system, user): return auth.CredentialInput( "real_user", f"real_user@{system} sekure pa55word" ) @staticmethod def get_password(system, user): cred = MockKeyring.get_credential(system, user) if user != cred.username: raise RuntimeError("unexpected username") return cred.password monkeypatch.setattr(auth, "keyring", MockKeyring) res = auth.Resolver(config, auth.CredentialInput()) assert res.username == "real_user" assert res.password == "real_user@system sekure pa55word" assert caplog.messages == [ "Querying keyring for username", "username set from keyring", "Querying keyring for password", "password set from keyring", ] @pytest.fixture def keyring_missing_get_credentials(monkeypatch): """Simulate keyring prior to 15.2 that does not have the 'get_credential' API.""" monkeypatch.delattr(auth.keyring, "get_credential") @pytest.fixture def entered_username(monkeypatch): monkeypatch.setattr(auth, "input", lambda prompt: "entered user", raising=False) def test_get_username_keyring_missing_get_credentials_prompts( entered_username, keyring_missing_get_credentials, config ): assert auth.Resolver(config, auth.CredentialInput()).username == "entered user" def test_get_username_keyring_missing_non_interactive_aborts( entered_username, keyring_missing_get_credentials, config ): with pytest.raises(exceptions.NonInteractive): auth.Private(config, auth.CredentialInput()).username def test_get_password_keyring_missing_non_interactive_aborts( entered_username, keyring_missing_get_credentials, config ): with pytest.raises(exceptions.NonInteractive): auth.Private(config, auth.CredentialInput("user")).password def test_get_username_keyring_runtime_error_logged( entered_username, monkeypatch, config, caplog ): class FailKeyring: """Simulate missing keyring backend raising RuntimeError on get_credential.""" @staticmethod def get_credential(system, username): raise RuntimeError("fail!") monkeypatch.setattr(auth, "keyring", FailKeyring) assert auth.Resolver(config, auth.CredentialInput()).username == "entered user" assert re.search( r"Error getting username from keyring.+Traceback.+RuntimeError: fail!", caplog.text, re.DOTALL, ) def test_get_password_keyring_runtime_error_logged( entered_username, entered_password, monkeypatch, config, caplog ): class FailKeyring: """Simulate missing keyring backend raising RuntimeError on get_password.""" @staticmethod def get_password(system, username): raise RuntimeError("fail!") monkeypatch.setattr(auth, "keyring", FailKeyring) assert auth.Resolver(config, auth.CredentialInput()).password == "entered pw" assert re.search( r"Error getting password from keyring.+Traceback.+RuntimeError: fail!", caplog.text, re.DOTALL, ) def _raise_home_key_error(): """Simulate environment from https://github.com/pypa/twine/issues/889.""" try: raise KeyError("HOME") except KeyError: raise KeyError("uid not found: 999") def test_get_username_keyring_key_error_logged( entered_username, monkeypatch, config, caplog ): class FailKeyring: @staticmethod def get_credential(system, username): _raise_home_key_error() monkeypatch.setattr(auth, "keyring", FailKeyring) assert auth.Resolver(config, auth.CredentialInput()).username == "entered user" assert re.search( r"Error getting username from keyring" r".+Traceback" r".+KeyError: 'HOME'" r".+KeyError: 'uid not found: 999'", caplog.text, re.DOTALL, ) def test_get_password_keyring_key_error_logged( entered_username, entered_password, monkeypatch, config, caplog ): class FailKeyring: @staticmethod def get_password(system, username): _raise_home_key_error() monkeypatch.setattr(auth, "keyring", FailKeyring) assert auth.Resolver(config, auth.CredentialInput()).password == "entered pw" assert re.search( r"Error getting password from keyring" r".+Traceback" r".+KeyError: 'HOME'" r".+KeyError: 'uid not found: 999'", caplog.text, re.DOTALL, ) def test_logs_cli_values(caplog, config): caplog.set_level(logging.INFO, "twine") res = auth.Resolver(config, auth.CredentialInput("username", "password")) assert res.username == "username" assert res.password == "password" assert caplog.messages == [ "username set by command options", "password set by command options", ] def test_logs_config_values(config, caplog): caplog.set_level(logging.INFO, "twine") config.update(username="username", password="password") res = auth.Resolver(config, auth.CredentialInput()) assert res.username == "username" assert res.password == "password" assert caplog.messages == [ "username set from config file", "password set from config file", ] @pytest.mark.parametrize( "password, warning", [ ("", "Your password is empty"), ("\x16", "Your password contains control characters"), ("entered\x16pw", "Your password contains control characters"), ], ) def test_warns_for_empty_password( password, warning, monkeypatch, entered_username, config, caplog, ): # Avoiding additional warning "No recommended backend was available" monkeypatch.setattr(auth.keyring, "get_password", lambda system, user: None) monkeypatch.setattr(getpass, "getpass", lambda prompt: password) assert auth.Resolver(config, auth.CredentialInput()).password == password assert caplog.messages[0].startswith(warning) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/test_check.py0000644000175100001770000001677014562147542016124 0ustar00runnerdocker# Copyright 2018 Dustin Ingram # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import textwrap import build import pretend import pytest from twine.commands import check class TestWarningStream: def setup_method(self): self.stream = check._WarningStream() def test_write_match(self): self.stream.write(":2: (WARNING/2) Title underline too short.") assert self.stream.getvalue() == "line 2: Warning: Title underline too short.\n" def test_write_nomatch(self): self.stream.write("this does not match") assert self.stream.getvalue() == "this does not match" def test_str_representation(self): self.stream.write(":2: (WARNING/2) Title underline too short.") assert str(self.stream) == "line 2: Warning: Title underline too short." def test_fails_no_distributions(caplog): assert not check.check([]) assert caplog.record_tuples == [ ( "twine.commands.check", logging.ERROR, "No files to check.", ), ] def build_sdist(src_path, project_files): """ Build a source distribution similar to `python3 -m build --sdist`. Returns the absolute path of the built distribution. """ project_files = { "pyproject.toml": ( """ [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" """ ), **project_files, } for filename, content in project_files.items(): (src_path / filename).write_text(textwrap.dedent(content)) builder = build.ProjectBuilder(src_path) return builder.build("sdist", str(src_path / "dist")) @pytest.mark.parametrize("strict", [False, True]) def test_warns_missing_description(strict, tmp_path, capsys, caplog): sdist = build_sdist( tmp_path, { "setup.cfg": ( """ [metadata] name = test-package version = 0.0.1 """ ), }, ) assert check.check([sdist], strict=strict) is strict assert capsys.readouterr().out == f"Checking {sdist}: " + ( "FAILED due to warnings\n" if strict else "PASSED with warnings\n" ) assert caplog.record_tuples == [ ( "twine.commands.check", logging.WARNING, "`long_description_content_type` missing. defaulting to `text/x-rst`.", ), ( "twine.commands.check", logging.WARNING, "`long_description` missing.", ), ] def test_warns_missing_file(tmp_path, capsys, caplog): sdist = build_sdist( tmp_path, { "setup.cfg": ( """ [metadata] name = test-package version = 0.0.1 long_description = file:README.rst long_description_content_type = text/x-rst """ ), }, ) assert not check.check([sdist]) assert capsys.readouterr().out == f"Checking {sdist}: PASSED with warnings\n" assert caplog.record_tuples == [ ( "twine.commands.check", logging.WARNING, "`long_description` missing.", ), ] def test_fails_rst_syntax_error(tmp_path, capsys, caplog): sdist = build_sdist( tmp_path, { "setup.cfg": ( """ [metadata] name = test-package version = 0.0.1 long_description = file:README.rst long_description_content_type = text/x-rst """ ), "README.rst": ( """ ============ """ ), }, ) assert check.check([sdist]) assert capsys.readouterr().out == f"Checking {sdist}: FAILED\n" assert caplog.record_tuples == [ ( "twine.commands.check", logging.ERROR, "`long_description` has syntax errors in markup " "and would not be rendered on PyPI.\n" "line 2: Error: Document or section may not begin with a transition.", ), ] def test_fails_rst_no_content(tmp_path, capsys, caplog): sdist = build_sdist( tmp_path, { "setup.cfg": ( """ [metadata] name = test-package version = 0.0.1 long_description = file:README.rst long_description_content_type = text/x-rst """ ), "README.rst": ( """ test-package ============ """ ), }, ) assert check.check([sdist]) assert capsys.readouterr().out == f"Checking {sdist}: FAILED\n" assert caplog.record_tuples == [ ( "twine.commands.check", logging.ERROR, "`long_description` has syntax errors in markup " "and would not be rendered on PyPI.\n" "No content rendered from RST source.", ), ] def test_passes_rst_description(tmp_path, capsys, caplog): sdist = build_sdist( tmp_path, { "setup.cfg": ( """ [metadata] name = test-package version = 0.0.1 long_description = file:README.rst long_description_content_type = text/x-rst """ ), "README.rst": ( """ test-package ============ A test package. """ ), }, ) assert not check.check([sdist]) assert capsys.readouterr().out == f"Checking {sdist}: PASSED\n" assert not caplog.record_tuples @pytest.mark.parametrize("content_type", ["text/markdown", "text/plain"]) def test_passes_markdown_description(content_type, tmp_path, capsys, caplog): sdist = build_sdist( tmp_path, { "setup.cfg": ( f""" [metadata] name = test-package version = 0.0.1 long_description = file:README.md long_description_content_type = {content_type} """ ), "README.md": ( """ # test-package A test package. """ ), }, ) assert not check.check([sdist]) assert capsys.readouterr().out == f"Checking {sdist}: PASSED\n" assert not caplog.record_tuples def test_main(monkeypatch): check_result = pretend.stub() check_stub = pretend.call_recorder(lambda a, strict=False: check_result) monkeypatch.setattr(check, "check", check_stub) assert check.main(["dist/*"]) == check_result assert check_stub.calls == [pretend.call(["dist/*"], strict=False)] # TODO: Test print() color output # TODO: Test log formatting ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/test_cli.py0000644000175100001770000000203214562147542015600 0ustar00runnerdocker# Copyright 2013 Donald Stufft # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pretend import pytest from twine import cli from twine.commands import upload def test_dispatch_to_subcommand(monkeypatch): replaced_main = pretend.call_recorder(lambda args: None) monkeypatch.setattr(upload, "main", replaced_main) cli.dispatch(["upload", "path/to/file"]) assert replaced_main.calls == [pretend.call(["path/to/file"])] def test_catches_enoent(): with pytest.raises(SystemExit): cli.dispatch(["non-existent-command"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/test_commands.py0000644000175100001770000000237714562147542016646 0ustar00runnerdockerimport os import pytest from twine import commands from twine import exceptions def test_ensure_wheel_files_uploaded_first(): files = commands._group_wheel_files_first( ["twine/foo.py", "twine/first.whl", "twine/bar.py", "twine/second.whl"] ) expected = [ "twine/first.whl", "twine/second.whl", "twine/foo.py", "twine/bar.py", ] assert expected == files def test_ensure_if_no_wheel_files(): files = commands._group_wheel_files_first(["twine/foo.py", "twine/bar.py"]) expected = ["twine/foo.py", "twine/bar.py"] assert expected == files def test_find_dists_expands_globs(): files = sorted(commands._find_dists(["twine/__*.py"])) expected = [ os.path.join("twine", "__init__.py"), os.path.join("twine", "__main__.py"), ] assert expected == files def test_find_dists_errors_on_invalid_globs(): with pytest.raises(exceptions.InvalidDistribution): commands._find_dists(["twine/*.rb"]) def test_find_dists_handles_real_files(): expected = [ "twine/__init__.py", "twine/__main__.py", "twine/cli.py", "twine/utils.py", "twine/wheel.py", ] files = commands._find_dists(expected) assert expected == files ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/test_integration.py0000644000175100001770000001230714562147542017362 0ustar00runnerdockerimport contextlib import datetime import functools import pathlib import platform import re import secrets import subprocess import sys from types import SimpleNamespace import pytest import requests from twine import __main__ as dunder_main from twine import cli pytestmark = [pytest.mark.enable_socket] skip_if_windows = pytest.mark.skipif( platform.system() == "Windows", reason="pytest-services fixtures don't support Windows", ) run = functools.partial(subprocess.run, check=True) @pytest.fixture(scope="session") def sampleproject_dist(tmp_path_factory: pytest.TempPathFactory): checkout = tmp_path_factory.mktemp("sampleproject", numbered=False) tag = datetime.datetime.now().strftime("%Y%m%d%H%M%S%f") run(["git", "clone", "https://github.com/pypa/sampleproject", str(checkout)]) pyproject = checkout / "pyproject.toml" pyproject.write_text( pyproject.read_text() .replace( 'name = "sampleproject"', 'name = "twine-sampleproject"', ) .replace( 'version = "3.0.0"', f'version = "3.0.0post{tag}"', ) ) run([sys.executable, "-m", "build", "--sdist"], cwd=checkout) [dist, *_] = (checkout / "dist").glob("*") assert dist.name == f"twine-sampleproject-3.0.0.post{tag}.tar.gz" return dist sampleproject_token = ( "pypi-AgENdGVzdC5weXBpLm9yZwIkNDgzYTFhMjEtMzEwYi00NT" "kzLTkwMzYtYzc1Zjg4NmFiMjllAAJEeyJwZXJtaXNzaW9ucyI6IH" "sicHJvamVjdHMiOiBbInR3aW5lLXNhbXBsZXByb2plY3QiXX0sIC" "J2ZXJzaW9uIjogMX0AAAYg2kBZ1tN8lj8dlmL3ScoVvr_pvQE0t" "6PKqigoYJKvw3M" ) def test_pypi_upload(sampleproject_dist): command = [ "upload", "--repository-url", "https://test.pypi.org/legacy/", "--username", "__token__", "--password", sampleproject_token, str(sampleproject_dist), ] cli.dispatch(command) def test_pypi_error(sampleproject_dist, monkeypatch, capsys): command = [ "twine", "upload", "--repository-url", "https://test.pypi.org/legacy/", "--username", "foo", "--password", "bar", str(sampleproject_dist), ] monkeypatch.setattr(sys, "argv", command) message = ( r"HTTPError: 403 Forbidden from https://test\.pypi\.org/legacy/" + r".+authentication information" ) error = dunder_main.main() assert error captured = capsys.readouterr() assert re.search(message, captured.out, re.DOTALL) @pytest.fixture( params=[ "twine-1.5.0.tar.gz", "twine-1.5.0-py2.py3-none-any.whl", "twine-1.6.5.tar.gz", "twine-1.6.5-py2.py3-none-any.whl", ] ) def uploadable_dist(request): return pathlib.Path(__file__).parent / "fixtures" / request.param @pytest.fixture(scope="session") def devpi_server(request, port_getter, watcher_getter, tmp_path_factory): server_dir = tmp_path_factory.mktemp("devpi") username = "foober" password = secrets.token_urlsafe() port = port_getter() url = f"http://localhost:{port}/" repo = f"{url}/{username}/dev/" run(["devpi-init", "--serverdir", server_dir, "--root-passwd", password]) def ready(): with contextlib.suppress(Exception): return requests.get(url) watcher_getter( name="devpi-server", arguments=["--port", str(port), "--serverdir", server_dir], checker=ready, # Needed for the correct execution order of finalizers request=request, ) def devpi_run(cmd): return run(["devpi", "--clientdir", server_dir / "client", *cmd]) devpi_run(["use", url + "root/pypi/"]) devpi_run(["user", "--create", username, f"password={password}"]) devpi_run(["login", username, "--password", password]) devpi_run(["index", "-c", "dev"]) return SimpleNamespace(url=repo, username=username, password=password) @skip_if_windows def test_devpi_upload(devpi_server, uploadable_dist): command = [ "upload", "--repository-url", devpi_server.url, "--username", devpi_server.username, "--password", devpi_server.password, str(uploadable_dist), ] cli.dispatch(command) @pytest.fixture(scope="session") def pypiserver_instance(request, port_getter, watcher_getter, tmp_path_factory): port = port_getter() url = f"http://localhost:{port}/" def ready(): with contextlib.suppress(Exception): return requests.get(url) watcher_getter( name="pypi-server", arguments=[ "--port", str(port), # allow anonymous uploads "-P", ".", "-a", ".", tmp_path_factory.mktemp("packages"), ], checker=ready, # Needed for the correct execution order of finalizers request=request, ) return SimpleNamespace(url=url) @skip_if_windows def test_pypiserver_upload(pypiserver_instance, uploadable_dist): command = [ "upload", "--repository-url", pypiserver_instance.url, "--username", "any", "--password", "any", str(uploadable_dist), ] cli.dispatch(command) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/test_main.py0000644000175100001770000000463414562147542015767 0ustar00runnerdocker# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys import pretend import requests from twine import __main__ as dunder_main from twine.commands import upload # Hard-coding control characters for red text; couldn't find a succinct alternative RED_ERROR = "\x1b[31mERROR \x1b[0m" PLAIN_ERROR = "ERROR " def _unwrap_lines(text): # Testing wrapped lines was ugly and inconsistent across environments return " ".join(line.strip() for line in text.splitlines()) def test_exception_handling(monkeypatch, capsys): monkeypatch.setattr(sys, "argv", ["twine", "upload", "missing.whl"]) error = dunder_main.main() assert error captured = capsys.readouterr() assert _unwrap_lines(captured.out) == ( f"{RED_ERROR} InvalidDistribution: Cannot find file (or expand pattern): " "'missing.whl'" ) def test_http_exception_handling(monkeypatch, capsys): monkeypatch.setattr(sys, "argv", ["twine", "upload", "test.whl"]) monkeypatch.setattr( upload, "upload", pretend.raiser( requests.HTTPError( response=pretend.stub( url="https://example.org", status_code=400, reason="Error reason", ) ) ), ) error = dunder_main.main() assert error captured = capsys.readouterr() assert _unwrap_lines(captured.out) == ( f"{RED_ERROR} HTTPError: 400 Bad Request from https://example.org " "Error reason" ) def test_no_color_exception(monkeypatch, capsys): monkeypatch.setattr(sys, "argv", ["twine", "--no-color", "upload", "missing.whl"]) error = dunder_main.main() assert error captured = capsys.readouterr() assert _unwrap_lines(captured.out) == ( f"{PLAIN_ERROR} InvalidDistribution: Cannot find file (or expand pattern): " "'missing.whl'" ) # TODO: Test verbose output formatting ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/test_package.py0000644000175100001770000003221414562147542016431 0ustar00runnerdocker# Copyright 2015 Ian Cordasco # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import string import pretend import pytest from twine import exceptions from twine import package as package_file from . import helpers def test_sign_file(monkeypatch): replaced_check_call = pretend.call_recorder(lambda args: None) monkeypatch.setattr(package_file.subprocess, "check_call", replaced_check_call) filename = "tests/fixtures/deprecated-pypirc" package = package_file.PackageFile( filename=filename, comment=None, metadata=pretend.stub(name="deprecated-pypirc"), python_version=None, filetype=None, ) try: package.sign("gpg2", None) except OSError: pass args = ("gpg2", "--detach-sign", "-a", filename) assert replaced_check_call.calls == [pretend.call(args)] def test_sign_file_with_identity(monkeypatch): replaced_check_call = pretend.call_recorder(lambda args: None) monkeypatch.setattr(package_file.subprocess, "check_call", replaced_check_call) filename = "tests/fixtures/deprecated-pypirc" package = package_file.PackageFile( filename=filename, comment=None, metadata=pretend.stub(name="deprecated-pypirc"), python_version=None, filetype=None, ) try: package.sign("gpg", "identity") except OSError: pass args = ("gpg", "--detach-sign", "--local-user", "identity", "-a", filename) assert replaced_check_call.calls == [pretend.call(args)] def test_run_gpg_raises_exception_if_no_gpgs(monkeypatch): replaced_check_call = pretend.raiser(FileNotFoundError("not found")) monkeypatch.setattr(package_file.subprocess, "check_call", replaced_check_call) gpg_args = ("gpg", "--detach-sign", "-a", "pypircfile") with pytest.raises(exceptions.InvalidSigningExecutable) as err: package_file.PackageFile.run_gpg(gpg_args) assert "executables not available" in err.value.args[0] def test_run_gpg_raises_exception_if_not_using_gpg(monkeypatch): replaced_check_call = pretend.raiser(FileNotFoundError("not found")) monkeypatch.setattr(package_file.subprocess, "check_call", replaced_check_call) gpg_args = ("not_gpg", "--detach-sign", "-a", "pypircfile") with pytest.raises(exceptions.InvalidSigningExecutable) as err: package_file.PackageFile.run_gpg(gpg_args) assert "not_gpg executable not available" in err.value.args[0] def test_run_gpg_falls_back_to_gpg2(monkeypatch): def check_call(arg_list): if arg_list[0] == "gpg": raise FileNotFoundError("gpg not found") replaced_check_call = pretend.call_recorder(check_call) monkeypatch.setattr(package_file.subprocess, "check_call", replaced_check_call) gpg_args = ("gpg", "--detach-sign", "-a", "pypircfile") package_file.PackageFile.run_gpg(gpg_args) gpg2_args = replaced_check_call.calls[1].args assert gpg2_args[0][0] == "gpg2" def test_package_signed_name_is_correct(): filename = "tests/fixtures/deprecated-pypirc" package = package_file.PackageFile( filename=filename, comment=None, metadata=pretend.stub(name="deprecated-pypirc"), python_version=None, filetype=None, ) assert package.signed_basefilename == "deprecated-pypirc.asc" assert package.signed_filename == (filename + ".asc") @pytest.mark.parametrize( "pkg_name,expected_name", [ (string.ascii_letters, string.ascii_letters), (string.digits, string.digits), (string.punctuation, "-.-"), ("mosaik.SimConfig", "mosaik.SimConfig"), ("mosaik$$$$.SimConfig", "mosaik-.SimConfig"), ], ) def test_package_safe_name_is_correct(pkg_name, expected_name): package = package_file.PackageFile( filename="tests/fixtures/deprecated-pypirc", comment=None, metadata=pretend.stub(name=pkg_name), python_version=None, filetype=None, ) assert package.safe_name == expected_name def test_metadata_dictionary_keys(): """Merge multiple sources of metadata into a single dictionary.""" package = package_file.PackageFile.from_filename(helpers.SDIST_FIXTURE, None) assert set(package.metadata_dictionary()) == { # identify release "name", "version", # file content "filetype", "pyversion", # additional meta-data "metadata_version", "summary", "home_page", "author", "author_email", "maintainer", "maintainer_email", "license", "description", "keywords", "platform", "classifiers", "download_url", "supported_platform", "comment", "md5_digest", "sha256_digest", "blake2_256_digest", # PEP 314 "provides", "requires", "obsoletes", # Metadata 1.2 "project_urls", "provides_dist", "obsoletes_dist", "requires_dist", "requires_external", "requires_python", # Metadata 2.1 "provides_extras", "description_content_type", # Metadata 2.2 "dynamic", } @pytest.mark.parametrize("gpg_signature", [(None), (pretend.stub())]) def test_metadata_dictionary_values(gpg_signature): """Pass values from pkginfo.Distribution through to dictionary.""" meta = pretend.stub( name="whatever", version=pretend.stub(), metadata_version=pretend.stub(), summary=pretend.stub(), home_page=pretend.stub(), author=pretend.stub(), author_email=pretend.stub(), maintainer=pretend.stub(), maintainer_email=pretend.stub(), license=pretend.stub(), description=pretend.stub(), keywords=pretend.stub(), platforms=pretend.stub(), classifiers=pretend.stub(), download_url=pretend.stub(), supported_platforms=pretend.stub(), provides=pretend.stub(), requires=pretend.stub(), obsoletes=pretend.stub(), project_urls=pretend.stub(), provides_dist=pretend.stub(), obsoletes_dist=pretend.stub(), requires_dist=pretend.stub(), requires_external=pretend.stub(), requires_python=pretend.stub(), provides_extras=pretend.stub(), description_content_type=pretend.stub(), dynamic=pretend.stub(), ) package = package_file.PackageFile( filename="tests/fixtures/twine-1.5.0-py2.py3-none-any.whl", comment=pretend.stub(), metadata=meta, python_version=pretend.stub(), filetype=pretend.stub(), ) package.gpg_signature = gpg_signature result = package.metadata_dictionary() # identify release assert result["name"] == package.safe_name assert result["version"] == meta.version # file content assert result["filetype"] == package.filetype assert result["pyversion"] == package.python_version # additional meta-data assert result["metadata_version"] == meta.metadata_version assert result["summary"] == meta.summary assert result["home_page"] == meta.home_page assert result["author"] == meta.author assert result["author_email"] == meta.author_email assert result["maintainer"] == meta.maintainer assert result["maintainer_email"] == meta.maintainer_email assert result["license"] == meta.license assert result["description"] == meta.description assert result["keywords"] == meta.keywords assert result["platform"] == meta.platforms assert result["classifiers"] == meta.classifiers assert result["download_url"] == meta.download_url assert result["supported_platform"] == meta.supported_platforms assert result["comment"] == package.comment # PEP 314 assert result["provides"] == meta.provides assert result["requires"] == meta.requires assert result["obsoletes"] == meta.obsoletes # Metadata 1.2 assert result["project_urls"] == meta.project_urls assert result["provides_dist"] == meta.provides_dist assert result["obsoletes_dist"] == meta.obsoletes_dist assert result["requires_dist"] == meta.requires_dist assert result["requires_external"] == meta.requires_external assert result["requires_python"] == meta.requires_python # Metadata 2.1 assert result["provides_extras"] == meta.provides_extras assert result["description_content_type"] == meta.description_content_type # Metadata 2.2 assert result["dynamic"] == meta.dynamic # GPG signature assert result.get("gpg_signature") == gpg_signature TWINE_1_5_0_WHEEL_HEXDIGEST = package_file.Hexdigest( "1919f967e990bee7413e2a4bc35fd5d1", "d86b0f33f0c7df49e888b11c43b417da5520cbdbce9f20618b1494b600061e67", "b657a4148d05bd0098c1d6d8cc4e14e766dbe93c3a5ab6723b969da27a87bac0", ) def test_hash_manager(): """Generate hexdigest via HashManager.""" filename = "tests/fixtures/twine-1.5.0-py2.py3-none-any.whl" hasher = package_file.HashManager(filename) hasher.hash() assert hasher.hexdigest() == TWINE_1_5_0_WHEEL_HEXDIGEST def test_fips_hash_manager_md5(monkeypatch): """Generate hexdigest without MD5 when hashlib is using FIPS mode.""" replaced_md5 = pretend.raiser(ValueError("fipsmode")) monkeypatch.setattr(package_file.hashlib, "md5", replaced_md5) filename = "tests/fixtures/twine-1.5.0-py2.py3-none-any.whl" hasher = package_file.HashManager(filename) hasher.hash() hashes = TWINE_1_5_0_WHEEL_HEXDIGEST._replace(md5=None) assert hasher.hexdigest() == hashes @pytest.mark.parametrize("exception_class", [TypeError, ValueError]) def test_fips_hash_manager_blake2(exception_class, monkeypatch): """Generate hexdigest without BLAKE2 when hashlib is using FIPS mode.""" replaced_blake2b = pretend.raiser(exception_class("fipsmode")) monkeypatch.setattr(package_file.hashlib, "blake2b", replaced_blake2b) filename = "tests/fixtures/twine-1.5.0-py2.py3-none-any.whl" hasher = package_file.HashManager(filename) hasher.hash() hashes = TWINE_1_5_0_WHEEL_HEXDIGEST._replace(blake2=None) assert hasher.hexdigest() == hashes def test_fips_metadata_excludes_md5_and_blake2(monkeypatch): """Generate a valid metadata dictionary for Nexus when FIPS is enabled. See also: https://github.com/pypa/twine/issues/775 """ replaced_blake2b = pretend.raiser(ValueError("fipsmode")) replaced_md5 = pretend.raiser(ValueError("fipsmode")) monkeypatch.setattr(package_file.hashlib, "md5", replaced_md5) monkeypatch.setattr(package_file.hashlib, "blake2b", replaced_blake2b) filename = "tests/fixtures/twine-1.5.0-py2.py3-none-any.whl" pf = package_file.PackageFile.from_filename(filename, None) mddict = pf.metadata_dictionary() assert "md5_digest" not in mddict assert "blake2_256_digest" not in mddict @pytest.mark.parametrize( "read_data, missing_fields", [ pytest.param( b"Metadata-Version: 2.3\nName: test-package\nVersion: 1.0.0\n", "Name, Version", id="unsupported Metadata-Version", ), pytest.param( b"Metadata-Version: 2.2\nName: UNKNOWN\nVersion: UNKNOWN\n", "Name, Version", id="missing Name and Version", ), pytest.param( b"Metadata-Version: 2.2\nName: UNKNOWN\nVersion: 1.0.0\n", "Name", id="missing Name", ), pytest.param( b"Metadata-Version: 2.2\nName: test-package\nVersion: UNKNOWN\n", "Version", id="missing Version", ), ], ) def test_pkginfo_returns_no_metadata(read_data, missing_fields, monkeypatch): """Raise an exception when pkginfo can't interpret the metadata. This could be caused by a version number or format it doesn't support yet. """ monkeypatch.setattr(package_file.wheel.Wheel, "read", lambda _: read_data) filename = "tests/fixtures/twine-1.5.0-py2.py3-none-any.whl" with pytest.raises(exceptions.InvalidDistribution) as err: package_file.PackageFile.from_filename(filename, comment=None) assert ( f"Metadata is missing required fields: {missing_fields}." in err.value.args[0] ) assert "1.0, 1.1, 1.2, 2.0, 2.1, 2.2" in err.value.args[0] def test_malformed_from_file(monkeypatch): """Raise an exception when malformed package file triggers EOFError.""" filename = "tests/fixtures/malformed.tar.gz" with pytest.raises(exceptions.InvalidDistribution) as err: package_file.PackageFile.from_filename(filename, comment=None) assert "Invalid distribution file" in err.value.args[0] def test_package_from_egg(): filename = "tests/fixtures/twine-3.3.0-py3.9.egg" package_file.PackageFile.from_filename(filename, comment=None) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/test_register.py0000644000175100001770000001023414562147542016660 0ustar00runnerdockerimport pretend import pytest from twine import cli from twine import exceptions from twine.commands import register from . import helpers @pytest.fixture() def register_settings(make_settings): """Return a factory function for settings.Settings for register.""" return make_settings( """ [pypi] repository: https://test.pypi.org/legacy/ username:foo password:bar """ ) def test_successful_register(register_settings): """Return a successful result for a valid repository url and package.""" stub_response = pretend.stub( is_redirect=False, status_code=200, headers={"location": "https://test.pypi.org/legacy/"}, raise_for_status=lambda: None, ) stub_repository = pretend.stub( register=lambda package: stub_response, close=lambda: None ) register_settings.create_repository = lambda: stub_repository result = register.register(register_settings, helpers.WHEEL_FIXTURE) assert result is None def test_exception_for_redirect(register_settings): """Raise an exception when repository URL results in a redirect.""" repository_url = register_settings.repository_config["repository"] redirect_url = "https://malicious.website.org/danger/" stub_response = pretend.stub( is_redirect=True, status_code=301, headers={"location": redirect_url}, ) stub_repository = pretend.stub( register=lambda package: stub_response, close=lambda: None ) register_settings.create_repository = lambda: stub_repository with pytest.raises( exceptions.RedirectDetected, match=rf"{repository_url}.+{redirect_url}.+\nIf you trust these URLs", ): register.register(register_settings, helpers.WHEEL_FIXTURE) def test_non_existent_package(register_settings): """Raise an exception when package file doesn't exist.""" stub_repository = pretend.stub() register_settings.create_repository = lambda: stub_repository package = "/foo/bar/baz.whl" with pytest.raises( exceptions.PackageNotFound, match=f'"{package}" does not exist on the file system.', ): register.register(register_settings, package) @pytest.mark.parametrize("repo", ["pypi", "testpypi"]) def test_values_from_env_pypi(monkeypatch, repo): """Use env vars for settings when run from command line.""" def none_register(*args, **settings_kwargs): pass replaced_register = pretend.call_recorder(none_register) monkeypatch.setattr(register, "register", replaced_register) testenv = { "TWINE_REPOSITORY": repo, # Ignored because the TWINE_REPOSITORY is PyPI/TestPyPI "TWINE_USERNAME": "this-is-ignored", "TWINE_PASSWORD": "pypipassword", "TWINE_CERT": "/foo/bar.crt", } with helpers.set_env(**testenv): cli.dispatch(["register", helpers.WHEEL_FIXTURE]) register_settings = replaced_register.calls[0].args[0] assert "pypipassword" == register_settings.password assert "__token__" == register_settings.username assert "/foo/bar.crt" == register_settings.cacert def test_values_from_env_not_pypi(monkeypatch, write_config_file): """Use env vars for settings when run from command line.""" write_config_file( """ [distutils] index-servers = notpypi [notpypi] repository: https://upload.example.org/legacy/ username:someusername password:password """ ) def none_register(*args, **settings_kwargs): pass replaced_register = pretend.call_recorder(none_register) monkeypatch.setattr(register, "register", replaced_register) testenv = { "TWINE_REPOSITORY": "notpypi", "TWINE_USERNAME": "someusername", "TWINE_PASSWORD": "pypipassword", "TWINE_CERT": "/foo/bar.crt", } with helpers.set_env(**testenv): cli.dispatch(["register", helpers.WHEEL_FIXTURE]) register_settings = replaced_register.calls[0].args[0] assert "pypipassword" == register_settings.password assert "someusername" == register_settings.username assert "/foo/bar.crt" == register_settings.cacert ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/test_repository.py0000644000175100001770000002527714562147542017270 0ustar00runnerdocker# Copyright 2015 Ian Cordasco # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging from contextlib import contextmanager import pretend import pytest import requests from twine import repository from twine import utils @pytest.fixture() def default_repo(): return repository.Repository( repository_url=utils.DEFAULT_REPOSITORY, username="username", password="password", ) def test_gpg_signature_structure_is_preserved(): """Preserve 'gpg_signature' key when converting metadata.""" data = { "gpg_signature": ("filename.asc", "filecontent"), } tuples = repository.Repository._convert_data_to_list_of_tuples(data) assert tuples == [("gpg_signature", ("filename.asc", "filecontent"))] def test_content_structure_is_preserved(): """Preserve 'content' key when converting metadata.""" data = { "content": ("filename", "filecontent"), } tuples = repository.Repository._convert_data_to_list_of_tuples(data) assert tuples == [("content", ("filename", "filecontent"))] def test_iterables_are_flattened(): """Flatten iterable metadata to list of tuples.""" data = { "platform": ["UNKNOWN"], } tuples = repository.Repository._convert_data_to_list_of_tuples(data) assert tuples == [("platform", "UNKNOWN")] data = { "platform": ["UNKNOWN", "ANOTHERPLATFORM"], } tuples = repository.Repository._convert_data_to_list_of_tuples(data) assert tuples == [("platform", "UNKNOWN"), ("platform", "ANOTHERPLATFORM")] def test_set_client_certificate(default_repo): """Set client certificate for session.""" assert default_repo.session.cert is None default_repo.set_client_certificate(("/path/to/cert", "/path/to/key")) assert default_repo.session.cert == ("/path/to/cert", "/path/to/key") def test_set_certificate_authority(default_repo): """Set certificate authority for session.""" assert default_repo.session.verify is True default_repo.set_certificate_authority("/path/to/cert") assert default_repo.session.verify == "/path/to/cert" def test_make_user_agent_string(default_repo): """Add twine to User-Agent session header.""" assert "twine/" in default_repo.session.headers["User-Agent"] def response_with(**kwattrs): resp = requests.Response() for attr, value in kwattrs.items(): if hasattr(resp, attr): setattr(resp, attr, value) return resp def test_package_is_uploaded_404s(default_repo): """Return False when the project API response status isn't 200.""" default_repo.session = pretend.stub( get=lambda url, headers: response_with(status_code=404) ) package = pretend.stub(safe_name="fake", metadata=pretend.stub(version="2.12.0")) assert default_repo.package_is_uploaded(package) is False def test_package_is_uploaded_200s_with_no_releases(default_repo): """Return False when the list of releases for a project is empty.""" default_repo.session = pretend.stub( get=lambda url, headers: response_with( status_code=200, _content=b'{"releases": {}}', _content_consumed=True ), ) package = pretend.stub(safe_name="fake", metadata=pretend.stub(version="2.12.0")) assert default_repo.package_is_uploaded(package) is False def test_package_is_uploaded_with_releases_using_cache(default_repo): """Return True when the package is in the releases cache.""" default_repo._releases_json_data = {"fake": {"0.1": [{"filename": "fake.whl"}]}} package = pretend.stub( safe_name="fake", basefilename="fake.whl", metadata=pretend.stub(version="0.1"), ) assert default_repo.package_is_uploaded(package) is True def test_package_is_uploaded_with_releases_not_using_cache(default_repo): """Return True when the package is in the list of releases for a project.""" default_repo.session = pretend.stub( get=lambda url, headers: response_with( status_code=200, _content=b'{"releases": {"0.1": [{"filename": "fake.whl"}]}}', _content_consumed=True, ), ) package = pretend.stub( safe_name="fake", basefilename="fake.whl", metadata=pretend.stub(version="0.1"), ) assert default_repo.package_is_uploaded(package, bypass_cache=True) is True def test_package_is_uploaded_different_filenames(default_repo): """Return False when the package is not in the list of releases for a project.""" default_repo.session = pretend.stub( get=lambda url, headers: response_with( status_code=200, _content=b'{"releases": {"0.1": [{"filename": "fake.whl"}]}}', _content_consumed=True, ), ) package = pretend.stub( safe_name="fake", basefilename="foo.whl", metadata=pretend.stub(version="0.1"), ) assert default_repo.package_is_uploaded(package) is False def test_package_is_registered(default_repo): """Return API response from registering a package.""" package = pretend.stub( basefilename="fake.whl", metadata_dictionary=lambda: {"name": "fake"} ) resp = response_with(status_code=200) setattr(resp, "raw", pretend.stub()) setattr(resp.raw, "close", lambda: None) default_repo.session = pretend.stub( post=lambda url, data, allow_redirects, headers: resp ) assert default_repo.register(package) @pytest.mark.parametrize("disable_progress_bar", [True, False]) def test_disable_progress_bar_is_forwarded_to_rich( monkeypatch, tmpdir, disable_progress_bar, default_repo ): """Toggle display of upload progress bar.""" @contextmanager def ProgressStub(*args, **kwargs): assert "disable" in kwargs assert kwargs["disable"] == disable_progress_bar yield pretend.stub( add_task=lambda description, total: None, update=lambda task_id, completed: None, ) monkeypatch.setattr(repository.rich.progress, "Progress", ProgressStub) default_repo.disable_progress_bar = disable_progress_bar default_repo.session = pretend.stub( post=lambda url, data, allow_redirects, headers: response_with(status_code=200) ) fakefile = tmpdir.join("fake.whl") fakefile.write(".") def dictfunc(): return {"name": "fake"} package = pretend.stub( safe_name="fake", metadata=pretend.stub(version="2.12.0"), basefilename="fake.whl", filename=str(fakefile), metadata_dictionary=dictfunc, ) default_repo.upload(package) def test_upload_retry(tmpdir, default_repo, caplog): """Print retry messages when the upload response indicates a server error.""" default_repo.disable_progress_bar = True default_repo.session = pretend.stub( post=lambda url, data, allow_redirects, headers: response_with( status_code=500, reason="Internal server error" ) ) fakefile = tmpdir.join("fake.whl") fakefile.write(".") package = pretend.stub( safe_name="fake", metadata=pretend.stub(version="2.12.0"), basefilename="fake.whl", filename=str(fakefile), metadata_dictionary=lambda: {"name": "fake"}, ) # Upload with default max_redirects of 5 default_repo.upload(package) assert caplog.messages == [ ( 'Received "500: Internal server error"\n' f"Package upload appears to have failed. Retry {i} of 5." ) for i in range(1, 6) ] caplog.clear() # Upload with custom max_redirects of 3 default_repo.upload(package, 3) assert caplog.messages == [ ( 'Received "500: Internal server error"\n' f"Package upload appears to have failed. Retry {i} of 3." ) for i in range(1, 4) ] @pytest.mark.parametrize( "package_meta,repository_url,release_urls", [ # Single package ( [("fake", "2.12.0")], utils.DEFAULT_REPOSITORY, {"https://pypi.org/project/fake/2.12.0/"}, ), # Single package to testpypi ( [("fake", "2.12.0")], utils.TEST_REPOSITORY, {"https://test.pypi.org/project/fake/2.12.0/"}, ), # Multiple packages (faking a wheel and an sdist) ( [("fake", "2.12.0"), ("fake", "2.12.0")], utils.DEFAULT_REPOSITORY, {"https://pypi.org/project/fake/2.12.0/"}, ), # Multiple releases ( [("fake", "2.12.0"), ("fake", "2.12.1")], utils.DEFAULT_REPOSITORY, { "https://pypi.org/project/fake/2.12.0/", "https://pypi.org/project/fake/2.12.1/", }, ), # Not pypi ([("fake", "2.12.0")], "http://devpi.example.com", set()), # No packages ([], utils.DEFAULT_REPOSITORY, set()), ], ) def test_release_urls(package_meta, repository_url, release_urls): """Generate a set of PyPI release URLs for a list of packages.""" packages = [ pretend.stub(safe_name=name, metadata=pretend.stub(version=version)) for name, version in package_meta ] repo = repository.Repository( repository_url=repository_url, username="username", password="password", ) assert repo.release_urls(packages) == release_urls def test_package_is_uploaded_incorrect_repo_url(): """Return False when using an incorrect repository URL.""" repo = repository.Repository( repository_url="https://bad.repo.com/legacy", username="username", password="password", ) repo.url = "https://bad.repo.com/legacy" assert repo.package_is_uploaded(None) is False @pytest.mark.parametrize( "username, password, messages", [ (None, None, ["username: ", "password: "]), ("", "", ["username: ", "password: "]), ("username", "password", ["username: username", "password: "]), ], ) def test_logs_username_and_password(username, password, messages, caplog): caplog.set_level(logging.INFO, "twine") repository.Repository( repository_url=utils.DEFAULT_REPOSITORY, username=username, password=password, ) assert caplog.messages == messages ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/test_settings.py0000644000175100001770000001271514562147542016702 0ustar00runnerdocker"""Tests for the Settings class and module.""" # Copyright 2018 Ian Stapleton Cordasco # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import logging import pytest from twine import exceptions from twine import settings def test_settings_takes_no_positional_arguments(): """Raise an exception when Settings is initialized without keyword arguments.""" with pytest.raises(TypeError): settings.Settings("a", "b", "c") def test_settings_transforms_repository_config_pypi(write_config_file): """Set repository config and defaults when .pypirc is provided. Ignores the username setting due to PyPI being the index. """ config_file = write_config_file( """ [pypi] repository: https://upload.pypi.org/legacy/ username:this-is-ignored password:password """ ) s = settings.Settings(config_file=config_file) assert s.repository_config["repository"] == "https://upload.pypi.org/legacy/" assert s.sign is False assert s.sign_with == "gpg" assert s.identity is None assert s.username == "__token__" assert s.password == "password" assert s.cacert is None assert s.client_cert is None assert s.disable_progress_bar is False def test_settings_transforms_repository_config_non_pypi(write_config_file): """Set repository config and defaults when .pypirc is provided.""" config_file = write_config_file( """ [distutils] index-servers = notpypi [notpypi] repository: https://upload.example.org/legacy/ username:someusername password:password """ ) s = settings.Settings(config_file=config_file, repository_name="notpypi") assert s.repository_config["repository"] == "https://upload.example.org/legacy/" assert s.sign is False assert s.sign_with == "gpg" assert s.identity is None assert s.username == "someusername" assert s.password == "password" assert s.cacert is None assert s.client_cert is None assert s.disable_progress_bar is False @pytest.mark.parametrize( "verbose, log_level", [(True, logging.INFO), (False, logging.WARNING)] ) def test_setup_logging(verbose, log_level): """Set log level based on verbose field.""" settings.Settings(verbose=verbose) logger = logging.getLogger("twine") assert logger.level == log_level @pytest.mark.parametrize( "verbose", [True, False], ) def test_print_config_path_if_verbose(config_file, caplog, make_settings, verbose): """Print the location of the .pypirc config used by the user.""" make_settings(verbose=verbose) if verbose: assert caplog.messages == [f"Using configuration from {config_file}"] else: assert caplog.messages == [] def test_identity_requires_sign(): """Raise an exception when user provides identity but doesn't require signing.""" with pytest.raises(exceptions.InvalidSigningConfiguration): settings.Settings(sign=False, identity="fakeid") @pytest.mark.parametrize("client_cert", [None, ""]) def test_password_is_required_if_no_client_cert(client_cert, entered_password): """Set password when client_cert is not provided.""" settings_obj = settings.Settings(username="fakeuser", client_cert=client_cert) assert settings_obj.password == "entered pw" def test_client_cert_and_password_both_set_if_given(): """Set password and client_cert when both are provided.""" client_cert = "/random/path" settings_obj = settings.Settings( username="fakeuser", password="anything", client_cert=client_cert ) assert settings_obj.password == "anything" assert settings_obj.client_cert == client_cert def test_password_required_if_no_client_cert_and_non_interactive(): """Raise exception if no password or client_cert when non interactive.""" settings_obj = settings.Settings(username="fakeuser", non_interactive=True) with pytest.raises(exceptions.NonInteractive): settings_obj.password def test_no_password_prompt_if_client_cert_and_non_interactive(entered_password): """Don't prompt for password when client_cert is provided and non interactive.""" client_cert = "/random/path" settings_obj = settings.Settings( username="fakeuser", client_cert=client_cert, non_interactive=True ) assert not settings_obj.password class TestArgumentParsing: @staticmethod def parse_args(args): parser = argparse.ArgumentParser() settings.Settings.register_argparse_arguments(parser) return parser.parse_args(args) def test_non_interactive_flag(self): args = self.parse_args(["--non-interactive"]) assert args.non_interactive def test_non_interactive_environment(self, monkeypatch): monkeypatch.setenv("TWINE_NON_INTERACTIVE", "1") args = self.parse_args([]) assert args.non_interactive monkeypatch.setenv("TWINE_NON_INTERACTIVE", "0") args = self.parse_args([]) assert not args.non_interactive ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/test_upload.py0000644000175100001770000004520614562147542016327 0ustar00runnerdocker# Copyright 2014 Ian Cordasco # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import pretend import pytest import requests from twine import cli from twine import exceptions from twine import package as package_file from twine.commands import upload from . import helpers RELEASE_URL = "https://pypi.org/project/twine/1.5.0/" NEW_RELEASE_URL = "https://pypi.org/project/twine/1.6.5/" @pytest.fixture def stub_response(): """Mock successful upload of a package.""" return pretend.stub( is_redirect=False, url="https://test.pypi.org/legacy/", status_code=200, reason="OK", text=None, raise_for_status=lambda: None, ) @pytest.fixture def stub_repository(stub_response): """Allow assertions on the uploaded package.""" return pretend.stub( upload=pretend.call_recorder(lambda package: stub_response), close=lambda: None, release_urls=lambda packages: set(), ) @pytest.fixture def upload_settings(make_settings, stub_repository): """Use the stub repository when uploading.""" upload_settings = make_settings() upload_settings.create_repository = lambda: stub_repository return upload_settings def test_make_package_pre_signed_dist(upload_settings, caplog): """Create a PackageFile and print path, size, and user-provided signature.""" filename = helpers.WHEEL_FIXTURE expected_size = "15.4 KB" signed_filename = helpers.WHEEL_FIXTURE + ".asc" signatures = {os.path.basename(signed_filename): signed_filename} upload_settings.sign = True upload_settings.verbose = True package = upload._make_package(filename, signatures, upload_settings) assert package.filename == filename assert package.gpg_signature is not None assert caplog.messages == [ f"{filename} ({expected_size})", f"Signed with {signed_filename}", ] def test_make_package_unsigned_dist(upload_settings, monkeypatch, caplog): """Create a PackageFile and print path, size, and Twine-generated signature.""" filename = helpers.NEW_WHEEL_FIXTURE expected_size = "21.9 KB" signatures = {} upload_settings.sign = True upload_settings.verbose = True def stub_sign(package, *_): package.gpg_signature = (package.signed_basefilename, b"signature") monkeypatch.setattr(package_file.PackageFile, "sign", stub_sign) package = upload._make_package(filename, signatures, upload_settings) assert package.filename == filename assert package.gpg_signature is not None assert caplog.messages == [ f"{filename} ({expected_size})", f"Signed with {package.signed_filename}", ] def test_successs_prints_release_urls(upload_settings, stub_repository, capsys): """Print PyPI release URLS for each uploaded package.""" stub_repository.release_urls = lambda packages: {RELEASE_URL, NEW_RELEASE_URL} result = upload.upload( upload_settings, [ helpers.WHEEL_FIXTURE, helpers.SDIST_FIXTURE, helpers.NEW_SDIST_FIXTURE, helpers.NEW_WHEEL_FIXTURE, ], ) assert result is None captured = capsys.readouterr() assert captured.out.count(RELEASE_URL) == 1 assert captured.out.count(NEW_RELEASE_URL) == 1 def test_print_packages_if_verbose(upload_settings, caplog): """Print the path and file size of each distribution attempting to be uploaded.""" dists_to_upload = { helpers.WHEEL_FIXTURE: "15.4 KB", helpers.NEW_WHEEL_FIXTURE: "21.9 KB", helpers.SDIST_FIXTURE: "20.8 KB", helpers.NEW_SDIST_FIXTURE: "26.1 KB", } upload_settings.verbose = True result = upload.upload(upload_settings, dists_to_upload.keys()) assert result is None assert [m for m in caplog.messages if m.endswith("KB)")] == [ f"{filename} ({size})" for filename, size in dists_to_upload.items() ] def test_print_response_if_verbose(upload_settings, stub_response, caplog): """Print details about the response from the repository.""" upload_settings.verbose = True result = upload.upload( upload_settings, [helpers.WHEEL_FIXTURE, helpers.SDIST_FIXTURE], ) assert result is None response_log = ( f"Response from {stub_response.url}:\n" f"{stub_response.status_code} {stub_response.reason}" ) assert caplog.messages.count(response_log) == 2 def test_success_with_pre_signed_distribution(upload_settings, stub_repository, caplog): """Add GPG signature provided by user to uploaded package.""" # Upload a pre-signed distribution result = upload.upload( upload_settings, [helpers.WHEEL_FIXTURE, helpers.WHEEL_FIXTURE + ".asc"] ) assert result is None # The signature should be added via package.add_gpg_signature() package = stub_repository.upload.calls[0].args[0] assert package.gpg_signature == ( "twine-1.5.0-py2.py3-none-any.whl.asc", b"signature", ) # Ensure that a warning is emitted. assert ( "One or more packages has an associated PGP signature; these will " "be silently ignored by the index" in caplog.messages ) def test_warns_potential_pgp_removal_on_3p_index( make_settings, stub_repository, caplog ): """Warn when a PGP signature is specified for upload to a third-party index.""" upload_settings = make_settings( """ [pypi] repository: https://example.com/not-a-real-index/ username:foo password:bar """ ) upload_settings.create_repository = lambda: stub_repository # Upload a pre-signed distribution result = upload.upload( upload_settings, [helpers.WHEEL_FIXTURE, helpers.WHEEL_FIXTURE + ".asc"] ) assert result is None # The signature should be added via package.add_gpg_signature() package = stub_repository.upload.calls[0].args[0] assert package.gpg_signature == ( "twine-1.5.0-py2.py3-none-any.whl.asc", b"signature", ) # Ensure that a warning is emitted. assert ( "One or more packages has an associated PGP signature; a future " "version of twine may silently ignore these. See " "https://github.com/pypa/twine/issues/1009 for more information" in caplog.messages ) def test_exception_with_only_pre_signed_file(upload_settings, stub_repository): """Raise an exception when only a signed file is uploaded.""" # Upload only pre-signed file with pytest.raises(exceptions.InvalidDistribution) as err: upload.upload(upload_settings, [helpers.WHEEL_FIXTURE + ".asc"]) assert ( "Cannot upload signed files by themselves, must upload with a " "corresponding distribution file." in err.value.args[0] ) def test_success_when_gpg_is_run(upload_settings, stub_repository, monkeypatch): """Add GPG signature generated by gpg command to uploaded package.""" # Indicate that upload() should run_gpg() to generate the signature, which # we'll stub out to use WHEEL_FIXTURE + ".asc" upload_settings.sign = True upload_settings.sign_with = "gpg" monkeypatch.setattr( package_file.PackageFile, "run_gpg", pretend.call_recorder(lambda cls, gpg_args: None), ) # Upload an unsigned distribution result = upload.upload(upload_settings, [helpers.WHEEL_FIXTURE]) assert result is None # The signature should be added via package.sign() package = stub_repository.upload.calls[0].args[0] assert len(package.run_gpg.calls) == 1 assert helpers.WHEEL_FIXTURE in package.run_gpg.calls[0].args[1] assert package.gpg_signature == ( "twine-1.5.0-py2.py3-none-any.whl.asc", b"signature", ) @pytest.mark.parametrize("verbose", [False, True]) def test_exception_for_http_status( verbose, upload_settings, stub_response, capsys, caplog ): upload_settings.verbose = verbose stub_response.is_redirect = False stub_response.status_code = 403 stub_response.reason = "Invalid or non-existent authentication information" stub_response.text = stub_response.reason stub_response.raise_for_status = pretend.raiser(requests.HTTPError) with pytest.raises(requests.HTTPError): upload.upload(upload_settings, [helpers.WHEEL_FIXTURE]) captured = capsys.readouterr() assert RELEASE_URL not in captured.out if verbose: assert caplog.messages == [ f"{helpers.WHEEL_FIXTURE} (15.4 KB)", f"Response from {stub_response.url}:\n403 {stub_response.reason}", stub_response.text, ] else: assert caplog.messages == [ "Error during upload. Retry with the --verbose option for more details." ] def test_get_config_old_format(make_settings, config_file): try: make_settings( """ [server-login] username:foo password:bar """ ) except KeyError as err: assert all( text in err.args[0] for text in [ "'pypi'", "--repository-url", config_file, "https://docs.python.org/", ] ) def test_deprecated_repo(make_settings): with pytest.raises(exceptions.UploadToDeprecatedPyPIDetected) as err: upload_settings = make_settings( """ [pypi] repository: https://pypi.python.org/pypi/ username:foo password:bar """ ) upload.upload(upload_settings, [helpers.WHEEL_FIXTURE]) assert all( text in err.value.args[0] for text in [ "https://pypi.python.org/pypi/", "https://upload.pypi.org/legacy/", "https://test.pypi.org/legacy/", "https://packaging.python.org/", ] ) @pytest.mark.parametrize( "repository_url, redirect_url, message_match", [ ( "https://test.pypi.org/legacy", "https://test.pypi.org/legacy/", ( r"https://test.pypi.org/legacy.+https://test.pypi.org/legacy/" r".+\nYour repository URL is missing a trailing slash" ), ), ( "https://test.pypi.org/legacy/", "https://malicious.website.org/danger/", ( r"https://test.pypi.org/legacy/.+https://malicious.website.org/danger/" r".+\nIf you trust these URLs" ), ), ], ) def test_exception_for_redirect( repository_url, redirect_url, message_match, make_settings, ): # Not using fixtures because this setup is significantly different upload_settings = make_settings( f""" [pypi] repository: {repository_url} username:foo password:bar """ ) stub_response = pretend.stub( is_redirect=True, url=redirect_url, status_code=301, headers={"location": redirect_url}, reason="Redirect", text="", ) stub_repository = pretend.stub( upload=lambda package: stub_response, close=lambda: None ) upload_settings.create_repository = lambda: stub_repository with pytest.raises(exceptions.RedirectDetected, match=message_match): upload.upload(upload_settings, [helpers.WHEEL_FIXTURE]) def test_prints_skip_message_for_uploaded_package( upload_settings, stub_repository, capsys, caplog ): upload_settings.skip_existing = True # Short-circuit the upload stub_repository.package_is_uploaded = lambda package: True result = upload.upload(upload_settings, [helpers.WHEEL_FIXTURE]) assert result is None captured = capsys.readouterr() assert RELEASE_URL not in captured.out assert caplog.messages == [ "Skipping twine-1.5.0-py2.py3-none-any.whl " "because it appears to already exist" ] def test_prints_skip_message_for_response( upload_settings, stub_response, stub_repository, capsys, caplog ): upload_settings.skip_existing = True stub_response.status_code = 400 stub_response.reason = "File already exists" stub_response.text = stub_response.reason # Do the upload, triggering the error response stub_repository.package_is_uploaded = lambda package: False result = upload.upload(upload_settings, [helpers.WHEEL_FIXTURE]) assert result is None captured = capsys.readouterr() assert RELEASE_URL not in captured.out assert caplog.messages == [ "Skipping twine-1.5.0-py2.py3-none-any.whl " "because it appears to already exist" ] @pytest.mark.parametrize( "response_kwargs", [ pytest.param( dict( status_code=400, reason=( 'A file named "twine-1.5.0-py2.py3-none-any.whl" already ' "exists for twine-1.5.0." ), ), id="pypi", ), pytest.param( dict( status_code=400, reason=( "Repository does not allow updating assets: pypi for url: " "http://www.foo.bar" ), ), id="nexus", ), pytest.param( dict( status_code=400, text=( '
\n' " Repository does not allow updating assets: pypi-local\n" "
\n" ), ), id="nexus_new", ), pytest.param( dict( status_code=409, reason=( 'A file named "twine-1.5.0-py2.py3-none-any.whl" already ' "exists for twine-1.5.0." ), ), id="pypiserver", ), pytest.param( dict( status_code=403, text=( "Not enough permissions to overwrite artifact " "'pypi-local:twine/1.5.0/twine-1.5.0-py2.py3-none-any.whl'" "(user 'twine-deployer' needs DELETE permission)." ), ), id="artifactory_old", ), pytest.param( dict( status_code=403, text=( "Not enough permissions to delete/overwrite artifact " "'pypi-local:twine/1.5.0/twine-1.5.0-py2.py3-none-any.whl'" "(user 'twine-deployer' needs DELETE permission)." ), ), id="artifactory_new", ), pytest.param( dict( status_code=400, text=( '{"message":"validation failed: file name has already been taken"}' ), ), id="gitlab_enterprise", ), ], ) def test_skip_existing_skips_files_on_repository(response_kwargs): assert upload.skip_upload( response=pretend.stub(**response_kwargs), skip_existing=True, package=package_file.PackageFile.from_filename(helpers.WHEEL_FIXTURE, None), ) @pytest.mark.parametrize( "response_kwargs", [ pytest.param( dict(status_code=400, reason="Invalid credentials"), id="wrong_reason" ), pytest.param(dict(status_code=404), id="wrong_code"), ], ) def test_skip_upload_doesnt_match(response_kwargs): assert not upload.skip_upload( response=pretend.stub(**response_kwargs), skip_existing=True, package=package_file.PackageFile.from_filename(helpers.WHEEL_FIXTURE, None), ) def test_skip_upload_respects_skip_existing(): assert not upload.skip_upload( response=pretend.stub(), skip_existing=False, package=package_file.PackageFile.from_filename(helpers.WHEEL_FIXTURE, None), ) @pytest.mark.parametrize("repo", ["pypi", "testpypi"]) def test_values_from_env_pypi(monkeypatch, repo): def none_upload(*args, **settings_kwargs): pass replaced_upload = pretend.call_recorder(none_upload) monkeypatch.setattr(upload, "upload", replaced_upload) testenv = { "TWINE_REPOSITORY": repo, # Ignored because TWINE_REPOSITORY is PyPI/TestPyPI "TWINE_USERNAME": "this-is-ignored", "TWINE_PASSWORD": "pypipassword", "TWINE_CERT": "/foo/bar.crt", } with helpers.set_env(**testenv): cli.dispatch(["upload", "path/to/file"]) upload_settings = replaced_upload.calls[0].args[0] assert "pypipassword" == upload_settings.password assert "__token__" == upload_settings.username assert "/foo/bar.crt" == upload_settings.cacert def test_values_from_env_non_pypi(monkeypatch, write_config_file): write_config_file( """ [distutils] index-servers = notpypi [notpypi] repository: https://upload.example.org/legacy/ username:someusername password:password """ ) def none_upload(*args, **settings_kwargs): pass replaced_upload = pretend.call_recorder(none_upload) monkeypatch.setattr(upload, "upload", replaced_upload) testenv = { "TWINE_REPOSITORY": "notpypi", "TWINE_USERNAME": "someusername", "TWINE_PASSWORD": "pypipassword", "TWINE_CERT": "/foo/bar.crt", } with helpers.set_env(**testenv): cli.dispatch(["upload", "path/to/file"]) upload_settings = replaced_upload.calls[0].args[0] assert "pypipassword" == upload_settings.password assert "someusername" == upload_settings.username assert "/foo/bar.crt" == upload_settings.cacert @pytest.mark.parametrize( "repo_url", ["https://upload.pypi.org/", "https://test.pypi.org/", "https://pypi.org/"], ) def test_check_status_code_for_wrong_repo_url(repo_url, upload_settings, stub_response): upload_settings.repository_config["repository"] = repo_url stub_response.url = repo_url stub_response.status_code = 405 with pytest.raises(exceptions.InvalidPyPIUploadURL): upload.upload( upload_settings, [ helpers.WHEEL_FIXTURE, helpers.SDIST_FIXTURE, helpers.NEW_SDIST_FIXTURE, helpers.NEW_WHEEL_FIXTURE, ], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/test_utils.py0000644000175100001770000002112614562147542016176 0ustar00runnerdocker# Copyright 2013 Donald Stufft # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os.path import pretend import pytest import requests from twine import exceptions from twine import utils from . import helpers def test_get_config(write_config_file): config_file = write_config_file( """ [distutils] index-servers = pypi [pypi] username = testuser password = testpassword """ ) assert utils.get_config(config_file) == { "pypi": { "repository": utils.DEFAULT_REPOSITORY, "username": "testuser", "password": "testpassword", }, } def test_get_config_no_distutils(write_config_file): """Upload by default to PyPI if an index server is not set in .pypirc.""" config_file = write_config_file( """ [pypi] username = testuser password = testpassword """ ) assert utils.get_config(config_file) == { "pypi": { "repository": utils.DEFAULT_REPOSITORY, "username": "testuser", "password": "testpassword", }, "testpypi": { "repository": utils.TEST_REPOSITORY, "username": None, "password": None, }, } def test_get_config_no_section(write_config_file): config_file = write_config_file( """ [distutils] index-servers = pypi foo [pypi] username = testuser password = testpassword """ ) assert utils.get_config(config_file) == { "pypi": { "repository": utils.DEFAULT_REPOSITORY, "username": "testuser", "password": "testpassword", }, } def test_get_config_override_pypi_url(write_config_file): config_file = write_config_file( """ [pypi] repository = http://pypiproxy """ ) assert utils.get_config(config_file)["pypi"]["repository"] == "http://pypiproxy" def test_get_config_missing(config_file): assert utils.get_config(config_file) == { "pypi": { "repository": utils.DEFAULT_REPOSITORY, "username": None, "password": None, }, "testpypi": { "repository": utils.TEST_REPOSITORY, "username": None, "password": None, }, } def test_empty_userpass(write_config_file): """Suppress prompts if empty username and password are provided in .pypirc.""" config_file = write_config_file( """ [pypi] username= password= """ ) config = utils.get_config(config_file) pypi = config["pypi"] assert pypi["username"] == pypi["password"] == "" def test_get_repository_config_missing(config_file): repository_url = "https://notexisting.python.org/pypi" exp = { "repository": repository_url, "username": None, "password": None, } assert utils.get_repository_from_config(config_file, "foo", repository_url) == exp assert utils.get_repository_from_config(config_file, "pypi", repository_url) == exp exp = { "repository": utils.DEFAULT_REPOSITORY, "username": None, "password": None, } assert utils.get_repository_from_config(config_file, "pypi") == exp @pytest.mark.parametrize( "repo_url, message", [ ( "ftp://test.pypi.org", r"scheme was required to be one of \['http', 'https'\]", ), ("https:/", "host was required but missing."), ("//test.pypi.org", "scheme was required but missing."), ("foo.bar", "host, scheme were required but missing."), ], ) def test_get_repository_config_with_invalid_url(config_file, repo_url, message): """Raise an exception for a URL with an invalid/missing scheme and/or host.""" with pytest.raises( exceptions.UnreachableRepositoryURLDetected, match=message, ): utils.get_repository_from_config(config_file, "pypi", repo_url) def test_get_repository_config_missing_repository(write_config_file): """Raise an exception when a custom repository isn't defined in .pypirc.""" config_file = write_config_file("") with pytest.raises( exceptions.InvalidConfiguration, match="Missing 'missing-repository'", ): utils.get_repository_from_config(config_file, "missing-repository") @pytest.mark.parametrize("repository", ["pypi", "missing-repository"]) def test_get_repository_config_missing_file(repository): """Raise an exception when a custom config file doesn't exist.""" with pytest.raises( exceptions.InvalidConfiguration, match=r"No such file.*missing-file", ): utils.get_repository_from_config("missing-file", repository) def test_get_config_deprecated_pypirc(): tests_dir = os.path.dirname(os.path.abspath(__file__)) deprecated_pypirc_path = os.path.join(tests_dir, "fixtures", "deprecated-pypirc") assert utils.get_config(deprecated_pypirc_path) == { "pypi": { "repository": utils.DEFAULT_REPOSITORY, "username": "testusername", "password": "testpassword", }, "testpypi": { "repository": utils.TEST_REPOSITORY, "username": "testusername", "password": "testpassword", }, } @pytest.mark.parametrize( ("cli_value", "config", "key", "strategy", "expected"), ( ("cli", {}, "key", lambda: "fallback", "cli"), (None, {"key": "value"}, "key", lambda: "fallback", "value"), (None, {}, "key", lambda: "fallback", "fallback"), ), ) def test_get_userpass_value(cli_value, config, key, strategy, expected): ret = utils.get_userpass_value(cli_value, config, key, strategy) assert ret == expected @pytest.mark.parametrize( ("env_name", "default", "environ", "expected"), [ ("MY_PASSWORD", None, {}, None), ("MY_PASSWORD", None, {"MY_PASSWORD": "foo"}, "foo"), ("URL", "https://example.org", {}, "https://example.org"), ("URL", "https://example.org", {"URL": "https://pypi.org"}, "https://pypi.org"), ], ) def test_default_to_environment_action(env_name, default, environ, expected): option_strings = ("-x", "--example") dest = "example" with helpers.set_env(**environ): action = utils.EnvironmentDefault( env=env_name, default=default, option_strings=option_strings, dest=dest, ) assert action.env == env_name assert action.default == expected @pytest.mark.parametrize( "repo_url", ["https://pypi.python.org", "https://testpypi.python.org"] ) def test_check_status_code_for_deprecated_pypi_url(repo_url): response = pretend.stub(status_code=410, url=repo_url) # value of Verbose doesn't matter for this check with pytest.raises(exceptions.UploadToDeprecatedPyPIDetected): utils.check_status_code(response, False) @pytest.mark.parametrize( "repo_url", ["https://pypi.python.org", "https://testpypi.python.org"], ) @pytest.mark.parametrize( "verbose", [True, False], ) def test_check_status_code_for_missing_status_code( caplog, repo_url, verbose, make_settings, config_file ): """Print HTTP errors based on verbosity level.""" response = pretend.stub( status_code=403, url=repo_url, raise_for_status=pretend.raiser(requests.HTTPError), text="Forbidden", ) make_settings(verbose=verbose) with pytest.raises(requests.HTTPError): utils.check_status_code(response, verbose) message = ( "Error during upload. Retry with the --verbose option for more details." ) assert caplog.messages.count(message) == 0 if verbose else 1 @pytest.mark.parametrize( ("size_in_bytes, formatted_size"), [(3704, "3.6 KB"), (1153433, "1.1 MB"), (21412841, "20.4 MB")], ) def test_get_file_size(size_in_bytes, formatted_size, monkeypatch): """Get the size of file as a string with units.""" monkeypatch.setattr(os.path, "getsize", lambda _: size_in_bytes) file_size = utils.get_file_size(size_in_bytes) assert file_size == formatted_size ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tests/test_wheel.py0000644000175100001770000000600314562147542016137 0ustar00runnerdocker# Copyright 2015 Ian Cordasco # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import pathlib import re import zipfile import pretend import pytest from twine import exceptions from twine import wheel from . import helpers @pytest.fixture( params=[ "fixtures/twine-1.5.0-py2.py3-none-any.whl", "alt-fixtures/twine-1.5.0-py2.py3-none-any.whl", ] ) def example_wheel(request): file_name = os.path.join(helpers.TESTS_DIR, request.param) return wheel.Wheel(file_name) def test_version_parsing(example_wheel): assert example_wheel.py_version == "py2.py3" def test_version_parsing_missing_pyver(monkeypatch, example_wheel): wheel.wheel_file_re = pretend.stub(match=lambda a: None) assert example_wheel.py_version == "any" def test_find_metadata_files(): names = [ "package/lib/__init__.py", "package/lib/version.py", "package/METADATA.txt", "package/METADATA.json", "package/METADATA", ] expected = [ ["package", "METADATA"], ["package", "METADATA.json"], ["package", "METADATA.txt"], ] candidates = wheel.Wheel.find_candidate_metadata_files(names) assert expected == candidates def test_read_valid(example_wheel): """Parse metadata from a valid wheel file.""" metadata = example_wheel.read().decode().splitlines() assert "Name: twine" in metadata assert "Version: 1.5.0" in metadata def test_read_non_existent_wheel_file_name(): """Raise an exception when wheel file doesn't exist.""" file_name = str(pathlib.Path("/foo/bar/baz.whl").resolve()) with pytest.raises( exceptions.InvalidDistribution, match=re.escape(f"No such file: {file_name}") ): wheel.Wheel(file_name) def test_read_invalid_wheel_extension(): """Raise an exception when file is missing .whl extension.""" file_name = str(pathlib.Path(__file__).parent / "fixtures" / "twine-1.5.0.tar.gz") with pytest.raises( exceptions.InvalidDistribution, match=re.escape(f"Not a known archive format for file: {file_name}"), ): wheel.Wheel(file_name) def test_read_wheel_empty_metadata(tmpdir): """Raise an exception when a wheel file is missing METADATA.""" whl_file = tmpdir.mkdir("wheel").join("not-a-wheel.whl") with zipfile.ZipFile(whl_file, "w") as zip_file: zip_file.writestr("METADATA", "") with pytest.raises( exceptions.InvalidDistribution, match=re.escape(f"No METADATA in archive: {whl_file}"), ): wheel.Wheel(whl_file) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/tox.ini0000644000175100001770000000566514562147542013610 0ustar00runnerdocker[tox] minversion = 3.8 envlist = lint,types,py{38,39,310,311,312},integration,docs isolated_build = True [testenv] deps = pretend pytest pytest-socket build coverage # Needed on 3.12 and newer due to setuptools not being pre-installed # in fresh venvs. # See: https://github.com/python/cpython/issues/95299 setuptools passenv = PYTEST_ADDOPTS commands = python -m coverage run -m pytest {posargs} python -m coverage html python -m coverage report --skip-covered --show-missing --fail-under 97 [testenv:integration] deps = {[testenv]deps} pytest-rerunfailures pytest-services devpi-server devpi pypiserver passenv = PYTEST_ADDOPTS commands = pytest -r aR tests/test_integration.py {posargs} [testenv:docs] deps = -rdocs/requirements.txt allowlist_externals = sh commands = sphinx-build -W --keep-going -b html -d {envtmpdir}/doctrees docs docs/_build/html sphinx-build -W --keep-going -b doctest -d {envtmpdir}/doctrees docs docs/_build/html doc8 docs README.rst --ignore-path docs/_build/html sphinx-build -W --keep-going -b linkcheck -d {envtmpdir}/doctrees docs docs/_build/linkcheck sh -c "python -m twine check --strict $TOX_PACKAGE" [testenv:watch-docs] deps = -rdocs/requirements.txt sphinx-autobuild commands = sphinx-autobuild -b html -d {envtmpdir}/doctrees \ --watch twine \ {posargs:--host 127.0.0.1} \ docs docs/_build/html [testenv:format] skip_install = True deps = isort black commands = isort twine/ tests/ black twine/ tests/ [testenv:lint] skip_install = True deps = {[testenv:format]deps} flake8 flake8-docstrings commands = isort --check-only --diff twine/ tests/ black --check --diff twine/ tests/ flake8 twine/ tests/ [testenv:types] deps = mypy lxml # required for more thorough type declarations keyring >= 22.3 # consider replacing with `mypy --install-types` when # https://github.com/python/mypy/issues/10600 is resolved types-requests commands = mypy --html-report mypy --txt-report mypy {posargs:twine} python -c 'with open("mypy/index.txt") as f: print(f.read())' [testenv:changelog] basepython = python3 deps = towncrier commands = towncrier build {posargs} [testenv:release] # specify Python 3 to use platform's default Python 3 basepython = python3 deps = build passenv = TWINE_PASSWORD TWINE_REPOSITORY setenv = TWINE_USERNAME = {env:TWINE_USERNAME:__token__} commands = python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" python -m build python -m twine upload dist/* [testenv:dev] envdir = {posargs:venv} recreate = True deps = {[testenv]deps} {[testenv:integration]deps} {[testenv:format]deps} {[testenv:lint]deps} {[testenv:types]deps} download = True usedevelop = True commands = python -c 'import sys; print(sys.executable)' python --version ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707659114.0697205 twine-5.0.0/twine/0000755000175100001770000000000014562147552013410 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/__init__.py0000644000175100001770000000246114562147542015523 0ustar00runnerdocker"""Top-level module for Twine. The contents of this package are not a public API. For more details, see https://github.com/pypa/twine/issues/194 and https://github.com/pypa/twine/issues/665. """ # Copyright 2018 Donald Stufft and individual contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. __all__ = ( "__title__", "__summary__", "__uri__", "__version__", "__author__", "__email__", "__license__", "__copyright__", ) __copyright__ = "Copyright 2019 Donald Stufft and individual contributors" import importlib_metadata metadata = importlib_metadata.metadata("twine") __title__ = metadata["name"] __summary__ = metadata["summary"] __uri__ = metadata["home-page"] __version__ = metadata["version"] __author__ = metadata["author"] __email__ = metadata["author-email"] __license__ = None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/__main__.py0000644000175100001770000000305314562147542015502 0ustar00runnerdocker#!/usr/bin/env python3 # Copyright 2013 Donald Stufft # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import http import logging import sys from typing import Any, cast import requests from twine import cli from twine import exceptions logger = logging.getLogger(__name__) def main() -> Any: # Ensure that all errors are logged, even before argparse cli.configure_output() try: error = cli.dispatch(sys.argv[1:]) except requests.HTTPError as exc: # Assuming this response will never be None response = cast(requests.Response, exc.response) error = True status_code = response.status_code status_phrase = http.HTTPStatus(status_code).phrase logger.error( f"{exc.__class__.__name__}: {status_code} {status_phrase} " f"from {response.url}\n" f"{response.reason}" ) except exceptions.TwineException as exc: error = True logger.error(f"{exc.__class__.__name__}: {exc.args[0]}") return error if __name__ == "__main__": sys.exit(main()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/auth.py0000644000175100001770000000727414562147542014734 0ustar00runnerdockerimport functools import getpass import logging from typing import Callable, Optional, Type, cast import keyring from twine import exceptions from twine import utils logger = logging.getLogger(__name__) class CredentialInput: def __init__( self, username: Optional[str] = None, password: Optional[str] = None ) -> None: self.username = username self.password = password class Resolver: def __init__(self, config: utils.RepositoryConfig, input: CredentialInput) -> None: self.config = config self.input = input @classmethod def choose(cls, interactive: bool) -> Type["Resolver"]: return cls if interactive else Private @property @functools.lru_cache() def username(self) -> Optional[str]: if cast(str, self.config["repository"]).startswith( (utils.DEFAULT_REPOSITORY, utils.TEST_REPOSITORY) ): # As of 2024-01-01, PyPI requires API tokens for uploads, meaning # that the username is invariant. return "__token__" return utils.get_userpass_value( self.input.username, self.config, key="username", prompt_strategy=self.username_from_keyring_or_prompt, ) @property @functools.lru_cache() def password(self) -> Optional[str]: return utils.get_userpass_value( self.input.password, self.config, key="password", prompt_strategy=self.password_from_keyring_or_prompt, ) @property def system(self) -> Optional[str]: return self.config["repository"] def get_username_from_keyring(self) -> Optional[str]: try: system = cast(str, self.system) logger.info("Querying keyring for username") creds = keyring.get_credential(system, None) if creds: return cast(str, creds.username) except AttributeError: # To support keyring prior to 15.2 pass except Exception as exc: logger.warning("Error getting username from keyring", exc_info=exc) return None def get_password_from_keyring(self) -> Optional[str]: try: system = cast(str, self.system) username = cast(str, self.username) logger.info("Querying keyring for password") return cast(str, keyring.get_password(system, username)) except Exception as exc: logger.warning("Error getting password from keyring", exc_info=exc) return None def username_from_keyring_or_prompt(self) -> str: username = self.get_username_from_keyring() if username: logger.info("username set from keyring") return username return self.prompt("username", input) def password_from_keyring_or_prompt(self) -> str: password = self.get_password_from_keyring() if password: logger.info("password set from keyring") return password # As of 2024-01-01, PyPI requires API tokens for uploads; # specialize the prompt to clarify that an API token must be provided. if cast(str, self.config["repository"]).startswith( (utils.DEFAULT_REPOSITORY, utils.TEST_REPOSITORY) ): prompt = "API token" else: prompt = "password" return self.prompt(prompt, getpass.getpass) def prompt(self, what: str, how: Callable[..., str]) -> str: return how(f"Enter your {what}: ") class Private(Resolver): def prompt(self, what: str, how: Optional[Callable[..., str]] = None) -> str: raise exceptions.NonInteractive(f"Credential not found for {what}.") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/cli.py0000644000175100001770000000723214562147542014534 0ustar00runnerdocker# Copyright 2013 Donald Stufft # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import logging.config from typing import Any, List, Tuple import importlib_metadata import rich import rich.highlighter import rich.logging import rich.theme import twine args = argparse.Namespace() def configure_output() -> None: # Configure the global Console, available via rich.get_console(). # https://rich.readthedocs.io/en/latest/reference/init.html # https://rich.readthedocs.io/en/latest/console.html rich.reconfigure( # Setting force_terminal makes testing easier by ensuring color codes. This # could be based on FORCE_COLORS or PY_COLORS in os.environ, since Rich # doesn't support that (https://github.com/Textualize/rich/issues/343). force_terminal=True, no_color=getattr(args, "no_color", False), highlight=False, theme=rich.theme.Theme( { "logging.level.debug": "green", "logging.level.info": "blue", "logging.level.warning": "yellow", "logging.level.error": "red", "logging.level.critical": "reverse red", } ), ) # Using dictConfig to override existing loggers, which prevents failures in # test_main.py due to capsys not being cleared. logging.config.dictConfig( { "disable_existing_loggers": False, "version": 1, "handlers": { "console": { "class": "rich.logging.RichHandler", "show_time": False, "show_path": False, "highlighter": rich.highlighter.NullHighlighter(), } }, "root": { "handlers": ["console"], }, } ) def list_dependencies_and_versions() -> List[Tuple[str, str]]: deps = ( "importlib-metadata", "keyring", "pkginfo", "requests", "requests-toolbelt", "urllib3", ) return [(dep, importlib_metadata.version(dep)) for dep in deps] def dep_versions() -> str: return ", ".join( "{}: {}".format(*dependency) for dependency in list_dependencies_and_versions() ) def dispatch(argv: List[str]) -> Any: registered_commands = importlib_metadata.entry_points( group="twine.registered_commands" ) parser = argparse.ArgumentParser(prog="twine") parser.add_argument( "--version", action="version", version=f"%(prog)s version {twine.__version__} ({dep_versions()})", ) parser.add_argument( "--no-color", default=False, required=False, action="store_true", help="disable colored output", ) parser.add_argument( "command", choices=registered_commands.names, ) parser.add_argument( "args", help=argparse.SUPPRESS, nargs=argparse.REMAINDER, ) parser.parse_args(argv, namespace=args) configure_output() main = registered_commands[args.command].load() # type: ignore[no-untyped-call] # python/importlib_metadata#288 # noqa: E501 return main(args.args) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707659114.0697205 twine-5.0.0/twine/commands/0000755000175100001770000000000014562147552015211 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/commands/__init__.py0000644000175100001770000000341314562147542017322 0ustar00runnerdocker"""Module containing the logic for the ``twine`` sub-commands. The contents of this package are not a public API. For more details, see https://github.com/pypa/twine/issues/194 and https://github.com/pypa/twine/issues/665. """ # Copyright 2013 Donald Stufft # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import glob import os.path from typing import List from twine import exceptions __all__: List[str] = [] def _group_wheel_files_first(files: List[str]) -> List[str]: if not any(fname for fname in files if fname.endswith(".whl")): # Return early if there's no wheel files return files files.sort(key=lambda x: -1 if x.endswith(".whl") else 0) return files def _find_dists(dists: List[str]) -> List[str]: uploads = [] for filename in dists: if os.path.exists(filename): uploads.append(filename) continue # The filename didn't exist so it may be a glob files = glob.glob(filename) # If nothing matches, files is [] if not files: raise exceptions.InvalidDistribution( "Cannot find file (or expand pattern): '%s'" % filename ) # Otherwise, files will be filenames that exist uploads.extend(files) return _group_wheel_files_first(uploads) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/commands/check.py0000644000175100001770000001371014562147542016641 0ustar00runnerdocker"""Module containing the logic for ``twine check``.""" # Copyright 2018 Dustin Ingram # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import email.message import io import logging import re from typing import Dict, List, Optional, Tuple, cast import readme_renderer.rst from rich import print from twine import commands from twine import package as package_file logger = logging.getLogger(__name__) _RENDERERS = { None: readme_renderer.rst, # Default if description_content_type is None "text/plain": None, # Rendering cannot fail "text/x-rst": readme_renderer.rst, "text/markdown": None, # Rendering cannot fail } # Regular expression used to capture and reformat docutils warnings into # something that a human can understand. This is loosely borrowed from # Sphinx: https://github.com/sphinx-doc/sphinx/blob # /c35eb6fade7a3b4a6de4183d1dd4196f04a5edaf/sphinx/util/docutils.py#L199 _REPORT_RE = re.compile( r"^:(?P(?:\d+)?): " r"\((?PDEBUG|INFO|WARNING|ERROR|SEVERE)/(\d+)?\) " r"(?P.*)", re.DOTALL | re.MULTILINE, ) class _WarningStream(io.StringIO): def write(self, text: str) -> int: matched = _REPORT_RE.search(text) if matched: line = matched.group("line") level_text = matched.group("level").capitalize() message = matched.group("message").rstrip("\r\n") text = f"line {line}: {level_text}: {message}\n" return super().write(text) def __str__(self) -> str: return self.getvalue().strip() def _parse_content_type(value: str) -> Tuple[str, Dict[str, str]]: """Implement logic of deprecated cgi.parse_header(). From https://docs.python.org/3.11/library/cgi.html#cgi.parse_header. """ msg = email.message.EmailMessage() msg["content-type"] = value return msg.get_content_type(), msg["content-type"].params def _check_file( filename: str, render_warning_stream: _WarningStream ) -> Tuple[List[str], bool]: """Check given distribution.""" warnings = [] is_ok = True package = package_file.PackageFile.from_filename(filename, comment=None) metadata = package.metadata_dictionary() description = cast(Optional[str], metadata["description"]) description_content_type = cast(Optional[str], metadata["description_content_type"]) if description_content_type is None: warnings.append( "`long_description_content_type` missing. defaulting to `text/x-rst`." ) description_content_type = "text/x-rst" content_type, params = _parse_content_type(description_content_type) renderer = _RENDERERS.get(content_type, _RENDERERS[None]) if description is None or description.rstrip() == "UNKNOWN": warnings.append("`long_description` missing.") elif renderer: rendering_result = renderer.render( description, stream=render_warning_stream, **params ) if rendering_result is None: is_ok = False return warnings, is_ok def check( dists: List[str], strict: bool = False, ) -> bool: """Check that a distribution will render correctly on PyPI and display the results. This is currently only validates ``long_description``, but more checks could be added; see https://github.com/pypa/twine/projects/2. :param dists: The distribution files to check. :param output_stream: The destination of the resulting output. :param strict: If ``True``, treat warnings as errors. :return: ``True`` if there are rendering errors, otherwise ``False``. """ uploads = [i for i in commands._find_dists(dists) if not i.endswith(".asc")] if not uploads: # Return early, if there are no files to check. logger.error("No files to check.") return False failure = False for filename in uploads: print(f"Checking {filename}: ", end="") render_warning_stream = _WarningStream() warnings, is_ok = _check_file(filename, render_warning_stream) # Print the status and/or error if not is_ok: failure = True print("[red]FAILED[/red]") logger.error( "`long_description` has syntax errors in markup" " and would not be rendered on PyPI." f"\n{render_warning_stream}" ) elif warnings: if strict: failure = True print("[red]FAILED due to warnings[/red]") else: print("[yellow]PASSED with warnings[/yellow]") else: print("[green]PASSED[/green]") # Print warnings after the status and/or error for message in warnings: logger.warning(message) return failure def main(args: List[str]) -> bool: """Execute the ``check`` command. :param args: The command-line arguments. :return: The exit status of the ``check`` command. """ parser = argparse.ArgumentParser(prog="twine check") parser.add_argument( "dists", nargs="+", metavar="dist", help="The distribution files to check, usually dist/*", ) parser.add_argument( "--strict", action="store_true", default=False, required=False, help="Fail on warnings", ) parsed_args = parser.parse_args(args) # Call the check function with the arguments from the command line return check(parsed_args.dists, strict=parsed_args.strict) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/commands/register.py0000644000175100001770000000553114562147542017412 0ustar00runnerdocker"""Module containing the logic for ``twine register``.""" # Copyright 2015 Ian Cordasco # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import os.path from typing import List, cast from rich import print from twine import exceptions from twine import package as package_file from twine import settings def register(register_settings: settings.Settings, package: str) -> None: """Pre-register a package name with a repository before uploading a distribution. Pre-registration is not supported on PyPI, so the ``register`` command is only necessary if you are using a different repository that requires it. :param register_settings: The configured options relating to repository registration. :param package: The path of the distribution to use for package metadata. :raises twine.exceptions.TwineException: The registration failed due to a configuration error. :raises requests.HTTPError: The repository responded with an error. """ repository_url = cast(str, register_settings.repository_config["repository"]) print(f"Registering package to {repository_url}") repository = register_settings.create_repository() if not os.path.exists(package): raise exceptions.PackageNotFound( f'"{package}" does not exist on the file system.' ) resp = repository.register( package_file.PackageFile.from_filename(package, register_settings.comment) ) repository.close() if resp.is_redirect: raise exceptions.RedirectDetected.from_args( repository_url, resp.headers["location"], ) resp.raise_for_status() def main(args: List[str]) -> None: """Execute the ``register`` command. :param args: The command-line arguments. """ parser = argparse.ArgumentParser( prog="twine register", description="register operation is not required with PyPI.org", ) settings.Settings.register_argparse_arguments(parser) parser.add_argument( "package", metavar="package", help="File from which we read the package metadata.", ) parsed_args = parser.parse_args(args) register_settings = settings.Settings.from_argparse(parsed_args) # Call the register function with the args from the command line register(register_settings, parsed_args.package) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/commands/upload.py0000644000175100001770000002102714562147542017050 0ustar00runnerdocker"""Module containing the logic for ``twine upload``.""" # Copyright 2013 Donald Stufft # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import logging import os.path from typing import Dict, List, cast import requests from rich import print from twine import commands from twine import exceptions from twine import package as package_file from twine import settings from twine import utils logger = logging.getLogger(__name__) def skip_upload( response: requests.Response, skip_existing: bool, package: package_file.PackageFile ) -> bool: """Determine if a failed upload is an error or can be safely ignored. :param response: The response from attempting to upload ``package`` to a repository. :param skip_existing: If ``True``, use the status and content of ``response`` to determine if the package already exists on the repository. If so, then a failed upload is safe to ignore. :param package: The package that was being uploaded. :return: ``True`` if a failed upload can be safely ignored, otherwise ``False``. """ if not skip_existing: return False status = response.status_code reason = getattr(response, "reason", "").lower() text = getattr(response, "text", "").lower() # NOTE(sigmavirus24): PyPI presently returns a 400 status code with the # error message in the reason attribute. Other implementations return a # 403 or 409 status code. return ( # pypiserver (https://pypi.org/project/pypiserver) status == 409 # PyPI / TestPyPI / GCP Artifact Registry or (status == 400 and any("already exist" in x for x in [reason, text])) # Nexus Repository OSS (https://www.sonatype.com/nexus-repository-oss) or (status == 400 and any("updating asset" in x for x in [reason, text])) # Artifactory (https://jfrog.com/artifactory/) or (status == 403 and "overwrite artifact" in text) # Gitlab Enterprise Edition (https://about.gitlab.com) or (status == 400 and "already been taken" in text) ) def _make_package( filename: str, signatures: Dict[str, str], upload_settings: settings.Settings ) -> package_file.PackageFile: """Create and sign a package, based off of filename, signatures and settings.""" package = package_file.PackageFile.from_filename(filename, upload_settings.comment) signed_name = package.signed_basefilename if signed_name in signatures: package.add_gpg_signature(signatures[signed_name], signed_name) elif upload_settings.sign: package.sign(upload_settings.sign_with, upload_settings.identity) file_size = utils.get_file_size(package.filename) logger.info(f"{package.filename} ({file_size})") if package.gpg_signature: logger.info(f"Signed with {package.signed_filename}") return package def upload(upload_settings: settings.Settings, dists: List[str]) -> None: """Upload one or more distributions to a repository, and display the progress. If a package already exists on the repository, most repositories will return an error response. However, if ``upload_settings.skip_existing`` is ``True``, a message will be displayed and any remaining distributions will be uploaded. For known repositories (like PyPI), the web URLs of successfully uploaded packages will be displayed. :param upload_settings: The configured options related to uploading to a repository. :param dists: The distribution files to upload to the repository. This can also include ``.asc`` files; the GPG signatures will be added to the corresponding uploads. :raises twine.exceptions.TwineException: The upload failed due to a configuration error. :raises requests.HTTPError: The repository responded with an error. """ dists = commands._find_dists(dists) # Determine if the user has passed in pre-signed distributions signatures = {os.path.basename(d): d for d in dists if d.endswith(".asc")} uploads = [i for i in dists if not i.endswith(".asc")] upload_settings.check_repository_url() repository_url = cast(str, upload_settings.repository_config["repository"]) print(f"Uploading distributions to {repository_url}") packages_to_upload = [ _make_package(filename, signatures, upload_settings) for filename in uploads ] if any(p.gpg_signature for p in packages_to_upload): if repository_url.startswith((utils.DEFAULT_REPOSITORY, utils.TEST_REPOSITORY)): # Warn the user if they're trying to upload a PGP signature to PyPI # or TestPyPI, which will (as of May 2023) ignore it. # This warning is currently limited to just those indices, since other # indices may still support PGP signatures. logger.warning( "One or more packages has an associated PGP signature; " "these will be silently ignored by the index" ) else: # On other indices, warn the user that twine is considering # removing PGP support outright. logger.warning( "One or more packages has an associated PGP signature; " "a future version of twine may silently ignore these. " "See https://github.com/pypa/twine/issues/1009 for more " "information" ) repository = upload_settings.create_repository() uploaded_packages = [] if signatures and not packages_to_upload: raise exceptions.InvalidDistribution( "Cannot upload signed files by themselves, must upload with a " "corresponding distribution file." ) for package in packages_to_upload: skip_message = ( f"Skipping {package.basefilename} because it appears to already exist" ) # Note: The skip_existing check *needs* to be first, because otherwise # we're going to generate extra HTTP requests against a hardcoded # URL for no reason. if upload_settings.skip_existing and repository.package_is_uploaded(package): logger.warning(skip_message) continue resp = repository.upload(package) logger.info(f"Response from {resp.url}:\n{resp.status_code} {resp.reason}") if resp.text: logger.info(resp.text) # Bug 92. If we get a redirect we should abort because something seems # funky. The behaviour is not well defined and redirects being issued # by PyPI should never happen in reality. This should catch malicious # redirects as well. if resp.is_redirect: raise exceptions.RedirectDetected.from_args( repository_url, resp.headers["location"], ) if skip_upload(resp, upload_settings.skip_existing, package): logger.warning(skip_message) continue utils.check_status_code(resp, upload_settings.verbose) uploaded_packages.append(package) release_urls = repository.release_urls(uploaded_packages) if release_urls: print("\n[green]View at:") for url in release_urls: print(url) # Bug 28. Try to silence a ResourceWarning by clearing the connection # pool. repository.close() def main(args: List[str]) -> None: """Execute the ``upload`` command. :param args: The command-line arguments. """ parser = argparse.ArgumentParser(prog="twine upload") settings.Settings.register_argparse_arguments(parser) parser.add_argument( "dists", nargs="+", metavar="dist", help="The distribution files to upload to the repository " "(package index). Usually dist/* . May additionally contain " "a .asc file to include an existing signature with the " "file upload.", ) parsed_args = parser.parse_args(args) upload_settings = settings.Settings.from_argparse(parsed_args) # Call the upload function with the arguments from the command line return upload(upload_settings, parsed_args.dists) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/exceptions.py0000644000175100001770000000734714562147542016155 0ustar00runnerdocker"""Module containing exceptions raised by twine.""" # Copyright 2015 Ian Stapleton Cordasco # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. class TwineException(Exception): """Base class for all exceptions raised by twine.""" pass class RedirectDetected(TwineException): """A redirect was detected that the user needs to resolve. In some cases, requests refuses to issue a new POST request after a redirect. In order to prevent a confusing user experience, we raise this exception to allow users to know the index they're uploading to is redirecting them. """ @classmethod def from_args(cls, repository_url: str, redirect_url: str) -> "RedirectDetected": if redirect_url == f"{repository_url}/": return cls( f"{repository_url} attempted to redirect to {redirect_url}.\n" f"Your repository URL is missing a trailing slash. " "Please add it and try again.", ) return cls( f"{repository_url} attempted to redirect to {redirect_url}.\n" f"If you trust these URLs, set {redirect_url} as your repository URL " "and try again.", ) class PackageNotFound(TwineException): """A package file was provided that could not be found on the file system. This is only used when attempting to register a package_file. """ pass class UploadToDeprecatedPyPIDetected(TwineException): """An upload attempt was detected to deprecated PyPI domains. The sites pypi.python.org and testpypi.python.org are deprecated. """ @classmethod def from_args( cls, target_url: str, default_url: str, test_url: str ) -> "UploadToDeprecatedPyPIDetected": """Return an UploadToDeprecatedPyPIDetected instance.""" return cls( "You're trying to upload to the legacy PyPI site '{}'. " "Uploading to those sites is deprecated. \n " "The new sites are pypi.org and test.pypi.org. Try using " "{} (or {}) to upload your packages instead. " "These are the default URLs for Twine now. \n More at " "https://packaging.python.org/guides/migrating-to-pypi-org/" " .".format(target_url, default_url, test_url) ) class UnreachableRepositoryURLDetected(TwineException): """An upload attempt was detected to a URL without a protocol prefix. All repository URLs must have a protocol (e.g., ``https://``). """ pass class InvalidSigningConfiguration(TwineException): """Both the sign and identity parameters must be present.""" pass class InvalidSigningExecutable(TwineException): """Signing executable must be installed on system.""" pass class InvalidConfiguration(TwineException): """Raised when configuration is invalid.""" pass class InvalidDistribution(TwineException): """Raised when a distribution is invalid.""" pass class NonInteractive(TwineException): """Raised in non-interactive mode when credentials could not be found.""" pass class InvalidPyPIUploadURL(TwineException): """Repository configuration tries to use PyPI with an incorrect URL. For example, https://pypi.org instead of https://upload.pypi.org/legacy. """ pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/package.py0000644000175100001770000002541214562147542015360 0ustar00runnerdocker# Copyright 2015 Ian Cordasco # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import hashlib import io import logging import os import re import subprocess from typing import Dict, NamedTuple, Optional, Sequence, Tuple, Union, cast import importlib_metadata import pkginfo from rich import print from twine import exceptions from twine import wheel from twine import wininst DIST_TYPES = { "bdist_wheel": wheel.Wheel, "bdist_wininst": wininst.WinInst, "bdist_egg": pkginfo.BDist, "sdist": pkginfo.SDist, } DIST_EXTENSIONS = { ".whl": "bdist_wheel", ".exe": "bdist_wininst", ".egg": "bdist_egg", ".tar.bz2": "sdist", ".tar.gz": "sdist", ".zip": "sdist", } MetadataValue = Union[Optional[str], Sequence[str], Tuple[str, bytes]] logger = logging.getLogger(__name__) def _safe_name(name: str) -> str: """Convert an arbitrary string to a standard distribution name. Any runs of non-alphanumeric/. characters are replaced with a single '-'. Copied from pkg_resources.safe_name for compatibility with warehouse. See https://github.com/pypa/twine/issues/743. """ return re.sub("[^A-Za-z0-9.]+", "-", name) class PackageFile: def __init__( self, filename: str, comment: Optional[str], metadata: pkginfo.Distribution, python_version: Optional[str], filetype: Optional[str], ) -> None: self.filename = filename self.basefilename = os.path.basename(filename) self.comment = comment self.metadata = metadata self.python_version = python_version self.filetype = filetype self.safe_name = _safe_name(metadata.name) self.signed_filename = self.filename + ".asc" self.signed_basefilename = self.basefilename + ".asc" self.gpg_signature: Optional[Tuple[str, bytes]] = None hasher = HashManager(filename) hasher.hash() hexdigest = hasher.hexdigest() self.md5_digest = hexdigest.md5 self.sha2_digest = hexdigest.sha2 self.blake2_256_digest = hexdigest.blake2 @classmethod def from_filename(cls, filename: str, comment: Optional[str]) -> "PackageFile": # Extract the metadata from the package for ext, dtype in DIST_EXTENSIONS.items(): if filename.endswith(ext): try: meta = DIST_TYPES[dtype](filename) except EOFError: raise exceptions.InvalidDistribution( "Invalid distribution file: '%s'" % os.path.basename(filename) ) else: break else: raise exceptions.InvalidDistribution( "Unknown distribution format: '%s'" % os.path.basename(filename) ) # If pkginfo encounters a metadata version it doesn't support, it may give us # back empty metadata. At the very least, we should have a name and version, # which could also be empty if, for example, a MANIFEST.in doesn't include # setup.cfg. missing_fields = [ f.capitalize() for f in ["name", "version"] if not getattr(meta, f) ] if missing_fields: supported_metadata = list(pkginfo.distribution.HEADER_ATTRS) raise exceptions.InvalidDistribution( "Metadata is missing required fields: " f"{', '.join(missing_fields)}.\n" "Make sure the distribution includes the files where those fields " "are specified, and is using a supported Metadata-Version: " f"{', '.join(supported_metadata)}." ) py_version: Optional[str] if dtype == "bdist_egg": (dist,) = importlib_metadata.Distribution.discover(path=[filename]) py_version = dist.metadata["Version"] elif dtype == "bdist_wheel": py_version = cast(wheel.Wheel, meta).py_version elif dtype == "bdist_wininst": py_version = cast(wininst.WinInst, meta).py_version else: py_version = None return cls(filename, comment, meta, py_version, dtype) def metadata_dictionary(self) -> Dict[str, MetadataValue]: """Merge multiple sources of metadata into a single dictionary. Includes values from filename, PKG-INFO, hashers, and signature. """ meta = self.metadata data: Dict[str, MetadataValue] = { # identify release "name": self.safe_name, "version": meta.version, # file content "filetype": self.filetype, "pyversion": self.python_version, # additional meta-data "metadata_version": meta.metadata_version, "summary": meta.summary, "home_page": meta.home_page, "author": meta.author, "author_email": meta.author_email, "maintainer": meta.maintainer, "maintainer_email": meta.maintainer_email, "license": meta.license, "description": meta.description, "keywords": meta.keywords, "platform": meta.platforms, "classifiers": meta.classifiers, "download_url": meta.download_url, "supported_platform": meta.supported_platforms, "comment": self.comment, "sha256_digest": self.sha2_digest, # PEP 314 "provides": meta.provides, "requires": meta.requires, "obsoletes": meta.obsoletes, # Metadata 1.2 "project_urls": meta.project_urls, "provides_dist": meta.provides_dist, "obsoletes_dist": meta.obsoletes_dist, "requires_dist": meta.requires_dist, "requires_external": meta.requires_external, "requires_python": meta.requires_python, # Metadata 2.1 "provides_extras": meta.provides_extras, "description_content_type": meta.description_content_type, # Metadata 2.2 "dynamic": meta.dynamic, } if self.gpg_signature is not None: data["gpg_signature"] = self.gpg_signature # FIPS disables MD5 and Blake2, making the digest values None. Some package # repositories don't allow null values, so this only sends non-null values. # See also: https://github.com/pypa/twine/issues/775 if self.md5_digest: data["md5_digest"] = self.md5_digest if self.blake2_256_digest: data["blake2_256_digest"] = self.blake2_256_digest return data def add_gpg_signature( self, signature_filepath: str, signature_filename: str ) -> None: if self.gpg_signature is not None: raise exceptions.InvalidDistribution("GPG Signature can only be added once") with open(signature_filepath, "rb") as gpg: self.gpg_signature = (signature_filename, gpg.read()) def sign(self, sign_with: str, identity: Optional[str]) -> None: print(f"Signing {self.basefilename}") gpg_args: Tuple[str, ...] = (sign_with, "--detach-sign") if identity: gpg_args += ("--local-user", identity) gpg_args += ("-a", self.filename) self.run_gpg(gpg_args) self.add_gpg_signature(self.signed_filename, self.signed_basefilename) @classmethod def run_gpg(cls, gpg_args: Tuple[str, ...]) -> None: try: subprocess.check_call(gpg_args) return except FileNotFoundError: if gpg_args[0] != "gpg": raise exceptions.InvalidSigningExecutable( f"{gpg_args[0]} executable not available." ) logger.warning("gpg executable not available. Attempting fallback to gpg2.") try: subprocess.check_call(("gpg2",) + gpg_args[1:]) except FileNotFoundError: raise exceptions.InvalidSigningExecutable( "'gpg' or 'gpg2' executables not available.\n" "Try installing one of these or specifying an executable " "with the --sign-with flag." ) class Hexdigest(NamedTuple): md5: Optional[str] sha2: Optional[str] blake2: Optional[str] class HashManager: """Manage our hashing objects for simplicity. This will also allow us to better test this logic. """ def __init__(self, filename: str) -> None: """Initialize our manager and hasher objects.""" self.filename = filename self._md5_hasher = None try: self._md5_hasher = hashlib.md5() except ValueError: # FIPs mode disables MD5 pass self._sha2_hasher = hashlib.sha256() self._blake_hasher = None try: self._blake_hasher = hashlib.blake2b(digest_size=256 // 8) except (ValueError, TypeError): # FIPS mode disables blake2 pass def _md5_update(self, content: bytes) -> None: if self._md5_hasher is not None: self._md5_hasher.update(content) def _md5_hexdigest(self) -> Optional[str]: if self._md5_hasher is not None: return self._md5_hasher.hexdigest() return None def _sha2_update(self, content: bytes) -> None: if self._sha2_hasher is not None: self._sha2_hasher.update(content) def _sha2_hexdigest(self) -> Optional[str]: if self._sha2_hasher is not None: return self._sha2_hasher.hexdigest() return None def _blake_update(self, content: bytes) -> None: if self._blake_hasher is not None: self._blake_hasher.update(content) def _blake_hexdigest(self) -> Optional[str]: if self._blake_hasher is not None: return self._blake_hasher.hexdigest() return None def hash(self) -> None: """Hash the file contents.""" with open(self.filename, "rb") as fp: for content in iter(lambda: fp.read(io.DEFAULT_BUFFER_SIZE), b""): self._md5_update(content) self._sha2_update(content) self._blake_update(content) def hexdigest(self) -> Hexdigest: """Return the hexdigest for the file.""" return Hexdigest( self._md5_hexdigest(), self._sha2_hexdigest(), self._blake_hexdigest(), ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/py.typed0000644000175100001770000000000014562147542015074 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/repository.py0000644000175100001770000002101114562147542016173 0ustar00runnerdocker# Copyright 2015 Ian Cordasco # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging from typing import Any, Dict, List, Optional, Set, Tuple, cast import requests import requests_toolbelt import rich.progress import urllib3 from requests import adapters from requests_toolbelt.utils import user_agent from rich import print import twine from twine import package as package_file KEYWORDS_TO_NOT_FLATTEN = {"gpg_signature", "content"} LEGACY_PYPI = "https://pypi.python.org/" LEGACY_TEST_PYPI = "https://testpypi.python.org/" WAREHOUSE = "https://upload.pypi.org/" OLD_WAREHOUSE = "https://upload.pypi.io/" TEST_WAREHOUSE = "https://test.pypi.org/" WAREHOUSE_WEB = "https://pypi.org/" logger = logging.getLogger(__name__) class Repository: def __init__( self, repository_url: str, username: Optional[str], password: Optional[str], disable_progress_bar: bool = False, ) -> None: self.url = repository_url self.session = requests.session() # requests.Session.auth should be Union[None, Tuple[str, str], ...] # But username or password could be None # See TODO for utils.RepositoryConfig self.session.auth = ( (username or "", password or "") if username or password else None ) logger.info(f"username: {username if username else ''}") logger.info(f"password: <{'hidden' if password else 'empty'}>") self.session.headers["User-Agent"] = self._make_user_agent_string() for scheme in ("http://", "https://"): self.session.mount(scheme, self._make_adapter_with_retries()) # Working around https://github.com/python/typing/issues/182 self._releases_json_data: Dict[str, Dict[str, Any]] = {} self.disable_progress_bar = disable_progress_bar @staticmethod def _make_adapter_with_retries() -> adapters.HTTPAdapter: retry = urllib3.Retry( allowed_methods=["GET"], connect=5, total=10, status_forcelist=[500, 501, 502, 503], ) return adapters.HTTPAdapter(max_retries=retry) @staticmethod def _make_user_agent_string() -> str: user_agent_string = ( user_agent.UserAgentBuilder("twine", twine.__version__) .include_implementation() .build() ) return cast(str, user_agent_string) def close(self) -> None: self.session.close() @staticmethod def _convert_data_to_list_of_tuples(data: Dict[str, Any]) -> List[Tuple[str, Any]]: data_to_send = [] for key, value in data.items(): if key in KEYWORDS_TO_NOT_FLATTEN or not isinstance(value, (list, tuple)): data_to_send.append((key, value)) else: for item in value: data_to_send.append((key, item)) return data_to_send def set_certificate_authority(self, cacert: Optional[str]) -> None: if cacert: self.session.verify = cacert def set_client_certificate(self, clientcert: Optional[str]) -> None: if clientcert: self.session.cert = clientcert def register(self, package: package_file.PackageFile) -> requests.Response: data = package.metadata_dictionary() data.update({":action": "submit", "protocol_version": "1"}) print(f"Registering {package.basefilename}") data_to_send = self._convert_data_to_list_of_tuples(data) encoder = requests_toolbelt.MultipartEncoder(data_to_send) resp = self.session.post( self.url, data=encoder, allow_redirects=False, headers={"Content-Type": encoder.content_type}, ) # Bug 28. Try to silence a ResourceWarning by releasing the socket. resp.close() return resp def _upload(self, package: package_file.PackageFile) -> requests.Response: data = package.metadata_dictionary() data.update( { # action ":action": "file_upload", "protocol_version": "1", } ) data_to_send = self._convert_data_to_list_of_tuples(data) print(f"Uploading {package.basefilename}") with open(package.filename, "rb") as fp: data_to_send.append( ("content", (package.basefilename, fp, "application/octet-stream")) ) encoder = requests_toolbelt.MultipartEncoder(data_to_send) with rich.progress.Progress( "[progress.percentage]{task.percentage:>3.0f}%", rich.progress.BarColumn(), rich.progress.DownloadColumn(), "•", rich.progress.TimeRemainingColumn( compact=True, elapsed_when_finished=True, ), "•", rich.progress.TransferSpeedColumn(), disable=self.disable_progress_bar, ) as progress: task_id = progress.add_task("", total=encoder.len) monitor = requests_toolbelt.MultipartEncoderMonitor( encoder, lambda monitor: progress.update( task_id, completed=monitor.bytes_read, ), ) resp = self.session.post( self.url, data=monitor, allow_redirects=False, headers={"Content-Type": monitor.content_type}, ) return resp def upload( self, package: package_file.PackageFile, max_redirects: int = 5 ) -> requests.Response: number_of_redirects = 0 while number_of_redirects < max_redirects: resp = self._upload(package) if resp.status_code == requests.codes.OK: return resp if 500 <= resp.status_code < 600: number_of_redirects += 1 logger.warning( f'Received "{resp.status_code}: {resp.reason}"' "\nPackage upload appears to have failed." f" Retry {number_of_redirects} of {max_redirects}." ) else: return resp return resp def package_is_uploaded( self, package: package_file.PackageFile, bypass_cache: bool = False ) -> bool: # NOTE(sigmavirus24): Not all indices are PyPI and pypi.io doesn't # have a similar interface for finding the package versions. if not self.url.startswith((LEGACY_PYPI, WAREHOUSE, OLD_WAREHOUSE)): return False safe_name = package.safe_name releases = None if not bypass_cache: releases = self._releases_json_data.get(safe_name) if releases is None: url = f"{LEGACY_PYPI}pypi/{safe_name}/json" headers = {"Accept": "application/json"} response = self.session.get(url, headers=headers) if response.status_code == 200: releases = response.json()["releases"] else: releases = {} self._releases_json_data[safe_name] = releases packages = releases.get(package.metadata.version, []) for uploaded_package in packages: if uploaded_package["filename"] == package.basefilename: return True return False def release_urls(self, packages: List[package_file.PackageFile]) -> Set[str]: if self.url.startswith(WAREHOUSE): url = WAREHOUSE_WEB elif self.url.startswith(TEST_WAREHOUSE): url = TEST_WAREHOUSE else: return set() return { f"{url}project/{package.safe_name}/{package.metadata.version}/" for package in packages } def verify_package_integrity(self, package: package_file.PackageFile) -> None: # TODO(sigmavirus24): Add a way for users to download the package and # check it's hash against what it has locally. pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/settings.py0000644000175100001770000002724214562147542015630 0ustar00runnerdocker"""Module containing logic for handling settings.""" # Copyright 2018 Ian Stapleton Cordasco # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import contextlib import logging from typing import Any, Optional, cast from twine import auth from twine import exceptions from twine import repository from twine import utils class Settings: """Object that manages the configuration for Twine. This object can only be instantiated with keyword arguments. For example, .. code-block:: python Settings(True, username='fakeusername') Will raise a :class:`TypeError`. Instead, you would want .. code-block:: python Settings(sign=True, username='fakeusername') """ def __init__( self, *, sign: bool = False, sign_with: str = "gpg", identity: Optional[str] = None, username: Optional[str] = None, password: Optional[str] = None, non_interactive: bool = False, comment: Optional[str] = None, config_file: str = utils.DEFAULT_CONFIG_FILE, skip_existing: bool = False, cacert: Optional[str] = None, client_cert: Optional[str] = None, repository_name: str = "pypi", repository_url: Optional[str] = None, verbose: bool = False, disable_progress_bar: bool = False, **ignored_kwargs: Any, ) -> None: """Initialize our settings instance. :param sign: Configure whether the package file should be signed. :param sign_with: The name of the executable used to sign the package with. :param identity: The GPG identity that should be used to sign the package file. :param username: The username used to authenticate to the repository (package index). :param password: The password used to authenticate to the repository (package index). :param non_interactive: Do not interactively prompt for username/password if the required credentials are missing. :param comment: The comment to include with each distribution file. :param config_file: The path to the configuration file to use. :param skip_existing: Specify whether twine should continue uploading files if one of them already exists. This primarily supports PyPI. Other package indexes may not be supported. :param cacert: The path to the bundle of certificates used to verify the TLS connection to the package index. :param client_cert: The path to the client certificate used to perform authentication to the index. This must be a single file that contains both the private key and the PEM-encoded certificate. :param repository_name: The name of the repository (package index) to interact with. This should correspond to a section in the config file. :param repository_url: The URL of the repository (package index) to interact with. This will override the settings inferred from ``repository_name``. :param verbose: Show verbose output. :param disable_progress_bar: Disable the progress bar. """ self.config_file = config_file self.comment = comment self.verbose = verbose self.disable_progress_bar = disable_progress_bar self.skip_existing = skip_existing self._handle_repository_options( repository_name=repository_name, repository_url=repository_url, ) self._handle_package_signing( sign=sign, sign_with=sign_with, identity=identity, ) # _handle_certificates relies on the parsed repository config self._handle_certificates(cacert, client_cert) self.auth = auth.Resolver.choose(not non_interactive)( self.repository_config, auth.CredentialInput(username, password), ) @property def username(self) -> Optional[str]: return self.auth.username @property def password(self) -> Optional[str]: with self._allow_noninteractive(): return self.auth.password def _allow_noninteractive(self) -> "contextlib.AbstractContextManager[None]": """Bypass NonInteractive error when client cert is present.""" suppressed = (exceptions.NonInteractive,) if self.client_cert else () return contextlib.suppress(*suppressed) @property def verbose(self) -> bool: return self._verbose @verbose.setter def verbose(self, verbose: bool) -> None: """Initialize a logger based on the --verbose option.""" self._verbose = verbose twine_logger = logging.getLogger("twine") twine_logger.setLevel(logging.INFO if verbose else logging.WARNING) @staticmethod def register_argparse_arguments(parser: argparse.ArgumentParser) -> None: """Register the arguments for argparse.""" parser.add_argument( "-r", "--repository", action=utils.EnvironmentDefault, env="TWINE_REPOSITORY", default="pypi", help="The repository (package index) to upload the package to. " "Should be a section in the config file (default: " "%(default)s). (Can also be set via %(env)s environment " "variable.)", ) parser.add_argument( "--repository-url", action=utils.EnvironmentDefault, env="TWINE_REPOSITORY_URL", default=None, required=False, help="The repository (package index) URL to upload the package to." " This overrides --repository. " "(Can also be set via %(env)s environment variable.)", ) parser.add_argument( "-s", "--sign", action="store_true", default=False, help="Sign files to upload using GPG.", ) parser.add_argument( "--sign-with", default="gpg", help="GPG program used to sign uploads (default: %(default)s).", ) parser.add_argument( "-i", "--identity", help="GPG identity used to sign files.", ) parser.add_argument( "-u", "--username", action=utils.EnvironmentDefault, env="TWINE_USERNAME", required=False, help="The username to authenticate to the repository " "(package index) as. (Can also be set via " "%(env)s environment variable.)", ) parser.add_argument( "-p", "--password", action=utils.EnvironmentDefault, env="TWINE_PASSWORD", required=False, help="The password to authenticate to the repository " "(package index) with. (Can also be set via " "%(env)s environment variable.)", ) parser.add_argument( "--non-interactive", action=utils.EnvironmentFlag, env="TWINE_NON_INTERACTIVE", help="Do not interactively prompt for username/password if the " "required credentials are missing. (Can also be set via " "%(env)s environment variable.)", ) parser.add_argument( "-c", "--comment", help="The comment to include with the distribution file.", ) parser.add_argument( "--config-file", default=utils.DEFAULT_CONFIG_FILE, help="The .pypirc config file to use.", ) parser.add_argument( "--skip-existing", default=False, action="store_true", help="Continue uploading files if one already exists. (Only valid " "when uploading to PyPI. Other implementations may not " "support this.)", ) parser.add_argument( "--cert", action=utils.EnvironmentDefault, env="TWINE_CERT", default=None, required=False, metavar="path", help="Path to alternate CA bundle (can also be set via %(env)s " "environment variable).", ) parser.add_argument( "--client-cert", metavar="path", help="Path to SSL client certificate, a single file containing the" " private key and the certificate in PEM format.", ) parser.add_argument( "--verbose", default=False, required=False, action="store_true", help="Show verbose output.", ) parser.add_argument( "--disable-progress-bar", default=False, required=False, action="store_true", help="Disable the progress bar.", ) @classmethod def from_argparse(cls, args: argparse.Namespace) -> "Settings": """Generate the Settings from parsed arguments.""" settings = vars(args) settings["repository_name"] = settings.pop("repository") settings["cacert"] = settings.pop("cert") return cls(**settings) def _handle_package_signing( self, sign: bool, sign_with: str, identity: Optional[str] ) -> None: if not sign and identity: raise exceptions.InvalidSigningConfiguration( "sign must be given along with identity" ) self.sign = sign self.sign_with = sign_with self.identity = identity def _handle_repository_options( self, repository_name: str, repository_url: Optional[str] ) -> None: self.repository_config = utils.get_repository_from_config( self.config_file, repository_name, repository_url, ) def _handle_certificates( self, cacert: Optional[str], client_cert: Optional[str] ) -> None: self.cacert = utils.get_cacert(cacert, self.repository_config) self.client_cert = utils.get_clientcert(client_cert, self.repository_config) def check_repository_url(self) -> None: """Verify we are not using legacy PyPI. :raises twine.exceptions.UploadToDeprecatedPyPIDetected: The configured repository URL is for legacy PyPI. """ repository_url = cast(str, self.repository_config["repository"]) if repository_url.startswith( (repository.LEGACY_PYPI, repository.LEGACY_TEST_PYPI) ): raise exceptions.UploadToDeprecatedPyPIDetected.from_args( repository_url, utils.DEFAULT_REPOSITORY, utils.TEST_REPOSITORY ) def create_repository(self) -> repository.Repository: """Create a new repository for uploading.""" repo = repository.Repository( cast(str, self.repository_config["repository"]), self.username, self.password, self.disable_progress_bar, ) repo.set_certificate_authority(self.cacert) repo.set_client_certificate(self.client_cert) return repo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/utils.py0000644000175100001770000002534314562147542015130 0ustar00runnerdocker# Copyright 2013 Donald Stufft # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import collections import configparser import functools import logging import os import os.path import unicodedata from typing import Any, Callable, DefaultDict, Dict, Optional, Sequence, Union, cast from urllib.parse import urlparse from urllib.parse import urlunparse import requests import rfc3986 from twine import exceptions # Shim for input to allow testing. input_func = input DEFAULT_REPOSITORY = "https://upload.pypi.org/legacy/" TEST_REPOSITORY = "https://test.pypi.org/legacy/" DEFAULT_CONFIG_FILE = "~/.pypirc" # TODO: In general, it seems to be assumed that the values retrieved from # instances of this type aren't None, except for username and password. # Type annotations would be cleaner if this were Dict[str, str], but that # requires reworking the username/password handling, probably starting with # get_userpass_value. RepositoryConfig = Dict[str, Optional[str]] logger = logging.getLogger(__name__) def get_config(path: str) -> Dict[str, RepositoryConfig]: """Read repository configuration from a file (i.e. ~/.pypirc). Format: https://packaging.python.org/specifications/pypirc/ If the default config file doesn't exist, return a default configuration for pypyi and testpypi. """ realpath = os.path.realpath(os.path.expanduser(path)) parser = configparser.RawConfigParser() try: with open(realpath) as f: parser.read_file(f) logger.info(f"Using configuration from {realpath}") except FileNotFoundError: # User probably set --config-file, but the file can't be read if path != DEFAULT_CONFIG_FILE: raise # server-login is obsolete, but retained for backwards compatibility defaults: RepositoryConfig = { "username": parser.get("server-login", "username", fallback=None), "password": parser.get("server-login", "password", fallback=None), } config: DefaultDict[str, RepositoryConfig] config = collections.defaultdict(lambda: defaults.copy()) index_servers = parser.get( "distutils", "index-servers", fallback="pypi testpypi" ).split() # Don't require users to manually configure URLs for these repositories config["pypi"]["repository"] = DEFAULT_REPOSITORY if "testpypi" in index_servers: config["testpypi"]["repository"] = TEST_REPOSITORY # Optional configuration values for individual repositories for repository in index_servers: for key in [ "username", "repository", "password", "ca_cert", "client_cert", ]: if parser.has_option(repository, key): config[repository][key] = parser.get(repository, key) # Convert the defaultdict to a regular dict to prevent surprising behavior later on return dict(config) def _validate_repository_url(repository_url: str) -> None: """Validate the given url for allowed schemes and components.""" # Allowed schemes are http and https, based on whether the repository # supports TLS or not, and scheme and host must be present in the URL validator = ( rfc3986.validators.Validator() .allow_schemes("http", "https") .require_presence_of("scheme", "host") ) try: validator.validate(rfc3986.uri_reference(repository_url)) except rfc3986.exceptions.RFC3986Exception as exc: raise exceptions.UnreachableRepositoryURLDetected( f"Invalid repository URL: {exc.args[0]}." ) def get_repository_from_config( config_file: str, repository: str, repository_url: Optional[str] = None, ) -> RepositoryConfig: """Get repository config command-line values or the .pypirc file.""" # Prefer CLI `repository_url` over `repository` or .pypirc if repository_url: _validate_repository_url(repository_url) return { "repository": repository_url, "username": None, "password": None, } try: config = get_config(config_file)[repository] except OSError as exc: raise exceptions.InvalidConfiguration(str(exc)) except KeyError: raise exceptions.InvalidConfiguration( f"Missing '{repository}' section from {config_file}.\n" f"More info: https://packaging.python.org/specifications/pypirc/ " ) config["repository"] = normalize_repository_url(cast(str, config["repository"])) return config _HOSTNAMES = { "pypi.python.org", "testpypi.python.org", "upload.pypi.org", "test.pypi.org", } def normalize_repository_url(url: str) -> str: parsed = urlparse(url) if parsed.netloc in _HOSTNAMES: return urlunparse(("https",) + parsed[1:]) return urlunparse(parsed) def get_file_size(filename: str) -> str: """Return the size of a file in KB, or MB if >= 1024 KB.""" file_size = os.path.getsize(filename) / 1024 size_unit = "KB" if file_size > 1024: file_size = file_size / 1024 size_unit = "MB" return f"{file_size:.1f} {size_unit}" def check_status_code(response: requests.Response, verbose: bool) -> None: """Generate a helpful message based on the response from the repository. Raise a custom exception for recognized errors. Otherwise, print the response content (based on the verbose option) before re-raising the HTTPError. """ if response.status_code == 410 and "pypi.python.org" in response.url: raise exceptions.UploadToDeprecatedPyPIDetected( f"It appears you're uploading to pypi.python.org (or " f"testpypi.python.org). You've received a 410 error response. " f"Uploading to those sites is deprecated. The new sites are " f"pypi.org and test.pypi.org. Try using {DEFAULT_REPOSITORY} (or " f"{TEST_REPOSITORY}) to upload your packages instead. These are " f"the default URLs for Twine now. More at " f"https://packaging.python.org/guides/migrating-to-pypi-org/." ) elif response.status_code == 405 and "pypi.org" in response.url: raise exceptions.InvalidPyPIUploadURL( f"It appears you're trying to upload to pypi.org but have an " f"invalid URL. You probably want one of these two URLs: " f"{DEFAULT_REPOSITORY} or {TEST_REPOSITORY}. Check your " f"--repository-url value." ) try: response.raise_for_status() except requests.HTTPError as err: if not verbose: logger.warning( "Error during upload. " "Retry with the --verbose option for more details." ) raise err def get_userpass_value( cli_value: Optional[str], config: RepositoryConfig, key: str, prompt_strategy: Optional[Callable[[], str]] = None, ) -> Optional[str]: """Get a credential (e.g. a username or password) from the configuration. Uses the following rules: 1. If ``cli_value`` is specified, use that. 2. If ``config[key]`` is specified, use that. 3. If ``prompt_strategy`` is specified, use its return value. 4. Otherwise return ``None`` :param cli_value: The value supplied from the command line. :param config: A dictionary of repository configuration values. :param key: The credential to look up in ``config``, e.g. ``"username"`` or ``"password"``. :param prompt_strategy: An argumentless function to get the value, e.g. from keyring or by prompting the user. :return: The credential value, i.e. the username or password. """ if cli_value is not None: logger.info(f"{key} set by command options") return cli_value elif config.get(key) is not None: logger.info(f"{key} set from config file") return config[key] elif prompt_strategy: warning = "" value = prompt_strategy() if not value: warning = f"Your {key} is empty" elif any(unicodedata.category(c).startswith("C") for c in value): # See https://www.unicode.org/reports/tr44/#General_Category_Values # Most common case is "\x16" when pasting in Windows Command Prompt warning = f"Your {key} contains control characters" if warning: logger.warning(f"{warning}. Did you enter it correctly?") logger.warning( "See https://twine.readthedocs.io/#entering-credentials " "for more information." ) return value else: return None #: Get the CA bundle via :func:`get_userpass_value`. get_cacert = functools.partial(get_userpass_value, key="ca_cert") #: Get the client certificate via :func:`get_userpass_value`. get_clientcert = functools.partial(get_userpass_value, key="client_cert") class EnvironmentDefault(argparse.Action): """Get values from environment variable.""" def __init__( self, env: str, required: bool = True, default: Optional[str] = None, **kwargs: Any, ) -> None: default = os.environ.get(env, default) self.env = env if default: required = False super().__init__(default=default, required=required, **kwargs) def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, values: Union[str, Sequence[Any], None], option_string: Optional[str] = None, ) -> None: setattr(namespace, self.dest, values) class EnvironmentFlag(argparse.Action): """Set boolean flag from environment variable.""" def __init__(self, env: str, **kwargs: Any) -> None: default = self.bool_from_env(os.environ.get(env)) self.env = env super().__init__(default=default, nargs=0, **kwargs) def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, values: Union[str, Sequence[Any], None], option_string: Optional[str] = None, ) -> None: setattr(namespace, self.dest, True) @staticmethod def bool_from_env(val: Optional[str]) -> bool: """Allow '0' and 'false' and 'no' to be False.""" falsey = {"0", "false", "no"} return bool(val and val.lower() not in falsey) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/wheel.py0000644000175100001770000000575114562147542015075 0ustar00runnerdocker# Copyright 2013 Donald Stufft # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import io import os import re import zipfile from typing import List, Optional from pkginfo import distribution from twine import exceptions # Monkeypatch Metadata 2.0 support distribution.HEADER_ATTRS_2_0 = distribution.HEADER_ATTRS_1_2 distribution.HEADER_ATTRS.update({"2.0": distribution.HEADER_ATTRS_2_0}) wheel_file_re = re.compile( r"""^(?P(?P.+?)(-(?P\d.+?))?) ((-(?P\d.*?))?-(?P.+?)-(?P.+?)-(?P.+?) \.whl|\.dist-info)$""", re.VERBOSE, ) class Wheel(distribution.Distribution): def __init__(self, filename: str, metadata_version: Optional[str] = None) -> None: self.filename = filename self.basefilename = os.path.basename(self.filename) self.metadata_version = metadata_version self.extractMetadata() @property def py_version(self) -> str: wheel_info = wheel_file_re.match(self.basefilename) if wheel_info is None: return "any" else: return wheel_info.group("pyver") @staticmethod def find_candidate_metadata_files(names: List[str]) -> List[List[str]]: """Filter files that may be METADATA files.""" tuples = [x.split("/") for x in names if "METADATA" in x] return [x[1] for x in sorted((len(x), x) for x in tuples)] def read(self) -> bytes: fqn = os.path.abspath(os.path.normpath(self.filename)) if not os.path.exists(fqn): raise exceptions.InvalidDistribution("No such file: %s" % fqn) if fqn.endswith(".whl"): archive = zipfile.ZipFile(fqn) names = archive.namelist() def read_file(name: str) -> bytes: return archive.read(name) else: raise exceptions.InvalidDistribution( "Not a known archive format for file: %s" % fqn ) try: for path in self.find_candidate_metadata_files(names): candidate = "/".join(path) data = read_file(candidate) if b"Metadata-Version" in data: return data finally: archive.close() raise exceptions.InvalidDistribution("No METADATA in archive: %s" % fqn) def parse(self, data: bytes) -> None: super().parse(data) fp = io.StringIO(data.decode("utf-8", errors="replace")) msg = distribution.parse(fp) self.description = msg.get_payload() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659106.0 twine-5.0.0/twine/wininst.py0000644000175100001770000000332714562147542015461 0ustar00runnerdockerimport os import re import zipfile from typing import Optional from pkginfo import distribution from twine import exceptions wininst_file_re = re.compile(r".*py(?P\d+\.\d+)\.exe$") class WinInst(distribution.Distribution): def __init__(self, filename: str, metadata_version: Optional[str] = None) -> None: self.filename = filename self.metadata_version = metadata_version self.extractMetadata() @property def py_version(self) -> str: m = wininst_file_re.match(self.filename) if m is None: return "any" else: return m.group("pyver") def read(self) -> bytes: fqn = os.path.abspath(os.path.normpath(self.filename)) if not os.path.exists(fqn): raise exceptions.InvalidDistribution("No such file: %s" % fqn) if fqn.endswith(".exe"): archive = zipfile.ZipFile(fqn) names = archive.namelist() def read_file(name: str) -> bytes: return archive.read(name) else: raise exceptions.InvalidDistribution( "Not a known archive format for file: %s" % fqn ) try: tuples = [ x.split("/") for x in names if x.endswith((".egg-info", "PKG-INFO")) ] schwarz = sorted((len(x), x) for x in tuples) for path in [x[1] for x in schwarz]: candidate = "/".join(path) data = read_file(candidate) if b"Metadata-Version" in data: return data finally: archive.close() raise exceptions.InvalidDistribution( "No PKG-INFO/.egg-info in archive: %s" % fqn ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1707659114.0697205 twine-5.0.0/twine.egg-info/0000755000175100001770000000000014562147552015102 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659114.0 twine-5.0.0/twine.egg-info/PKG-INFO0000644000175100001770000000635414562147552016207 0ustar00runnerdockerMetadata-Version: 2.1 Name: twine Version: 5.0.0 Summary: Collection of utilities for publishing packages on PyPI Home-page: https://twine.readthedocs.io/ Author: Donald Stufft and individual contributors Author-email: donald@stufft.io Project-URL: Source, https://github.com/pypa/twine/ Project-URL: Documentation, https://twine.readthedocs.io/en/latest/ Project-URL: Packaging tutorial, https://packaging.python.org/tutorials/packaging-projects/ Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Natural Language :: English Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: POSIX Classifier: Operating System :: POSIX :: BSD Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Python: >=3.8 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: pkginfo>=1.8.1 Requires-Dist: readme-renderer>=35.0 Requires-Dist: requests>=2.20 Requires-Dist: requests-toolbelt!=0.9.0,>=0.8.0 Requires-Dist: urllib3>=1.26.0 Requires-Dist: importlib-metadata>=3.6 Requires-Dist: keyring>=15.1 Requires-Dist: rfc3986>=1.4.0 Requires-Dist: rich>=12.0.0 .. image:: https://img.shields.io/pypi/v/twine.svg :target: https://pypi.org/project/twine .. image:: https://img.shields.io/pypi/pyversions/twine.svg :target: https://pypi.org/project/twine .. image:: https://img.shields.io/readthedocs/twine :target: https://twine.readthedocs.io .. image:: https://img.shields.io/github/actions/workflow/status/pypa/twine/main.yml?branch=main :target: https://github.com/pypa/twine/actions twine ===== Twine is a utility for `publishing`_ Python packages on `PyPI`_. It provides build system independent uploads of source and binary `distribution artifacts `_ for both new and existing `projects`_. See our `documentation`_ for a description of features, installation and usage instructions, and links to additional resources. Contributing ------------ See our `developer documentation`_ for how to get started, an architectural overview, and our future development plans. Code of Conduct --------------- Everyone interacting in the Twine project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. .. _`publishing`: https://packaging.python.org/tutorials/packaging-projects/ .. _`PyPI`: https://pypi.org .. _`distributions`: https://packaging.python.org/glossary/#term-Distribution-Package .. _`projects`: https://packaging.python.org/glossary/#term-Project .. _`documentation`: https://twine.readthedocs.io/ .. _`developer documentation`: https://twine.readthedocs.io/en/latest/contributing.html .. _`PSF Code of Conduct`: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659114.0 twine-5.0.0/twine.egg-info/SOURCES.txt0000644000175100001770000000425514562147552016774 0ustar00runnerdocker.coveragerc .flake8 .git-blame-ignore-revs .gitignore .isort.cfg .readthedocs.yaml AUTHORS LICENSE README.rst mypy.ini pyproject.toml pytest.ini setup.cfg tox.ini .github/dependabot.yml .github/ISSUE_TEMPLATE/01_upload_failed.yml .github/ISSUE_TEMPLATE/02_bug.yml .github/ISSUE_TEMPLATE/03_feature.yml .github/ISSUE_TEMPLATE/04_other.yml .github/ISSUE_TEMPLATE/config.yml .github/workflows/codeql-analysis.yml .github/workflows/main.yml .github/workflows/release.yml changelog/.gitignore docs/changelog.rst docs/conf.py docs/contributing.rst docs/index.rst docs/requirements.txt docs/internal/twine.auth.rst docs/internal/twine.cli.rst docs/internal/twine.commands.check.rst docs/internal/twine.commands.register.rst docs/internal/twine.commands.rst docs/internal/twine.commands.upload.rst docs/internal/twine.exceptions.rst docs/internal/twine.package.rst docs/internal/twine.repository.rst docs/internal/twine.rst docs/internal/twine.settings.rst docs/internal/twine.utils.rst docs/internal/twine.wheel.rst docs/internal/twine.wininst.rst tests/__init__.py tests/conftest.py tests/helpers.py tests/test_auth.py tests/test_check.py tests/test_cli.py tests/test_commands.py tests/test_integration.py tests/test_main.py tests/test_package.py tests/test_register.py tests/test_repository.py tests/test_settings.py tests/test_upload.py tests/test_utils.py tests/test_wheel.py tests/alt-fixtures/twine-1.5.0-py2.py3-none-any.whl tests/fixtures/deprecated-pypirc tests/fixtures/malformed.tar.gz tests/fixtures/twine-1.5.0-py2.py3-none-any.whl tests/fixtures/twine-1.5.0-py2.py3-none-any.whl.asc tests/fixtures/twine-1.5.0.tar.gz tests/fixtures/twine-1.6.5-py2.py3-none-any.whl tests/fixtures/twine-1.6.5.tar.gz tests/fixtures/twine-3.3.0-py3.9.egg twine/__init__.py twine/__main__.py twine/auth.py twine/cli.py twine/exceptions.py twine/package.py twine/py.typed twine/repository.py twine/settings.py twine/utils.py twine/wheel.py twine/wininst.py twine.egg-info/PKG-INFO twine.egg-info/SOURCES.txt twine.egg-info/dependency_links.txt twine.egg-info/entry_points.txt twine.egg-info/requires.txt twine.egg-info/top_level.txt twine/commands/__init__.py twine/commands/check.py twine/commands/register.py twine/commands/upload.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659114.0 twine-5.0.0/twine.egg-info/dependency_links.txt0000644000175100001770000000000114562147552021150 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659114.0 twine-5.0.0/twine.egg-info/entry_points.txt0000644000175100001770000000027114562147552020400 0ustar00runnerdocker[console_scripts] twine = twine.__main__:main [twine.registered_commands] check = twine.commands.check:main register = twine.commands.register:main upload = twine.commands.upload:main ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659114.0 twine-5.0.0/twine.egg-info/requires.txt0000644000175100001770000000024714562147552017505 0ustar00runnerdockerpkginfo>=1.8.1 readme-renderer>=35.0 requests>=2.20 requests-toolbelt!=0.9.0,>=0.8.0 urllib3>=1.26.0 importlib-metadata>=3.6 keyring>=15.1 rfc3986>=1.4.0 rich>=12.0.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707659114.0 twine-5.0.0/twine.egg-info/top_level.txt0000644000175100001770000000000614562147552017630 0ustar00runnerdockertwine