././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1785167912.5036843 twine-7.0.0/0000755000175100017510000000000015231700051012304 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/.coveragerc0000644000175100017510000000060415231700043014426 0ustar00runnerrunner[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__.: # exclude typing.TYPE_CHECKING if TYPE_CHECKING: if t.TYPE_CHECKING: [html] show_contexts = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/.flake80000644000175100017510000000076015231700043013463 0ustar00runnerrunner[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=1785167907.0 twine-7.0.0/.git-blame-ignore-revs0000644000175100017510000000067315231700043016413 0ustar00runnerrunner# 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=1785167912.4932346 twine-7.0.0/.github/0000755000175100017510000000000015231700050013643 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1785167912.4938784 twine-7.0.0/.github/ISSUE_TEMPLATE/0000755000175100017510000000000015231700050016026 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/.github/ISSUE_TEMPLATE/01_upload_failed.yml0000644000175100017510000001030515231700043021642 0ustar00runnerrunnername: "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: input 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: input 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=1785167907.0 twine-7.0.0/.github/ISSUE_TEMPLATE/02_bug.yml0000644000175100017510000000737215231700043017642 0ustar00runnerrunnername: "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: input id: environment-os-other attributes: label: "If you selected 'Other', describe your Operating System here" placeholder: "example: Linux hostname 6.5.10-200.fc38.x86_64" 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: input 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=1785167907.0 twine-7.0.0/.github/ISSUE_TEMPLATE/03_feature.yml0000644000175100017510000000252115231700043020510 0ustar00runnerrunnername: "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=1785167907.0 twine-7.0.0/.github/ISSUE_TEMPLATE/04_other.yml0000644000175100017510000000206415231700043020201 0ustar00runnerrunnername: "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=1785167907.0 twine-7.0.0/.github/ISSUE_TEMPLATE/config.yml0000644000175100017510000000022015231700043020012 0ustar00runnerrunnerblank_issues_enabled: false contact_links: - name: Packaging issues or metadata issues url: https://github.com/pypa/packaging-problems/issues ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/.github/dependabot.yml0000644000175100017510000000023115231700043016471 0ustar00runnerrunnerversion: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" cooldown: default-days: 7 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1785167912.4942267 twine-7.0.0/.github/workflows/0000755000175100017510000000000015231700050015700 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/.github/workflows/codeql-analysis.yml0000644000175100017510000000574715231700043021532 0ustar00runnerrunner# 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 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@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 # ℹ� 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@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: category: "/language:${{matrix.language}}" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/.github/workflows/main.yml0000644000175100017510000000776415231700043017367 0ustar00runnerrunnername: 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.10" DEFAULT_PYTHON_VERSION: "3.11" permissions: contents: read jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 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.10" - "3.11" - "3.12" - "3.13" - "3.14" platform: - ubuntu-latest - macos-latest - windows-latest tox-environment: - py runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 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 ${{ matrix.tox-environment }} # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 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@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 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=1785167907.0 twine-7.0.0/.github/workflows/release.yml0000644000175100017510000000274315231700043020053 0ustar00runnerrunnername: Publish to PyPI on: push: tags: - "*" permissions: contents: read jobs: build: name: "Build dists" runs-on: "ubuntu-latest" environment: name: "publish" steps: - name: "Checkout repository" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5.0.1 with: persist-credentials: false - name: "Setup Python" uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 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: "Upload dists" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: "dist" path: "dist/" if-no-files-found: error retention-days: 5 publish: name: "Publish to PyPI" if: startsWith(github.ref, 'refs/tags/') needs: ["build"] permissions: contents: write id-token: write runs-on: "ubuntu-latest" steps: - name: "Download dists" uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: "dist" path: "dist/" - name: "Publish dists to PyPI" uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/.gitignore0000644000175100017510000000245315231700043014301 0ustar00runnerrunner# 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=1785167907.0 twine-7.0.0/.isort.cfg0000644000175100017510000000024115231700043014201 0ustar00runnerrunner[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=1785167907.0 twine-7.0.0/.readthedocs.yaml0000644000175100017510000000112015231700043015526 0ustar00runnerrunner# .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=1785167907.0 twine-7.0.0/AUTHORS0000644000175100017510000000277715231700043013372 0ustar00runnerrunner# 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=1785167907.0 twine-7.0.0/LICENSE0000644000175100017510000002273715231700043013325 0ustar00runnerrunner 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=1785167912.5036511 twine-7.0.0/PKG-INFO0000644000175100017510000000673615231700051013415 0ustar00runnerrunnerMetadata-Version: 2.4 Name: twine Version: 7.0.0 Summary: Collection of utilities for publishing packages on PyPI Author-email: Donald Stufft and individual contributors License-Expression: Apache-2.0 Project-URL: Homepage, https://twine.readthedocs.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: 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.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE 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: keyring>=21.2.0; platform_machine != "ppc64le" and platform_machine != "s390x" Requires-Dist: rfc3986>=1.4.0 Requires-Dist: rich>=14.3.3 Requires-Dist: packaging>=26.1 Requires-Dist: id Provides-Extra: keyring Requires-Dist: keyring>=21.2.0; extra == "keyring" Dynamic: license-file .. |twine-version| image:: https://img.shields.io/pypi/v/twine.svg :target: https://pypi.org/project/twine .. |python-versions| image:: https://img.shields.io/pypi/pyversions/twine.svg :target: https://pypi.org/project/twine .. |docs-badge| image:: https://img.shields.io/readthedocs/twine :target: https://twine.readthedocs.io .. |build-badge| image:: https://img.shields.io/github/actions/workflow/status/pypa/twine/main.yml?branch=main :target: https://github.com/pypa/twine/actions |twine-version| |python-versions| |docs-badge| |build-badge| 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=1785167907.0 twine-7.0.0/README.rst0000644000175100017510000000332315231700043013775 0ustar00runnerrunner.. |twine-version| image:: https://img.shields.io/pypi/v/twine.svg :target: https://pypi.org/project/twine .. |python-versions| image:: https://img.shields.io/pypi/pyversions/twine.svg :target: https://pypi.org/project/twine .. |docs-badge| image:: https://img.shields.io/readthedocs/twine :target: https://twine.readthedocs.io .. |build-badge| image:: https://img.shields.io/github/actions/workflow/status/pypa/twine/main.yml?branch=main :target: https://github.com/pypa/twine/actions |twine-version| |python-versions| |docs-badge| |build-badge| 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=1785167912.4943628 twine-7.0.0/changelog/0000755000175100017510000000000015231700050014232 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/changelog/.gitignore0000644000175100017510000000001415231700043016217 0ustar00runnerrunner!.gitignore ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1785167912.4950018 twine-7.0.0/docs/0000755000175100017510000000000015231700050013233 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/docs/changelog.rst0000644000175100017510000007247515231700043015735 0ustar00runnerrunner========= 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 7.0.0 (2026-07-27) ------------------------ Bugfixes ^^^^^^^^ - Specify UTF-8 encoding when reading ``.pypirc`` files. (`#1268 `_) - Add missing subdependencies to ``--version`` output. (`#1275 `_) - The dependency on ``rich`` has been bumped to avoid a hang in some environments. (`#1308 `_) - Indices that respond with non-standard HTTP codes are now handled more gracefully. (`#1309 `_) Deprecations and Removals ^^^^^^^^^^^^^^^^^^^^^^^^^ - Fix uploading packages with metadata version 2.5. The fix no longer allows metadata version 2.0, which was never officially standardised. (`#1317 `_) Misc ^^^^ - `#1298 `_ twine 6.2.0 (2025-09-04) ------------------------ Features ^^^^^^^^ - Automatically refresh short-lived PyPI token in long running Trusted Publishing uploads. In the event that a trusted publishing upload job is taking longer than the validity period of a trusted publishing token (15 minutes at the time of this writing), *and* we are already 10 minutes into that validity period, we will begin to attempt to replace the token on each subsequent request. (`#1246 `_) Bugfixes ^^^^^^^^ - Fix compatibility kludge for invalid License-File metadata entries emitted by build backends to work also with ``packaging`` version 24.0. (`#1217 `_) - Fix a couple of incorrectly rendered error messages. (`#1224 `_) - ``twine`` now enforces ``keyring >= 21.2.0``, which was previously implicitly required by API usage. (`#1229 `_) - ``twine`` now catches ``configparser.Error`` to prevent accidental leaks of secret tokens or passwords to the user's console. (`#1240 `_) Deprecations and Removals ^^^^^^^^^^^^^^^^^^^^^^^^^ - Remove hacks that support ``--skip-existing`` for indexes other than PyPI and TestPyPI. To date, these hacks continue to accrue and there have been numerous issues with them, not the least of which being that every time we update them, the paid index providers change things to break the compatibility we implement for them. Beyond that, these hacks do not work when text is internationalized in the response from the index provider. For a sample of past issues, see: - https://github.com/pypa/twine/issues/1251 - https://github.com/pypa/twine/issues/918 - https://github.com/pypa/twine/issues/856 - https://github.com/pypa/twine/issues/693 - https://github.com/pypa/twine/issues/332 (`#1251 `_) - Remove support for MD5 digests during uploads. This support was entirely vestigial, as MD5 is not a secure hash function and is not actually required on upload by PyPI. Indices that cross-reference the uploaded content with a digest should use the provided SHA-256 and/or BLAKE2 digests instead. (`#1262 `_) Misc ^^^^ - `#1247 `_ Twine 6.1.0 (2025-01-17) ------------------------ Features ^^^^^^^^ - Twine now has preliminary built-in support for `Trusted Publishing `_ as an authentication mechanism. (`#1194 `_) Deprecations and Removals ^^^^^^^^^^^^^^^^^^^^^^^^^ - Remove support for ``egg`` and ``wininst`` distribution types. These are not accepted by PyPI and not produced by any modern build-backends. (`#1195 `_) - Twine no longer supports ``.tar.bz2`` source distributions. (`#1200 `_) Misc ^^^^ - ``packaging`` is used instead of ``pkginfo`` for parsing and validating metadata. This aligns metadata validation to the one performed by PyPI. ``packaging`` version 24.0 or later is required. Support for metadata version 2.4 requires ``packaging`` 24.2 or later. ``pkginfo`` is not a dependency anymore. (`#1180 `_) - Use ``"source"`` instead of ``None`` as ``pyversion`` for ``sdist`` uploads. This is what PyPI (and most likely other package indexes) expects. (`#1191 `_) Twine 6.0.1 (2024-11-30) ------------------------ Bugfixes ^^^^^^^^ - Fixed a regression where ``twine check`` would fail to expand wildcards, e.g. ``twine check 'dist/*'``. (`#1188 `_) Misc ^^^^ - `#1184 `_ Twine 6.0.0 (2024-11-29) ------------------------ Bugfixes ^^^^^^^^ - Restore support for pkginfo 1.11 (`#1116 `_) Deprecations and Removals ^^^^^^^^^^^^^^^^^^^^^^^^^ - Username for PyPI and Test PyPI now defaults to __token__ but no longer overrides a username configured in the environment or supplied on the command line. Workflows still supplying anything other than __token__ for the username when uploading to PyPI or Test PyPI will now fail. Either supply __token__ or do not supply a username at all. (`#1121 `_) Misc ^^^^ - `#1024 `_ Twine 5.1.1 (2024-06-26) ------------------------ Bugfixes ^^^^^^^^ - Resolve DeprecationWarnings when extracting ``twine`` metadata. (`#1115 `_) - Fix bug for Repository URLs with auth where the port was lost. When attempting to prevent printing authentication credentials in URLs provided with username and password, we did not properly handle the case where the URL also contains a port (when reconstructing the URL). This is now handled and tested to ensure no regressions. (`#fix-repo-urls-with-auth-and-port `_) Twine 5.1.0 (2024-05-15) ------------------------ Features ^^^^^^^^ - Add the experimental ``--attestations`` flag. (`#1095 `_) Twine 5.1.0 (2024-05-15) ------------------------ Misc ^^^^ - `#1104 `_ Twine 5.0.0 (2024-02-10) ------------------------ Bugfixes ^^^^^^^^ - Use ``email.message`` instead of ``cgi`` as ``cgi`` has been deprecated (`#969 `_) Features ^^^^^^^^ - Remove support for usernames other than ``__token__`` when uploading to PyPI and TestPyPI (`#1040 `_) Misc ^^^^ - `#931 `_, `#991 `_, `#1028 `_ 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=1785167907.0 twine-7.0.0/docs/conf.py0000644000175100017510000002344315231700043014542 0ustar00runnerrunner# 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/?", # PyPI uses
anchor links that are not understood by # linkcheck. Ignore these link targets in the check until they are # supported. Maybe use linkcheck_anchors_ignore_for_url configuration # option once Sphinx 7.1 or later is used, see # https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-linkcheck_anchors_ignore_for_url r"https://pypi\.org/project/[^/]+/\#", ] intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "requests": ("https://requests.readthedocs.io/en/latest/", None), "packaging": ("https://packaging.pypa.io/en/latest/", None), } # Be strict about the invalid references: nitpicky = True # -- 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=1785167907.0 twine-7.0.0/docs/contributing.rst0000644000175100017510000002310315231700043016475 0ustar00runnerrunnerContributing ============ 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 principal 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=1785167907.0 twine-7.0.0/docs/index.rst0000644000175100017510000002077515231700043015111 0ustar00runnerrunner.. 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 an API token for PyPI: .. code-block:: bash keyring set https://upload.pypi.org/legacy/ __token__ and paste your API key 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/``. .. 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/pypi/warehouse/issues/1627 .. _`Twine issue #338`: https://github.com/pypa/twine/issues/338 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1785167912.496521 twine-7.0.0/docs/internal/0000755000175100017510000000000015231700050015047 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/docs/internal/twine.auth.rst0000644000175100017510000000010015231700043017660 0ustar00runnerrunnertwine.auth module ================= .. automodule:: twine.auth ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/docs/internal/twine.cli.rst0000644000175100017510000000007515231700043017501 0ustar00runnerrunnertwine.cli module ================ .. automodule:: twine.cli ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/docs/internal/twine.commands.check.rst0000644000175100017510000000013615231700043021605 0ustar00runnerrunnertwine.commands.check module =========================== .. automodule:: twine.commands.check ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/docs/internal/twine.commands.register.rst0000644000175100017510000000014715231700043022356 0ustar00runnerrunnertwine.commands.register module ============================== .. automodule:: twine.commands.register ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/docs/internal/twine.commands.rst0000644000175100017510000000027115231700043020531 0ustar00runnerrunnertwine.commands package ====================== .. automodule:: twine.commands .. toctree:: :maxdepth: 4 twine.commands.check twine.commands.register twine.commands.upload ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/docs/internal/twine.commands.upload.rst0000644000175100017510000000014115231700043022010 0ustar00runnerrunnertwine.commands.upload module ============================ .. automodule:: twine.commands.upload ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/docs/internal/twine.exceptions.rst0000644000175100017510000000012215231700043021104 0ustar00runnerrunnertwine.exceptions module ======================= .. automodule:: twine.exceptions ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/docs/internal/twine.package.rst0000644000175100017510000000011115231700043020314 0ustar00runnerrunnertwine.package module ==================== .. automodule:: twine.package ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/docs/internal/twine.repository.rst0000644000175100017510000000012215231700043021142 0ustar00runnerrunnertwine.repository module ======================= .. automodule:: twine.repository ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/docs/internal/twine.rst0000644000175100017510000000035015231700043016727 0ustar00runnerrunnertwine package ============= .. automodule:: twine .. toctree:: :maxdepth: 4 twine.commands twine.auth twine.cli twine.exceptions twine.package twine.repository twine.settings twine.utils twine.wheel ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/docs/internal/twine.settings.rst0000644000175100017510000000011415231700043020564 0ustar00runnerrunnertwine.settings module ===================== .. automodule:: twine.settings ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/docs/internal/twine.utils.rst0000644000175100017510000000010315231700043020062 0ustar00runnerrunnertwine.utils module ================== .. automodule:: twine.utils ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/docs/internal/twine.wheel.rst0000644000175100017510000000010315231700043020026 0ustar00runnerrunnertwine.wheel module ================== .. automodule:: twine.wheel ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/docs/requirements.txt0000644000175100017510000000013715231700043016522 0ustar00runnerrunnerdoc8>=0.8.0 furo>=2021.10.09 readme-renderer>=17.4 Sphinx>=6 sphinxcontrib-programoutput>=0.17 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/mypy.ini0000644000175100017510000000135615231700043014011 0ustar00runnerrunner[mypy] show_traceback = True ; --strict settings warn_redundant_casts = True warn_unused_configs = True warn_unused_ignores = True 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=1785167907.0 twine-7.0.0/pyproject.toml0000644000175100017510000000446215231700043015227 0ustar00runnerrunner# pyproject.toml [build-system] requires = ["setuptools>=77.0.3", "setuptools_scm[toml]>=6.0"] build-backend = "setuptools.build_meta" [project] name = "twine" authors = [ { name = "Donald Stufft and individual contributors", email = "donald@stufft.io" }, ] description = "Collection of utilities for publishing packages on PyPI" license = "Apache-2.0" license-files = ["LICENSE"] classifiers = [ "Intended Audience :: Developers", "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.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", ] requires-python = ">=3.10" dependencies = [ "readme-renderer >= 35.0", "requests >= 2.20", "requests-toolbelt >= 0.8.0, != 0.9.0", "urllib3 >= 1.26.0", # workaround for missing binaries on these platforms, see #1158 "keyring >= 21.2.0; platform_machine != 'ppc64le' and platform_machine != 's390x'", "rfc3986 >= 1.4.0", "rich >= 14.3.3", "packaging >= 26.1", "id", ] dynamic = ["version"] [project.readme] file = "README.rst" content-type = "text/x-rst" [project.urls] Homepage = "https://twine.readthedocs.io/" Source = "https://github.com/pypa/twine/" Documentation = "https://twine.readthedocs.io/en/latest/" "Packaging tutorial" = "https://packaging.python.org/tutorials/packaging-projects/" [project.entry-points."twine.registered_commands"] check = "twine.commands.check:main" upload = "twine.commands.upload:main" register = "twine.commands.register:main" [project.optional-dependencies] keyring = ["keyring >= 21.2.0"] [project.scripts] twine = "twine.__main__:main" [tool.setuptools] packages = ["twine", "twine.commands"] include-package-data = true [tool.setuptools_scm] [tool.towncrier] package = "twine" filename = "docs/changelog.rst" directory = "changelog" issue_format = "`#{issue} `_" underlines = ["-", "^"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/pytest.ini0000644000175100017510000000050615231700043014337 0ustar00runnerrunner[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=1785167912.5039773 twine-7.0.0/setup.cfg0000644000175100017510000000004615231700051014125 0ustar00runnerrunner[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1785167912.4986923 twine-7.0.0/tests/0000755000175100017510000000000015231700050013445 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/__init__.py0000644000175100017510000000000015231700043015546 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/conftest.py0000644000175100017510000000376215231700043015656 0ustar00runnerrunnerimport 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") ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1785167912.499808 twine-7.0.0/tests/fixtures/0000755000175100017510000000000015231700050015316 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/fixtures/deprecated-pypirc0000644000175100017510000000011315231700043020642 0ustar00runnerrunner[server-login] username:testusername password:testpassword [pypi] foo:bar ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/fixtures/everything.metadata230000644000175100017510000000311015231700043021346 0ustar00runnerrunnerMetadata-Version: 2.3 Name: BeagleVote Version: 1.0a2 Platform: ObscureUnix Platform: RareDOS Supported-Platform: RedHat 7.2 Supported-Platform: i386-win32-2791 Summary: A module for collecting votes from beagles. Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM Keywords: dog,puppy,voting,election Home-page: http://www.example.com/~cschultz/bvote/ Download-URL: …/BeagleVote-0.45.tgz Author: C. Schultz, Universal Features Syndicate, Los Angeles, CA Author-email: "C. Schultz" Maintainer: C. Schultz, Universal Features Syndicate, Los Angeles, CA Maintainer-email: "C. Schultz" License: This software may only be obtained by sending the author a postcard, and then the user promises not to redistribute it. Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console (Text Based) Provides-Extra: pdf Requires-Dist: reportlab; extra == 'pdf' Requires-Dist: pkginfo Requires-Dist: PasteDeploy Requires-Dist: zope.interface (>3.5.0) Requires-Dist: pywin32 >1.0; sys_platform == 'win32' Requires-Python: >=3 Requires-External: C Requires-External: libpng (>=1.5) Requires-External: make; sys_platform != "win32" Project-URL: Bug Tracker, http://bitbucket.org/tarek/distribute/issues/ Project-URL: Documentation, https://example.com/BeagleVote Provides-Dist: OtherProject Provides-Dist: AnotherProject (3.4) Provides-Dist: virtual_package; python_version >= "3.4" Dynamic: Obsoletes-Dist This description intentionally left blank. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/fixtures/everything.metadata240000644000175100017510000000325615231700043021362 0ustar00runnerrunnerMetadata-Version: 2.4 Name: BeagleVote Version: 1.0a2 Platform: ObscureUnix Platform: RareDOS Supported-Platform: RedHat 7.2 Supported-Platform: i386-win32-2791 Summary: A module for collecting votes from beagles. Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM Keywords: dog,puppy,voting,election Home-page: http://www.example.com/~cschultz/bvote/ Download-URL: …/BeagleVote-0.45.tgz Author: C. Schultz, Universal Features Syndicate, Los Angeles, CA Author-email: "C. Schultz" Maintainer: C. Schultz, Universal Features Syndicate, Los Angeles, CA Maintainer-email: "C. Schultz" License: This software may only be obtained by sending the author a postcard, and then the user promises not to redistribute it. License-Expression: Apache-2.0 OR BSD-2-Clause License-File: LICENSE.APACHE License-File: LICENSE.BSD Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console (Text Based) Provides-Extra: pdf Requires-Dist: reportlab; extra == 'pdf' Requires-Dist: pkginfo Requires-Dist: PasteDeploy Requires-Dist: zope.interface (>3.5.0) Requires-Dist: pywin32 >1.0; sys_platform == 'win32' Requires-Python: >=3 Requires-External: C Requires-External: libpng (>=1.5) Requires-External: make; sys_platform != "win32" Project-URL: Bug Tracker, http://bitbucket.org/tarek/distribute/issues/ Project-URL: Documentation, https://example.com/BeagleVote Provides-Dist: OtherProject Provides-Dist: AnotherProject (3.4) Provides-Dist: virtual_package; python_version >= "3.4" Dynamic: Obsoletes-Dist This description intentionally left blank. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/fixtures/malformed.tar.gz0000644000175100017510000000001215231700043020406 0ustar00runnerrunner‹ÿ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/fixtures/twine-1.5.0.tar.gz0000644000175100017510000005142715231700043020245 0ustar00runnerrunner‹¿ŒÿTÿdist/twine-1.5.0.tarí}ý{7¶pÝù+´¤y€,Œ þêeãlÝØi|7±ýØÎÍöM½0À³�:3ئÝÜ¿ý=’FóƉë¶{áic˜‘ŽŽŽ¤ó¥££øÆóÝzÃÞ±77¾ú•>›ðÙÛÙá¿»üw³¹ÍùóU£¹½···¹ÛÜÛûj³±µ �ÄÎW�ð™E± ñ•× Â¾õ•ƒbƒÁ�ÄŽ¨¿�OlŒÿÁ»Ë×§ç¿Êøïno/ÿÝí=5þÛ;»8þÛ»;{_‰ÍõøÿêŸ'â@Œ½(Á@LÝ`:vÅÍ(#çÚ½À�C¯;‹Ý¾ˆASEx¾B¹!VˆG®Š�F±õDˆ¤‚ø¶õ„ž½‹Ü ŠANj§Ó9q&®xîNoüm?€?¾�û/DeŽ«�ޏñâUêtä#‚Õu=(‚)Âwƶeâß¾¸ˆgƒA,ž÷éç·ý´½Žâxµ66zNÍõááFÕúoÇ÷½H¼q½¾;ÏÿE?¿ÓOÛóÁ ëÜÄEo4ñb¯o£y»÷Ö‹í¾ûÂ:v|ñ’çL žC˜NÜ ÆÀ1¸þvˆ�³{Áä…õÖ {â :“:ö³x>‰úebLÑ ®;º0¾Œeý÷ƒ^´ñ»àÿÛ;[kþÿèüŸÆ¿ �½ÞÆo:þ»��Ýõøÿ¦ão»“i<ù¿¹³�㿵×ÜÚÜ…�ßlìmm6Öòÿ1>G8Ê ÂAÝsIðöB×A1‹òZN ë«õç?î“[ÿ½‘ãÝq0´A«{ý¿±×ØUü‡äDc¯ �Öëÿ>­ œÂˆ·,k_}¬—j Ϭg¢ºc׉ÜV‡¦‹xÞÜlìÔ7·ê�ÍËŠtgÃVç›�Ž8ô¢éØ™ Ô¦QïGVâtƒYLÜåÚ #É\"7žMã GPˆ<»}Øn£#.fÓiÆ¢ïNC·ç =2�O=Ъ‰qq ²ÒøÖ,$›ÿ•T%zâøýHøA,|0>ú¢4œKXUDÞÐ��ëôûbܸa}ì�î�L1tš¹Qaó®ßwýÞ\›-1‹õoð 9oØá³b6ÞÅ<¼;óÆÈ<ü�7œ…ü ™p�5F`,Ý9T�¦0õoë?ͼÞG$p, Ü%0•ƒÙP4¶D£ÑÚ�ÿ¶N_v]޼ˆù9üuoÝ~¯Tû‰3¨7 Cd¢}¤@„s¸š<àè{rÃÊë�ˆr�QÑ{.±f¨àþ-aý›þ·œ*ÿ†*ã(@–‰Ø€ ½` æÛçC̆#Ùi€…Â9Ö GJ®/÷û…¹m©.àP–ìž_! Ùí¶|Õnƒæ;öâ ¨~h5¯ªÚ`rOs*Ïï�g4²ÎLÉ�®; mǰµ ™tEÌ1,¼,`"$’‰“¹]Åßl° 4QÒü@amGïeIãê*ð4u[û 0ÿÄõC’1¾ äæRDÐx°°gÿ¦åÌÿÝ®‡PÃ1–Ê<'!LpüÀ¯`ä\ù5Ù8,H˪UT–A~AÙ?j&ºÎcÓŒû¼{{P@‘±$ªО~'žökâé´¨Þ¨=='†õãGi!×h"ÿ™·LÐdá•‹³‘)SK9q‡:5‚�ʼI¯y÷g„ÛVm3Ã!Í1a6¡{q©´Ð@RU¨ã¼š€ 'ü8›¶DMÜN• ½ˆ' êqz–#– ¦M`’‘EM%ÄA(WªehP« #=%+ž¶4[ƒ™ßƒQ�{6诠ÖCœ^dab³6ëEm,†óº„(jIØ«K€žjÈÔR ù«ÛÕ2iœzáF½Ð›Ê)<óÑãàÅHÿJ4ƒ±€i`ÛB5ßjUF ½ÍÒ½“È¥°ŒFH0`-\C>àqõ®É¦gÜXßb› W™Îb©'�ÞHcOkQ+§O¨V›�"¯pî«!¦®³=-ÎæC-è×Öì­#†9T•V31pmY¸Í�÷•Pg™¨×¸Â¨ BÉåE+ÉN’æeˆðÑ JxÖFÝG’*áÔ„)Ézß©±ö__¾}#i >OïC @Ï'®9s ,Ž}y�œ)ñ&!.\ö¼§õD _#σDryOÆmnÈ$FÉ)$¿ìñ7|R�¦nÏx=v­�êæýÌMã’¦Ç×ck×Aî: ‘óž¯R|Vÿv¼1ú4ØOãôF\g¢[dà˜$È·œ}ñ˧;4©n2VÐ~Rí(è*5I5ÓAž½’zo"I®,ä+5dæ’ÌÈJé¹”ü/Äõs)ï^¤;[J°ÀÕmð¨–Í(Òè�R&|çÚJ“ÛA²JVé±`ˆmà PÝÌ5d.HÐúÁè2ÕÅÄ#®KÚ†pX³Šƒ)2^Ô‘×w;Õ2hÏÁjM¢u!݇¼¹D­™E_+¬wTªÀ"B�Š{ì±ÒHè¼vDíL©�Dæ¾”3�Cb˜Õ@…™F‰+†e5޽,!¤m¥Ö-Hñ@�C� k�&a2õCT'Bî*IÙ*]µˆý,nÓÑM´©¶nhKzl †>-SaŒµb®ápWí¦^V!Ià,€äå× Ô*]—lø¦™ÆØkÙ3vQõLH‹u4|­¤èÆžÿ1’Î+ÖLÕÎ .I ’Œ€C+O™Š×Þ´¹,Â(B¾ôR:Fyâ³h)QO �‹‚A Ú&³Gx™æ”(ßTåeU¼Ô®mÛ_ÒŽi)çš~~ ªì…ë„@nC�e–¤V!kP5Zvä}•ÛHã1S¬ÔÏ‘x/Ðd¤€‡Ä"бK_ZS, WVÄ6"�_43ß�¿á=µXÐì5x¾ë]ª\$ãÃëÌÌ  +w…ì2厖ÂVsYå"Ѫœäû®=´Á޾E€¥j"Pàmâ2QB唵JJHk„@Ò¶K8�>icI*¨\èE+Ò\ß8—î?>[uµ,ä·mPcX_†@Î3çSgêâŽ)(Œ•2/2zTFáZv¶ùP€«”é7/·Dª|Í2 `"( �Íi ‚¦ÜhÀ_‚ÚhÂ7dk_�äÒ –Á0)TkSLPù�I5%J>@H…ß÷a0›* ¥y7y¥ˆç0(¶tµ•ϦðjW¤%Ì{¼1£’Áì¿ÆZ^M°ÅTÓ ô@ÚEâ,»8ؘ8>°´«ª ­…j‡j…þ%—&{ÄjÉv³Ä釗´“s˜RI�׫;ÀŒJŒ¥|R•Õ‡Ó2Š-üAš,röà M2š&Ú$ ¡Dk]]2 \Ç¡vÇÄæ| ÎÈ¥=i¹í#FŠê:½‘3E§sÒ"² *R(@h߆d®v DR‡#ï5NI2 @â±Xj,Š PèF‘‰>ö… gá8îPOô9�óUTþ¼I:#˜€ÄŸÇss�àœÓ.½¡�¡ÛWn€˜eßzh©cÇwƒY”áÛŸÅ$¡¾ˆM$Ô^`A¼Æñ€"j0 ,5‘An£* ÛE�Áà_T<‰R6á›Ó:©DsûèÖÁ œlTC y7ëÛ-æìÊ 8›Ã´"£Üï;a¡:èm2ª´Á›ò–×/%y¼ƒ¼Sªláp£Ô"!öÉú½ÄÒ(=\ìçj翲ñß»[»ÍuüÏc|l»0ÌGîÇæ"{€eÜ3´jüÌ(ÒÀéƒRÅ ©„½—]ìŽi�B ÆÞGÚ/Ö‡ŽVéç­½ª±%Ϙ9ҖŰÑ‘›Æ�dSö¬÷îÀ»úÜZ9J÷ðÏIlë¢�e½äÍȨeYH'n§ÕB4Zç¶ïNãQK4�‰XÇ’o!#�ÑYE)€oÐê ¤…ÖÑOÔþDò„ ÒŽe=êúÇh»ëÄ_=Öúo4ö¶w³ë¿±µµ^ÿ�ñùöèåëSqúê•e�½/9 UÇUgwBx³Ý»ªôôâìõñÉ?¾{wüæðiIìï‹R 4¤?áŠñj_ò òhXU _Ó‹Ããó}Þ¦goÞp­Ó³Ë‹ýz_åꙦ—ÖO—}jU% ¥a¬ ==éø„ðmÑ»?¹½Q`ŸqLî”t(zîŸÏYõýç‹n·æÔ¸@Ã@{šk rNI­PÅß±"3Æ‚‰cKWÎªê¨ †û‹÷Xq bÀž�À ýŸc—J((Ž|È;1 P]g R^›-ËGi ÿ�Jwé¿/NO2¥”7­¸ä¤4œpR‰ÖµŠeÝ%µe™lÕ¾{­ê.¬z(ËdëºÓY7Ó5¦øT¡y—.bø©8\%0ÎZš‘ûÎ6*Êü�ýr>dÚ£GiR›™R†É™ÐÈŽ3Ê¥L;]pˆXp˪àÙ)ØkQ„.0Ï`v“â¯e¨‚;^õG_È^árý râo'H¼Ø�$àÐæs´iDß°zÚFÖFÁ0ôây2ºº(q g¾ŠP‰é܈;麴… Õ–´ª«�Oô­?[*Ëz°à}bˆÀ0ǧB«$LòÇgU, Z¹?‰� YÂîò§QvqSäÂÆ–Rü]Ô»Ì52ŒÐäÒøÞúv' ƒ�=l <²‹�®hHjÉ?ßQ¹òÍÓþ'¯vªÈ@å t{1Ú’Cc®Ø×äe‘_ÙÀ’.$챸û\Ò‘¤Ô¯×—¤�%Ýa6]ÜÉ—tƒK|fþ*üàF3=u‡vï Ù±w”&Ř“œY‚7¾p¬If/_¸Z[(\¼$]îXÀ¬e<þÈ“ §÷AøŒ¹ir˜ãŸ–=ia'~øEè,é3KÛâKI¼¤¿\âÁz[ú©ŒÇì!–.® ,¥züÓ¨·´ËŒ‘<�†rK¶ j\t“§sÝyÿ’U/AQ Š`»\'Š<ÔþbQOZx…¸->õ–0bÖb0b©â,cÄ\äs™×bÄPa*ÆŠ¬%(±ªõÌ”PG›Ò#ޝ– MŠ]1Ö¬ó-A› |öìN¶M¥Žš“ î¨Í£NzÞÌéPë—<ÑW ðæÀÞ…hOœ¬5Þ%HÃë/ÂÙÔž °†×Ëi�*õBr“¾½œâXä‹:�Rë‹éŽï—ôBšÅ½PöAÖ”NMÉ ç|¶Ö ‡!cpM|jeI?¤mRÜe¸, Ydå~ ÞÚò)æ3ä2V£, ìFHËXŽ*´2êo0jŠá*wð_9™ýý9CчÍ�.tTm;þÓ"s¬ƒ À/]ìêÐY±´’fÝ2iÅEV)(,ÏIióPÅÁʈ35k2;–ÊCèFsœ1+ÊâÞ¶ðï=ü¿oÁ’Æ)ôxþßæîÞfÎÿ»»öÿ>Òùo5à‹Ý½Op“ôÃ…ÄÇ„ñLítpÄ�‘‶¤m+Y3èÙ·Œõ…¿EÊ3L®(åQ‚ÂjŠËßBºŠqoYÔhl® -µ�mY¾Èël'Ùéµ  ôˆ¥Ö=C틯+ µjâ˜þº¢˯Õ*Ã+Ÿšˆ_☯$žùjɲˆW«ª‡Q)9ðÌ¢@mx«ø-½«j·ùו”,©¦j³³–�Ö¸¯æ“ÍB±OÉÏ qQ¼n):Úþ¥)—7 •ðÆB¼ Ö¹5Ó¹}ì¤9T Ñc†[ˆšäÅKÑRÎêe(­è“d�Ë¢Bì)"ì}þ<4 'sÉÒçNibÞwNkvå­ŽÐ^ã’øÑú“ g,tµ�ÝaYUØ)Æ–vEyŠïב;©Îz†ó(˜®á„Ñ=)vç«o$Þá¤v±ƒØ„¾ØE¼¬ äE,ä‹y‘T–ó"å ^ºÚïÓ—ÉGÜ ­C»_¿>}{´a“7gƒ4pÕ܆Ì’Ôû¢av?0Ê9nY¨Í’…”Ÿ¥4awôÝì/ëuÎÂÀuAÚK!¬^-ED:˜—/Š~äcÏaí°ú_fÉì˜9¿¤ gT©/ÐĪÒYž¬ªŠaH(õ®LÇq9�ˆLÈ5‹Jµ‡@«š*Pö!CÝ¡\�&² $ÕÃÆÞüýZy™‡šKŠk°ªj†Ög‡¯î¦4ƸDž½#…oiçØ9÷ôËûà³P‘‰S,ëP9^йÛïÆ¢È«ž#é�Úï"’�ö|UØ�~7J…~òX_¶"´B¿`=à»UÖÂC’%Y idqæÕ@hf–‚j4TUˬ„ã‰lI;§°—Êúº’v¸d¼ÜwXa>.Ø^ÈCC̤=Uˆ™²µ–Ò_ï$¤1[´aPP•ø¡2Ê¢6Ò–3Ådo �Ìm˜Ó›7 Û4ý㨗°X¬—H›q¹^¢üþŠ Ÿë×7ñ7<üm¥»ðkÄ¿9~ytrqô«Üÿ±Øÿßlîmeã¿·w7·×þÿÇøˆ¥Ÿƒ©ÓCýÍëáùxkQ±ÿ‘¹äšöfMü7J\�JÍÍÍíâò(ÔÍÍ�íPtjÌ�D–uytþöBœŠ—§'‡Ç—ǧ'âÕé¹xwqTçGg秇ï^âã•:<¾¸±¬å5€ÕFÇm:^’](¡«O±¡ß›»áDf™|>=Îþ¤YDi…À$ëÏÈÞ¡sä’÷"}Ë’ëcc2I›F‘hh™ö_:kA’ÔÐÀ  JIÞ…àÆÇ‹–BÎF2—g½ŸõYœ¢â$Çñ”]èPòAÖÀ¹ûªiw:ÑÍ5?ó=�vó"Õ>gq@œˆPeIIRã„Á¸&óØÐ�1áJ§òðéÌïËd×�¯Nó@)e;2ñÊܦ#Ýä™…ÓϬj2ê±¥)I%êA$*^•ëa÷šÔ’‰þ^#W¢3‹\™è[�Â^Ó¹a�Ï´ÿBi{#‰R w ¨×Ý9ãí`M�§ €¨x€ F4òð ;ðþALÇ’{·²³ù”²§N0™žN8Ž"�v+8<�+Rà&ª iB.^†Ã{±>�Q²pº!ó)�à­ Â\‡h^§ÖfFÃÕ½U«k¹>bR 9Ý$Óœ]æ èp.qc@×lD.°oññã16¦Þ4`Ñj$¢DrciÚâ{Ôv°Á$—šRxÄŌšœ�…öƒZB&g¥´tQÈõ¨’fE#¬Ôº¦n Ô 9LlÜ¿ñú”HÃ¯Ó GÐKüY•$¢•Ì�q<¯Bó“‚²…7Æðý2ƨI‹¯4b»ŠcJbœ©9¶%ùñtÖ…Š@6™€£–<TY8FôDŠÓBÒ*¶æ§¤®æÚ*½Ä.`8¶Œá8s(7øv,*PÇ�Æœ!T­2™w– a¦Qì¢1Vèݬñ�"¨|*dœ™�$rÇãšü—¯³àaÐK[*¬RS#¶A"ï/�ˆjÏ™NÇhÏ>'eI�—I1R½±ãM"YVõ ÈFLrjö绸½ì„-»¦õSF„ë‘À2×r%ª ŽÔ ¤‘5¥A+Õ|Ûp¦õC…ÆÑ,¬YñJ£%áß í•€¢ËL`¨Ùöˆ€çà”ÕC« ¼ÎÐÁ×ì‹d!Z1ÒÞÓ2£ìáD!Aiìg¨ÙðoJq8vn¢™cÇî�Y¸ô¾—J.%VN1·e¬Š8:£¬r©I Fö ¹ê�¢þ„ôFJƒ&ÛÓ�ôeêÉU 4üdýH9¥Tæí¸öp¬hN8‘Ò£0{«šašœ2ÿGV÷6&ô7*65:qæ �ʲÊ�vó]š_y3'ÛhìŠæ jð7`é™¶Ee:¦bžTK¬"‚œ?—c¡ÌlÄŠ µ,6bªÜ;Ì[‰Éo +Öòaø<è²Se ?¹Î9ÄÚ³*ü_IäQk]£5öw$ʬ¼Æ(b_Hˆó$Ä{_(O-Yh‘j˜2«éJQÀ¾¥3cFF›=£ÍÐÅD-5¥¹1gœçú¤šÔM%£^ãÈ1%ÌjræÖ�µõ]TgjJØãìH½ˆd’(å &)ž(Rª³?€·ÝÒ*A4pš@®#̃3H:�¨•úUu‹ ²´¬pHK'§—Ç/�JÉî).& 5^Õ‚¹fŒõ\°r¤¤ÑQp”UçÀp9}‡/ÆRÓÊ-¤£ÌÀãjbKÆDËœñ—‰ƒV ¤‚QLÏBBÒŒR)�0XÁ�å“È÷¤´‚ŽÂ.!nB•dêDK[ÿ«É‡S3I/Õ´»økŽ�âm˜¬ÃØMŒ�!M4¯çr�ÕƒwA*MœS!t‡x�:^²º>z¿w��)í€n¼3tØ~àòmÀ¤î{ê Bk“¿“æºz‡0z¢ç ù°‘³TfÝ—ÓRáZ“>iþá�žÜAÉhnÜÉ‚[¨ˆž7¡Üž]I{VòÑm™sVª•¢FI]¦•gà@�=/i& 7â=è�@‹¹žãÉîœ C2dÑ’‘+›Œ,…Ä[TK†G.ç(A²‚X¢õ�2ýÌ¢èÔK�c]@À°Kâø¢$¾;¸8¾ j¾?¾|}úîR¼?8??8¹<>º§çæŽñé+qpòƒøûñÉaMÞÜ…kS‡ÊxÄ$ú†Û0Yä7tÓ™ƒåHä!Ó#̰I Þåñ団Ðø¤~|òêüøäû£·G'—5ñöèüåkÀïà»ã7Ç—?ÐTyu|yrtÁ{ÚàìàÆæÝ›ƒsqöîüìôâˆ%#ïw�Ñ‘hO¡9OݱÓwÙæ2¦ RLCUcêä@^:@“,ᚆÿ�ÝpQ4›�y@üÖ‹ˆ/GAÏÓ†'³d¹;H®Is{0o$Âû³ö*b�7žÓõÆ´»{ŒRRð�Øt3€Gcòÿvt+)ë0jw&ÆKì ÃÛw‡coˆ¹¶«5½#[K95Ù[rçtÖ·ôݱ×%%‹Ð¢UÏ>zÕXLwùÑömñôg&˜bþèΠÓyEeZÓ(:¾µÃð'øs¡¶ª“Mkº¶Iz¢ ,,P0ÙsŽÚ{7q{IBT\S€1úkCÞ×E‰Ër·8³$‘o¦yÆŒŸ€iÏCg°GmzW–îÛ*|°·ã€gå0úx „ò«}L1H³F’{FÉGo< YŠ8cu‡øÁt©nØKÑ€›t#˜ 8ÙP?Îz«€v";ýk�6ø2ˆ/ 掫½v &øÙâ ‡�î5—ÜÛ4î!0æüûjÎéu˜ÚñZºy¤tÂÞ(Ø1Hþ¿d7˜�x%�KüïœDÜy[º¼±¦øØœ&—;ñ1´A:Ž˜Žc…²ºcé³!½bÙj ¼›@W±Çʘñ¢d/Ôú×Á Zl«i" ¨I·(–Â+Ÿ¿Öz¥óŸÜ™ò1òĦ¤‰${’%'Þc¸¥k oÀ<—1¯b¢Ç€éÑw`!pqPOû^c'œgQ®¦œ\¤³0Lö~¤÷øª¼‰�е¼û´;—*�ìeÆMˆ¨uécÊzœÆÂ¶îÿ÷öàäøÕÑÅ¥íùÿ·»8ÿóç5îßÙj®Ïÿ?ÊGùdì§¥~Ÿ¾=Â<àúÑÁ;зÎ/, Ò,Ä��ºzůÏìé¼à%¦•€w]'^üÒ�Lã%u—ÁE¼T© ¬¯ÖŸÖÿÙß¿GÝõôqãÛ›; \ÿ{{{›»Í½=ŠÿÝÛ]¯ÿÇø¼uc§ïÄN]Æð¶DÃnX'`¶8qºe¼€Ib]Ì&h›¶@b©“€dgÄ(r<79ŒÊØQÒÎægÇÖë`âÖQÉk‰¥W [¡ÒZýbnY£ŽñHã,~¬÷-ÝÛ^`Iµ¤• h®™‘ËÖar…†ê»NGBÙÛÕ/ý…¯UòЃÅýŸçzßÑÝÏU§¡§žyþæž¿è´í|3/ùFaÔXXIRÊ^Óq@w¥|bQýåýh..8¯þ±xª$úEþ¦_/¸±%ßw¼QÒ]ÚéAmE›Š*zãæöŒbÁ0ò­”Ýd8g·M •Òö¾¾¼<»�–«ƒ!B¼aîË wCªt§3•·p¸ñl •äètDg%;X�¦XÿäŸ&�ªÊÛL� Mw6L]ÏAñ=�f³¹‹C÷�j°:´‰®¯Äî�ÆÝWÜ·�Cº q˜‚&ÙFt;©†ùè—ŃBDÁ™ö¤k\¾¹ *S7ñÐy»<ݰ9 Ïì òiÃzÈ�å…‹î ž_nžèÁÁÔ tûw$åx•ñ´ È’Ñàí€`Ù¼6�]ˆ4YàeùÚ5;�xiå�·*Å�çc`o“g�GC�QÎw:Ê#�U¹¹ç¨ÖØØ—Ã@Þ6˜!+åЊf¡«'2‡>v]Xä Àd zE´}å±»·ˆ´uŒ¶bÊ&©9p~Pwm�Fò9Þˆ¤!f-¤Ðe*�/CÕ')æ7í† òÎþ\ðìtª’žœú‚еŒ<%xÝn„Té'C†$šOÕüÃé�z1…N$¯sÂg^¹Ò–3vþhÑpÇÞ)Œë3ôLß"I‘š¾º¯ˆÉ.asít:ØB½Þw1€˜ [wÄs¤.½�Î2¦ø &ô)ʱÁ|Ñ: ^ŽÌ¨^j¾dTïθ‘_Ž•ÿ)A^¯”Âjêh$ИÜàÎwhçì§�+£ÆsŠæ¯>�fºI 9q…¼¢Úhô"/Nx¶÷Ñ¡PÞä3=7#†«ºŒ¦Ç`Ýz鉹\¨Ø6…[Ö»ã ÷±ÕÂhëQ¾~¾SoJA;}’’Öù/ïðÌe‡|¡†-øªVâJÚIÄû¥`yÏ[_ˆ}†WFØœèâ¿m¢m¾bÓ–³Cº\°Ï_ˆEŠG`ÛÏòE·lTÄÜ?/¡¯¼Nÿþ_þ< rõQêí,"52UäC}tÿ„x@ìôâøòôüüá?ž8><:¹<¾¤G3Ô§âìàââýéù!Ö퉗§oѳwUÒô¯mÛWÅý-Aï!‡C¾„­eÊ}PË ä´ ãûŒ© åP¿ó#wž=½S™ÜÓ«E»Ëd舿X÷ÖcÅ!Æ(Wu�d¬‹©Ä7þ.£Q}Tƒ…­³–)• ï>$÷6½Q§¯Yôâ, sî ¸„tÆ‹•z{™"¼9(*jJ"]6a›$ºŒÏþÎ-+Ç ò² ’‰Ž°h—Íõp%Ü¿?û^èšêõH£‘m2YMؤÖuÕÕɥkb �a)äç1h Y$’EŠHh[=\ S;¿ Y`¢¡¹bA~¬­Œƒªg¬1½Rr ~ W>wåÙó¼ôÍ~&:Çç/µ!tãv{ zØÇìǶ}7þFþù ö÷Ÿ6·Ð\›ÈèèœOð)(üuá…½TݪÙÐ÷^üzÖ5iù|©CÀl§À<ÖÛņ>¸\ÏhÐY–�DÑÎ}ÐÀ=ÆÞ6åò[NöØ 78tZAÞ¡Ç“ Û…¡ÎÛÀ×››Rö|¦Î$£7•†Ôøà¶Ó)dŠuqÄ{Út$ �ÒNè˜eŽÈ@ý²¼¶RŸÄw¦‚Ïç³Qݤì€R¿rû´ß©ÛuX(ó_$”OÜ,YZºÕ4DÚ¼x=ÕpMà�é5Zs ÅÐ|´µm*íÔãƒ1¨^¥;I ºÓo DÚ¶Å‘aÁù‰Ã Ë†ÿÉU�×�Ê-?éå–")2v×ÕG*Ã�zÉÄsò9}Ë×—â„y!*³p\ítj2Àž&YG>8mÈ·¤»* ÏלÍ`Ïq‹7Ò‡äìRS.?-=œ†¸Ü—©ÃiN`2*wL‰Õ˜ig)¼ç;çДw0J7@2°P�F@M¾ôCPûº ņdžN“÷Áˆ›Ô¹k tÏÀR@Ò¶Ä»“¿Ÿœ¾?±^â¥èhi…-Júë㈀ý�[þ´ÇCî¦Ã5˪�l("‡† jiÿß…:K¬2˜ÕOP9Á­[X§3Ô Þ‘?{ ªšåN§(€䂎}a¹·Nïô"ùò�;+ R±b)üòÝÅáê…ßxþìön”=<»Áï=¿ܤÉyÃÐF9¬š0/¹GQüÖ¼oy{÷Þ5öîYcë¾åíæ½kÜ¿�í{Ö8ÆÃÑIöRxòò³F(çl~6_o¡ýçïÿ%{¾�¹ÿ�´ÿ·³»·×ܤü?;Íݽõþߣ�¿%o'þuö³¬ÏÙ¿²¬ûT ö§¬‡Ý�zØ}¨Þz¨}§‡ÚoZ´Ïd}ѾÒî'=ä>Òƒí¥÷�¬‡Û'Z¶?ôÀûBºôEû@+íÿ•Õ>OÂ8–îçÜsç¡öo–îÛ|Ù~ ^Uoìˤ™h¡{ÞZ´ßbY¼¯"+ßwÿdykwî�XÅû!K¡íwXzòÔ>†Ú¿X˜ÜŸøõ÷%>c?"»aݵïð8û ¼Ï°êþ‚µ|?áKö`ÿà>û_°_ðEû«í|ѾÀçî|Ñ>Àçûÿ?ßï¿e%~}ƒu¾ÿþ¿ýgúëAí7ýòi9ó þ÷/ó»/÷·[¿†ýáüê_èO¿Ã�n=¬ßü!ý剟ü³ýãÖûÃÈþ;ðÿ°²Õ 5þ{ss«¹—‰ÿÞÙ^ŸÿxœÏR¦¯¬™ï!/�h_4,ëeM¯ƒ é `Q]Y ÝÑÙÖŸì8t®½Èžã-LÀ;lÏ÷ ÆDF’_YÒ@Šê¤Gb•�×fdýiúqȹûAñš:!&¬aU¿­20îï‹rÓÞ-Hw8lcé++v†|$‚ÔXh_lÒ�èÚo«¬šôpí­þüõ?�?îù�Íí­:ÿµÓÜÚÜÚÙÛ¡ó_›;ëõÿ8÷¿&9››�­ô™ ë‰õDí‘ö�TKŽQˆ ]k ò­Wÿ T:t¤ñáa™LeDð|™°ƒN«jåWŸ%¢Ï‰]i"šYŠÔ†¯Ãß‘_^u«ã™,¾J¶ƒEg½å>2@12 å³"£âd(LiðäK3¨tH¯ ×M! r‹jÍq¬6ÿ†èëö³ ¿1‡Ì#B”ï”m‘:C\#‚q$“òËR¿æ‘þ®üFR-j+ILûi¶%)Jì:()Á!^ìãäR��6áŦ½G¯®ènohÏ–²„„LJVóJ<•fMìUÙÊÏ5ÿÕ>AW¢I¶`-j°B?QÞçKÎÚm0�Çn»Íe«ú¥Â^Óû~rÀH—‰ø8•�ùŠÚf¹`êú•R²3WªÚèF¬Tey NþVà@‡Ö¯f¡§1áÀúÿT•ä/ÒÓuú¥ë÷ôŽr´oîŽ �Râ*Ý?Fì½$DÃ,¶J„Æ]å)šb¥B2>c岞q'ºÑf�åûù«—¤È‰{·wï[aï~¶îYÜn޷½[ؾ_……A_ c2’‹’Ýÿ.¬È_)$ ŸHlì«È@ܰ2v&ݾƒËQKœ¾ËG_ p6X“˜/¶RˆZ /4EM'Ý^UY^ž­®|�Zjzðd´¸¬JW²4'›J"t#t³~0;À2 ®L"õ°A7j»~ÊcER„V*�‚:¬® ›.G·^\MŽèe°æëøJñºì>â¼VÇ~/òŸçÕ©wÈÿ­]PöPþïì6·wöš(ÿw·Ö÷þò[ƒ`zɤÖâ-þ—ˆÿU…²–pZ¾çdðÀóû(ˆc�0·S,ÛŽƒn¤ Ç3íÅ1Ì¢"ÙSR«ò¡LMm´Û˜-®|Ue© ÀÜ’¤�.�W•µÛX¬&ôCv2P]St&öetÓ¬éŠê6ùœ¯�±×Oã_ (ÿÇÏÜ#¬fÊÅ{f‡Ý2ÊõÂæG@2@´gÜ&ŽO¾ÇSQªm''TTã–ï—«º2µ1ÂŒú¶L5£}ë6*4¡ I´A+œ†^eýLZ(׊ڕª—ªª‘.§Ã&j2§2¸ž$8 N4Pªl* ™8 ® •½¥E*6—Vó4ºmœ±må÷Ç¥x!Á˪_å<Ù‹©^Ð1¦êËI­��ßFÿ“ û1ü?»;»ÛYýo§±Îÿ¸öÿ¬Àµÿ'åÿ €'9ñH»vÜÛø&t¦KTObd îáÑ«ƒwo.Ûf�þÐ�Û|O8G�AàíkÔÏLÙ˜”¬Ä“ißSzw { ?$vö¿ϯDèßáb5Q²¹PIŠ*¾�·öù9”¸)U13ÿ`š(ƒƒ©�3Í­¨~Ú}:hW)•J)‰Cg;¨§éC ˜Ðü¶Ž7•ÀúDé �Yéšøè*“ZK†ÝïS·ñg:a•Š�ç÷êg²¿W*UÓî'ƒpÜÝ* ;cÛŸ–ZÆû¡âÂá]ÁÐeö#%ÚP¶¤ð.eÊ(\Uý;)÷Iî*|ÛÚšÖ¿»Y°ËûŽ¥¼võ�¼ž�³ë5ý%ó€džÿåLý7¢ ÚS‹i’~{%úx›:žvê·uåD Ë8_h~!õ¤>þ­´Ù~lKLfKe «ƒjàÝÒÝòjé ¥Ñ•´SçvÊ ‡¥¥_y˜Êjãïòâ+›“¸\8tßJG^=fãj4ËŸ]ŽO¬”{c�µ4O¹Ïø lmüsº>¤ïÊßR–á„•$�0B�"¿|Ò5ÙŠ `�v�ÞG¥ª –šoâªE�;`p¡(wÔÓßeÕ*†ø¤æuZ™«hÂÐuQ¤òxPã%IjÚ¥�x~á3 UÓ6ŒsKÁ¶~·û?äÁû}Äÿ4w×ñ?¿ÍøÞáGŠÿiìm©ñß�ÿÑÿ³¹µÎÿ³öÿ¬ý?kÿûÚmÜPhƒ0f¥¤�^`ì‹qŒ‚ÒYþªaðOuÔA�$Ñ'øµ>?Á?õ�ËT¡ŠxÈfem­ãÓ{ß‹ƒa¬½ô| ÐÂâĸé±ê>]ùÞlZöëeïÍ!Àš$XbÉÂGX½˜‹YKiíó{•ÿz#øqäc§Añ?[{Í-Ü "ù¿¹Žÿyùÿç�Ynt=3ðYlk­¬µ‚?ή�05Ã�Æžª£‚N¥Ã 9[%±íg¡¯KTðܨ¯?4Zfã tÖ ,ÛGm€ùb‰«cqLýTa˜ÔàÕ<ÿУ«ðÿmx˜9ÿ¿»¹ÝXóÿµý·æôkNorzu2_oø¶C� ÍL?@Ét=�&©GYh²y PF´ Ž¿V†a0›î—ž�-§…ÊÞWRÈÙØ•¶yîV¤«*®³ïN•�eUY"Ý¿l~ªé.ÁÏüL: šŸÊ6gÝJú²¶}¬R�U䓪ʖ�øßŸÓÌĤ_œ. 0„þà ™%$¥´¨´²e!̦e@R— ãÒÁÌ”K¿WPÒÙtÖEÝ©ü{M1%¿QvkŠåÌÑÂ�­Ê£Ó0©psKM0û@æ…<£7•i ÷¥ÁoV±�~¿­rH&Ä.Õë]cÒ¡9¿_Ê¿Q‰#JO©¡j¤žàP‹ pµ¤´ ¹„øŒOz¢© ”z¦Žë$ÍöF0„hÿCϦ=fd5=d“ô¾Z½! 4ZÁ¼›ûzx.Þ���]\$ï},Ÿ8?z{\òè\Ï#ÐS6L°éˆ'�^xE-¯Ã»É�ã•_¼ôôÉyÌÚøOAƒOð"b�tÅ×„Ó ÕÔ½e‘ê¿ì$J z•î{°’Ì •œï³ô’ïkCz³ˆ©Ä#ùÊ´Š_m‰òÓ¨\OuWspL¼O‘ÏÞx¸‚]u™ª-¯ù":YÙpoc5QÕtèËr‚$ÇÖéÊÀšn ¦ïÛQÇS؈wý 9Ùü¤ý~j~t�ˆê&C!]�Ãé°­N¼m•R§Þ1þ­î”xx`h&ÉÈ(€š˜j –Ìóò¥y†‘°b;9_Q@”_Ÿ©Ã‹¥fÞ)E”J`ÖDÂ죫©+uR¾qÝWICP}°y;¦È4?±?–׌ˆ‡.|z×®o3“î:"Ïo޹Z¢Â“Þ^æ2(á`šgVsuˆè‰8tc”ØˌµìJ7êßb¨îtûékÖ,ÕO¾åÆ¢ïõ §z~rô« ž«Ävúšß`Ë}fm„8VJx—Üö�‡z¨Œ´Ça�©šH!/WûJõï{„3ÞÇCòŠÅæÿnȳT|Ú(4*‹í›xª’ùp%³œ�Pþ»;7$r2êyš_”ÞòÉ/Q†õSò0 ¾ÂR6= }ùR)·­–ld_zfœj©ZÉ®n/(½ Âí|0Ï]UÕ|ãâx,)¤ó‡Ræ¾UÊ󋇆2�¯²DL·ÀH}$Ý¡ ‡¡–jUñ‰FÅäzÂLè]ñ®¨+›|©°—*uGr>ÑT_*ÉÒæÚjßHŸV4՛йü¹4¯7’ƒèÝFù¨’Ê‚:'¦­B©,+´ !A×®©»xˇsû¦\õ™i«ò}Ëhëè6Æ(b¾çH^»’L[™:Î$Q �çSB?£?áÞôDo7Ø*4’Å p2Åh[pâ%Êájêj�Ú‘ÞàÊor­(éßùýàÆÏ\‘F“M ø\­ÅÒ1½l-sÔ˜pfè¬é´‡8Ó[þ‡b6O[5¶ Õé<¹‡@ÚɃVIí{14¢Õ¡)�ýsàeG2U ö™3ùjRê2,œÅuX® Xé’Q9»ëô]x{ì�+|¶Ä 0jÛŸ{£oJÕ›ÝÍÏôQÎ'rC>-ZüUx:ƪlfÎk†AÜ ÆªcXºQªYø¼vó>Cž MO™È¸tX!ÓÉ´�´IEtðF¦qÒ}QÁÒð 8@ �Ïömž´�Œ]~rsg2Hi@Š3µ3gŸgÚ—‡)Tiù3ShL\`üCWÓ2ùX„*Å¿ ‹ð9ˆtA~–)Ž{àèƒr5ÔäÉ¢ièÙç™jÒS¥JËŸ™BÆ%!ª ñ(Sø£;GÁ©’êwvèÇNŒ TS¿³åŒë?TQãQQàѸ†Ú mhL�g¹á—w)¶³Øäßäðb­*hý?3kÏŽÎÄVc;·š¯a­ê¾¨ßµìÑo¾H@K.H Ðû:¼.§ä°y«dwÃnæPú¨£H-ý¬V܃¶t™¤ºA!™ª‘~º û©©‡‹*  jÜ8WI½XT‘•Ù\5~lPóSò5ÉÄ‘èP¥°›OÆ¡üxÃã>¦æàkuRïD¡ìP÷XÄ<Û–ØÜ5ÕH†�JýÂF>”&ý ã”FÒÎ¥CÓ†ÇĹ·\¤b¨(l ¶¥½ÐæÙ˪¤ª2ÙÄzLÓ*!lRâƒQ÷Ê 3Øòy%QóC M} „ºZ1Õ°º�jZOÉkÏùáÖ}üuðÉFb%ÆÏÊ>#ø1šbNS6Fl0‹2†e‘ÉTËMœ}²ñ+kâš yüF9¼Tº=Žôuf’Kj_Ñ!ýà~%ïZIÙ³ 'ûn6ÍolqÉwVGØÝº Õ]ÅïÞcè*mH^+¢ ÷Ñ¥CЏ”U  ‚/MóiŒíý@ð`¾V�˜Q¦fö9&«¢ 4jG1Œ6†CÈýÔºw@¨´W‹ ɱf\ n¨–€ˆ3Çûœz$úXZéfpQ‘@Zâ©úZ�ª¥ÕÃ.ëcÈNÀ\ðj�»í8œ¹ˆ¿rƦžÂx/¿�ü>˜Nu\òEDK€%mãMåS¾])}Q¹²ø¿œ\“KûñŠPXrYú}ššqS:©LÑ ¹×¥é÷i|Ê�¤WJ¿çåé÷i¾ÇÍ+ ¯¨õ{]£~�¶ÉTÊF—þbë—Ê^—R¼IŠ®‘é^åðé1ž½Èù}¥ÿÿÙ3 «l­jºz�è¶ž%5õɼÒ/ðÜn·É6i·muœïSKà›D\Ã�}ø¿úçØß’øŸMý»JüçÞN£‘Éÿ»»½¾ÿaÿ³ŽÿYÇÿ,ÍÿKpè _ƒ:Hqˆ�ºŠŒ*hEÁ<(Ϧ¡3œ8-˜°PüZ&Ë”õ_R}6Pö¤à!åGÞ„O 97mÏŸÎhz³Ïb«è‚bñBT¶jújb¨@©K?¬Ä¥žz©ÁcXN.¡™K&³1H¿KÒð1³Ue÷Kz+¶¤·É�8z‚ö¡qû HŸ=�«P©¤Žp1#K s+Zæö¹*äE´ïEŒ[ŽŽøE'`¼_ÎE±zžL±,iæâ’Ÿ¬üI)š¬Ë±J–6&Í©b›ó(u ‘} ÙxÏÜ¡S¥Èò4=(­Pz·:iˆ]‘GN¤SÝ–tnÙRÚÙÅ2º<‡@˜ÅAO% -US£fà¤õE˜]Ë!Øp¬Xv?½w”�ÈÛÞ¥+uHOFüòIÝ­MÆ–ü%7ê¹Áý”Ú]ψ‘«�ƒ¡ç—Œ.’kÅ�ó|b„À¯r;¨M�§´‘;ªª;àõU†ž,ãx¢6±èXbBÃÔfõGoÊRØ,?À/<Λ}Éež�[Æb. `³Ú*Ž73Py óΡöÓá8²q FôGÎ5ûH‰tÍ.œNåFXº”4º{ú˜¾ {N&Ób,žHyŠL©,˜Þ!–#AÅïÛRj2[f×1DÑŠiV§Àô¦q[g[elJ¥0ÊHM‘d#ñJ«d86£ñ.r¹ø �ƒT‚ÂEÒë†�1‹°(`¡Hʼn•7ZcOT:ÃNµ&u['fe“jw$]°ï� lù-;‰4”=”~²N¦¿‰~‹’ÿ �C‹Â7é«ÀÝ2T‹ímÔ;8¥;Üv‹" @RKÊ¥¤.µ¤*D1f¸Â¹ K–Á—f] C ã±P•¦ Mƒ¿PÛ&BTX¢"�¤)ÑÊ/K:¼ö;P²šb*Ipx”VK© ×�Lš*%º`rÉ:8�±Êí‹H“W�ÝFÂæ]ÊÒ«OÎÙüÚK-#^^E‚” eº‹®o3Ö 7Á”:�~žØsÆì˯KæƒÐÜ~9“Ñ:ÓÈ~d¢ÃÖDùó!¢yjš¶D™ò8›‘]Ÿ‰[&‹öݸ%­lGØòoS05òNÿÇøÔ n�æÿÙinídòÿíní­ïZûÖþŸµÿ'ãÿIÎD©o?{S²K2g¹.ÈQq|ª`ªßËÏoµ½ W�±”§�ŒÄ‚z?'UÀNíô¨‹§ÌÒ@×·É͇I.ãd™@ì×G‡GçíƒËËó‹v³½Iaý‹Þ7ÚÍŵíÙšr+¿”0aoK,mæn‡�4à=B—B¨1#Îý6¬Èƒ~óÏÊßΞ£ì†‰ôB}aÿåoÕJâãûø»ú·Ä¶¨ðËîÌ÷ñõ3|M�(‘êÓO§ë%?0ô‹~i8?⡯ÿHt¯SB«¯§š43ìÿ9:ÿîôâûBû@|@­bŽHµ¥}B%½¨Dîx`bg£÷éKc× ÊÛ:e?²ˆ d¡ÈHZóQº‚Ë¡Þj’¨¨ŸoA™�åÏ5âI¨&¡n`ÆCH“w_¤ÆÓžpæKõjVoLªÛ”«¬Â¢êÀ ¶M«L«ƒŸ|è.‡Q¿ý œÐƒtÛÕ¬w!{Øí'¿zGœxù$€Uë �¶ÄÓ¨ŒÇ¼ ^*¢$AíeœAå `'ì�¼kIÉ`ìÿçM_á¼'`fQ>¶¯ªPÔ.e€«¦þ©8Ü¿àОAu‹H›—‚å�è0äøxÕ_D’ÔÖ+=˜MùH߇[é£+o”9¼èE ÷(Y~{typxpyPÆÇ·é{§¢ÞÄãÏÉÅÍ*0Ò•[°•o `ÜØU&ø.Ù«Œ“Û�«¤¼{Uà„¹å!‹ƒ&a¾\ˆ]©9ö%¢ëæËB?»eµëRÛ)««V¡¯Xå—5ã¢ÁàÅÍû™¦#–¬¥s[‘—5óCJ �Îõ0C$L^5ÞQᓺÄfª¼³_¡r–qaHÉÁJJXà�¶vßE1͵¢M¢aVB1ôÁ´šfnFÀ22ˆL\°úæ2çØ:=Çoaÿñá�´ï°ÿ¶[”ÿk»±ÕÜkpþ/L¶¶ÿáóûRáï©HË©Z¬ž†%ûÙtžè“?öÿò£ ÿTÄT_— ��Óü_Ò '¨ ¦©·\ 8A+6í²4ä\Éñç¥e: ›üç*‘0«ÖJäý•Èí@ß[ó¹;’uWª¢§Ã|¥2 ãóµòù«*ŸŠÔz8©¡kÍêªÿé‘ýMó¿n77×ù_ãñﻘ:Èõ{sÐâü�‘߯¿†þ�)`Óãßlnï®÷å³fÔkþ¿`ý›—U|öÚ_eý7v3ü¿ÙØk¬ó>ÊçC/ðñÐw›=qÑ•Åg÷…ºh‚�¿´ð�e}XxÊ•%Ïþ¨šê…ÍÏ%€õŠûì¥ìÿÊþ¿ÆöæNFþ7övkùÿ(Ÿ¬]Ù »a�P ߊd¼€Ib]p> þ»ç=·Öë`âÖ1GKK,½æÖ: ì+-±ò=¶²F�²©´Dö[KF,´–®X‡Én„ê»2†÷ñ£é/—Ä*ñ~$Ùÿy®÷Ý}ñ|Ùy îù‹NÛÎ7ór†á8ÇPÔÀ‡¿2L ’§$s¹× €è/ïGsq1 f@Øc €—#/ú›~]/üôÈØõ†˜ŠÝ]Rt�¢ %VôðB®Þ,tmóLpDQAP…¦†NÏuí†âõååÙ… }uð*$Ž»5òÜŒ0x¨Óa"ò=Q@QIŽNGt‘ij„iú¸·Ó â¦CÑñ…Ø8ãˆÇ†¦;FæÐxQ4sÍfs‡F­ŒS¿EÊÌcoâRw°ûÐaüÊM¥cå‹ä�t×…>&Î]ß ܾ-çv�‡[“àòÍEúØ0M,ÌFåòtÃæ’hy£ƒ* xÌ� hÛçùå` ´œiètiQGò§˜hKFƒ:‘,»KàŒ°4YàeÙ¸=Œ›î«S’0¹té:èQh’M£"GC�Agú~11"VNŸ·˜9�‚ϰµ ¨ýYá/0 <&­&2 ÛÇ ¹›9¨Œ ¼"Ú¾b¯[!i)¡S–�„«ùAÝÕçºñ´•‘î,Ë��" ®étÌÜB‰ùM»!3<Â;�ª¤§ëcü¡›4Pòß©ÒO† IÃ+)_¸0’9æ+6m9;d€,öù ±Hñl{ãY¾è–�Š˜ûç%ôåPÉôÿ_þ< rõQ:½yDjdªÈ‡úè þ ErØ Gø�'Ž�N.�/éÑL¼»8:?9x{tuçå‰êSqvpqñþôüëöÄËÓ·oÖÝUiH?п¶m_÷“NKññ1�##»§C rŸÏH+²ÒU‘f}¿T#+�ÕéH–§)¦R0½‹FõQ 6&\I펂7§77‰#uA/ÎÂ0ç‚3NG&/VêíJÙš²ÍGØ&‰.ã³<RD2Ñ–N<¤®„û)‹²M&« ›Ôº®z¸2¹î•º(‹D²H ­c«‡+#qÏFY44w@,TB"ùheî•Èh W>׉ä¤o¾ð3Ñ9>© ¡·ÛÓ‰ƒÔ àØ˜€üoðT�q´ÿ´¹…æ:ØDF§@ç|‚OAᯠ/ì¥êV͆¾÷â׳®IËçKf;æ±r˜úàr=Dý+Ð剢�û �'7{Û”Ëo��˜ì®:~o*Ø]"‡Sû|9+Û…`v‡¼˜Mƒ¤ìùŒ•Œ˜îU¦!5>¸ít ™b]ñ ´:eAÒÐ1¢^8C‡®pAõHgNÆÔuó/htƒ”‚ÏY«PÝÈ¥_¹} ºÐíˆ:,”ù/Ê'n–,-Ýj¢>u¢Rþ ×Ú5Zs ÅÐ|´µm*íÔã¾c:J%Ýé7‰ "mÛâȰàˆüœç2B çj‰)§åY¢ƒw—¯OÏ/¤HRi ­tl*Ã�zÉÄsò9}Ûп�æ…¨ÌÂqµÓ©¡’²õÒéȇ‚ÚR,LÉ·¤»;6¨å]œ5Ž8›Á žs>y…²œ]jÊå§e¢‡Ó—ÑXüH|ŒÍ Ñw¯Ýq0%Vãú×^øô�Ž á|¯‘©±¯Hº’�… ñÌëꚦ8²J0uÐ `(æ0<6tš¼8¡Uªo2ä`ÅèžÉ¤¾-ñîäï'§ïO¬—:§p Œ ¼EFììÊ- Úã!÷æTª¬:ó%@d‹ƒ)fÂ…ª-íÿ»�ç¶TÑTõTN@Õx£NHA½#8ö@U5Ë�BÃi4|¥2–{ëôN/’/ÿ¸³*+–Â/ß]®^ø�çÏnïFÙë…dÃï=¿ܤÉyÆ 'Xͤ ó’{ÅoÍû–·wï]cïž5¶î[ÞnÞ»ÆýÛØ¾g�ãÉtìâæìðäåg�PÎÙül¾Þ¸û?·ÿ§òmÑÞÿJñ?;[ÙýÿÍõþßã|ÔýƒêjñbŸ6ÃÈ1EÙðɦ½go®Ëÿ­õqúîüåÑÅ/ÿ;ïßlîeÖÿæÖæ:ÿç£|¤¹cɤÖÛƒ“ãWG—¶ç[çG‡o�ì0Š™ؽÁÐÒ{ý m áK^úŦí8R~ø]š’¿%ï&PÙî:ò¥d÷z¶;™Æs‹Œà ü·Ýãv¹ùD_M�z(×rÓ†:µc>áX¦ä‰‚œJ|k¥ó XÙcqÖ‚8kñú±V­µî¿³–ˆçì»8˜¶Çh%/“{½ó„yø+¿×Ÿ?>ÿOM¢_UÿÛÜÍê�íµþ÷xã¿6îÖŸõgýYÖŸõgýYÖŸõgýYÖŸõgýYÖŸõgýYÖŸõgýYÖŸõç?âóÿ Ut²¸././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/fixtures/twine-1.6.5.tar.gz0000644000175100017510000006413415231700043020252 0ustar00runnerrunner‹€õqVÿdist/twine-1.6.5.tarí½{{ÛÆ±8œ‹O±‡®’ ‰Ô-e-7Š-Ç:±%=’üsó::$H‚$j`P“æ|öw.»‹Å…ÔŲӜ’M-ØËììììÌììLrín³eïÚ;_}¦Ï&|övvøï.ÿÝloó_þ|Õjïnmoíµwáûfkkgw÷+±óÕøÌãĉ„øÊ„ÑЉËÊA±Ñè–Aâ@Ôß?È'1æÿàÝÅ듳óÏ2ÿ»5%óßj·v`þwö¶ö6·[{{0ÿÛ»[{_‰ÍõüöÏq |/ND837œù®¸ž„bâ\¹bIäõç‰;I(ˆT„@”a…dâz‘yQœXO„H+xa`[OèÙ»Ø�± …ÑÔ¡~z½cgêŠgîÔñüï†!ü ìÄ>µyä×{=qí%ªÔëÉGÔVßõ‚±gؾãÛ–õÿÅy2�ñlH?¿‹é§í…Ðà$IfqgccàDÐÝnÔ­ÿv‚À‹Å׺¾xöúù�O?m/…Ï­3ljóÁdê%Ðp¿¾‹qâNÝ/±‡îsëÈ Ä ¦™P<G@N2ý)À^}7ÆÁÙƒpúÜzëDqÐ�œiûE<›Æý2 1¤è 7]_ÄáT¼]¸Q,ž%áÔ@f7°§øÈìê,FÞxîŠ~ÓÓô, ‡ùÃ,y±ˆ`ìçáüÚÄ1~/€„ÈK_㋺u0t¦âÂñã)ÔuàÇw ý°Îsë¿Ý /=Àt>†‰œ÷¿û<Ò#D�kæ1Œò«õç÷äÿÃp!àþûÿöNk½ÿÿ>óß…�&Þ`ãw�ÿÝÖöözþ×ù·Ýé,Y<žü·½½|þw¶aþqö·6wwvaþ÷¶6×òßùâ,ƒrˆx £ \¼‘ë ˜…òš$‰õöüŸ°ÿ&N0výplƒTÿÕ—Xÿ;ÛÛ»šÿoíÑúo·Ûëõÿ%>�0šÁŒw,k_}¬ŠŒgÖ×¢J€ëÄn§Gä"žµ7[;ÍV»ÙÚ}Þ³,! H>†×;;=ñý|:‘ûϹ'q3 C¿ïú‰¸…Ù 0þ稛®p®Bo P¨‚À ã9Œ¢0Š‹ýn«~7›í½\¿ÛÐï©“LbV ' T’bTY�¥MExÚGânï'.(?‘;žû0ùîÍ,r㘔ÖLs»=qx3pIßµ™¤3`QPCY=…г0ö’0Z€ÞúÑ]@97‘ãa!“ ½†=ÉÖìâÀ¶Ò�mîä¶µ×kh¨6{âÝÌ�!B{ãÀI�0f'†!†¡ûáœØ9‚8›û>�!ç‚î ‡ ÔÃàÇ.3ÿx>›…Ì�çШróÖëÙâbâÅÔÒÀ™ÇPÛ;››ÂÅy€ŸÓÅé‘p‚¡d^¹ fÎà£=àS ¨˜ð0+Ô[›Eœ´%N6ÿÒl›ÇI[á!ƒœxÙo,œÙ, g‘ç$®¿°¨¿ƒ^G‰4_VFÎf·>z@� E©óÚE õœ_G¡œØ­¸kz0>|•Žqjs‡ˆ6êq(æÁ ú'ÔÁ�R ¨7‹p^MûPfÚKæŽ$>(JÍõz_¡A“Aˆô4ÜBƒý6‰xâú>A=õÆ“„û�zq,±®hÒÀ §Î߇€:ìÈ»A MLyq ɶÒéi¦Èô•wC=¡JC<3,%‹{Óhp[78r T(²¹«çü—pÌÆ'5†bùížxSNäjùÝÅ_€ÆÎ`)# §‹©]-é8ßç_özâÀ÷ÃkêJ„ÐÅ3wà�ôÌZ´b ¬®ÙbæEƒ^qÈ-àiá„·ÙŒ?z³¦{ãÅ `(aä;cì¡×cÂaÜ‹$dîC° 4ÕX]Qw,y�|q¸Ô¨ZÉYNØ<¾w"œH¨yã1‘1PÝìã f"v�h0Á‰Æ îõNü¡ytüê!Ô3ãˆ'áÜå`‡‰D_bè ¸e_®˜´S˜ì÷aôQ8Q8'¾B™©\ãïaÑ…×1³Û¹f¶�¬²Noì&3'Žmù7Kš­lQxå a™„@ï°hf£¹/¦ÀÚ‰ox#sNpµ…sZCà$v~޾í‰sIY°¥Ý¸×)'"˜Õ©R8ÝÙ%°“.�-Ø ²kê[ …—^<ó�…@¤£=éÈé#tH jwHc7™Ï�ªqÿáÅ÷Ý¡ÙØn+…zèÏ¢D–McæåÂ=äGÛþKZqN§À€˜!;ŠÊx6®È•Ïõ‘„�ݨé{S�V¬Z{ؽ \,,òèØ–èØ&1£�EGû[œFÚkÄ™û¸’HÅ( §Ò\ Dw˜õ¶�) ©«i¨õ@€ï"5ˆÄ½I2•p·>ôÞ�dg´ ˆ•´dVÞÞc^|x¸@Ý^ïg¡äÂgf¼»Ì ŠœÛRëAMv”�`wEÜ,ÄØû1mɃ‰;øHÍÐÒ‡ p‰Æ=œ7Ns’ò£-±"ÀÄÿnÈk~ûW.· @ìÃjEI‰àö"™€„ÍÏÏ�íxæ\°‡s\úf/0ÒsÀÉ`‚`8b ' I¨úùßRý¯½¹ Ê^«½ÓÚÚío»…ö?|½Öÿ¾Èù_óë&¬ä!1OFÍoñ ÞñfT1Ÿ�彦?÷|drÁÈÏ#~†›Eƒ-F$@ÕxKô¦ùϹ7øˆ&¹â˜ßȽ­-Ñjuvà¿m�ËŒ� IN¦}þº7î¿×êÃô0p0�"dö,y�Æ…›®zøž=:m8O°7z&.K9¸C_ ­‘¬‘ÕãÏ‘��4€Ê vClTž'™'áØ ÜˆȲ=¶r^y3t|êÀÆ6ræ~òWõ˜Å­ˆY«H°s#ºÜèŠT ܬh¬²*ÈÀÈ�ÛÇ‹X} �µ?G#À0mÜðcQfÈœ”ø š4FÂOêÔ7j€ P(RD6p�,dÆ®~ìq[г LôiØàlì—Ô“ÓÌÆ¥”Nœ� ÕDa˜4hg¹-ÛéÇÔ&T˜:*@L?ýy¤ä{ð„ömî1$�°a?q£¤¶ÙÈ7U«ÚÕzݺCAúÁ ¸Â)Ñ:¢µÙ?ÐTç§¶ù€�œ'RͲ \7%ÐÍo }�Ó‚Q\C É•°¢@º¼®Ä¾¨¶ìÍ*6��,Tš$$5�S+ ÔèâeÓµywš<¬hœ.ƒ’€6qw%‘�ÖX´«rï6¶¿®Ö��l<Ä­0pAI4ZÚH&¯õ$)V…7ð4q°ä�+%âß%oI§)ªäÕ•ç^kÓ¯”Þ¿/wUõ *jÚîYG®‚ÊUl©×]ܺóȃ1UÔ©8ŸmÓ=BΑÐ�7žÆ•´ºür·’Èu©º1¹32jû�|Tp€FÔÜf— ¬I½@mK—íÒšƒy©tõ³ ¡âÍóhþ¤<�|KœŽèǶøIW�A y¸¢j‚èH;H®nL+—kê"@»´Í4A?¨ª¦ŠÏû‡¯×Šmñ‹.<ÀNAÆuo¨WµFËõ ôþƒ·-ùëv+Ö œ-"²�À3Ü}"ëE‚‚"ôã�z8g&À>-aëñ*=‹T_%‡ª¾¤5G�Æ +o[”ƒŒ�>p‰5C=hî_²­QÇÿ’¤ò¯†4¡¡ÍÃ}%òÂ9¨¡Ä·© ¤PŒÇ9hh 7çDw«}•x)ð¸¿Û?)ÈmK §²bÿ#ô‚aÈîvå«n$tßKjP þ¡Ó¾¬ËÖ@UöSNåN3ëø³‰³Ñwg´‚Ä6£ÖtSh]!Ó‡…7G3!"Ñ�ÈÉn¹}ÅßlÐôFlRÉðSh k;Ú—I*Idä俉u_û 0ÿ +îŒÉu(�‹bj�' Gö/`ZÎâ_áz8 œc)|ÀsÚ$Ã)Î@ÍÌœË[~'&@6 粃bÕ�eP…‡>ZéQw4Õå�V §)ðîQâMA|AÆV-Q5híé÷âé°!žþD‹ê�òérX?Aœer�¦û?ó–)ªV¼r‘Y�2EÐèÑ^B6? ?*5,»æÝ¤·«úf†C’cÊl"÷üBI9 ÀNU£�ój4L�èã|Ö=TÅ{uRHc&”ã4•#”ÜL—šIg%•'¡Z«WaCƒZ}mÂ.ÙL¡A¶ 7Ø ¿‚dØŒ�¼HÆnm–‹ºX 醄(JI8ª h=Ó‘)¥û¯îDË´s…"o&Ix eÄKÿµxsd`ÛBußéÔDÜz—w÷Hb>E�e4A„káòÏ«wE¶†�å-¶=@KÀUfóDÊ $7ÒÜÓZÔÂéªÕåFŽWHûjŠièÒÎ~ºÓlÁ¸°foÄx°�|h¨.µ{žbàÚ²p—ï«M�÷D½ÆD€U´Xó¢•h§�è2ÂöÐ)î>𬋲7Î$UBÒ’d¹ïÄXû¯/Þ¾‘8“ûxìäš”KÍâlЗ×hišoâÜå“÷¬œÈ›DÊ׈ÅË-\k“dêw¹�¦n” ù™Ú°päàˆÑá“&Û¤½Û‚H†ó~q¥‰:üH�GxNÝ:\Ùìo-‰!z±d0 CÛdöØ^®;e *vU{Q/´¦kÛö§ôcªÌ…®€ŸŸ€L{N翦<Ë,I­B¥´ìÈ +Ͻ|S¬ÔÏyÏQw$‰ÓA*Yx‰ãKµJ¹$H¡CLaáa'R F}óÝÙ>¼†Íæã‘x°Þ¥ìE{|t•£¼†ÂGÙ¬ )»´Ül5—U¶-ÓI¾ïÚcêl°RO7x›ÚNÔ¦rÂâ%5J@k„š¤sHÒš�€±|ÒÅ’TPÙ4МV&¾q.Ü¿?X†µ,ä7]cXp†@V4ÞÎgÎÌÅ#^kU^dô¨Š›kÕÙæ€®R¥ßX¼Ú™ò Ëhu„ mmÎØhª­ü¥V[møf4ÂÚWMriÕ–Á0é2�–¦' û;Sò7PMÉØ´BŒï‡(œÏÔ¥y7™§ˆçpS¬òju;™ÏÐÙá H¬óáŸ�$(d0ûo°”׬:5tØíbñ–]nL�XÚe]N…–BµeµFÿ’m“Mc�ôÛ[’ìà :Òy™I�×w·„•Jù¤.M«�'eB ¸má’d‘s°)q’“4Q'Q¥R{:µŽ#m—Ið¾Ûh8—ÑåùÌ V(ë &Î ­Ïi�È&¨HéB8´çjKB,e82c#IÒ&“6H<«@��"Gƒ =u“hl_Úä<ò3ͽÔäƒÆ²‚ ˆ ³Ã?€7I«#ÐhOàýÃåýäE¼¥‚A‘mqçŒÃû3/â«Z¤”8ÝL{w\§¼@�­L-S’©¨5i^©£�:ÐÓeáÁÒù!»"+°³-Y�—i­Vv)>„ŒpT%DTœ­ ÷†ŒàÛfîÂgU�Äiñ")0½Ì™JrS�~PcdFVÂ=?„ïÞ}ŠÆs � Íût\oH`hÿ•'!fÙ·^ Rªïn8�s|ûALB!ê“ØDŠí%Äk<É=à2„ û6вÀ±]”‚þE‘Á )U³}“¬ÓJDÛ‡7z åÝBÉ»YÞî0gWjÀéÈŠ”ò`èDC˜„~䠵ɨÒMlÆg_¿VäÝW²b̨²Fã�J‡6±ß¬ßÙÿ‡&çñî~Üåþ¾ËÞÿÚÝÚÛ\ûÿ|‰�m—ºùÈóØ‚g:ñÞϵjüÎÉÓÀ‚,ÅrI‚ƒWºæãb"WßûHç¥Àñо*Í ¢15±dŒGª°èV"zòи—jØ–õÞõ¡yWÇ-¨ÆÙþWz·eÙDz^ðadܱ,Ä÷Óé �©s3tgɤ#ÚÀ;¬#É®�'hþŽ­²ÉÑp„þÉ-´ž~¢Î'Ò'¬‡ö,ë‹®ôƱûNòÅüÿZ­=ºÿ•Yÿ­íÖzý‰Ïw‡/^Ÿˆ“W¯,ëìð­xÁî²Úÿ;‡í¨S•§ç§¯�Žÿþý»£7/ŸVÄþ¾¨T@0úžœ¯ö%¿ C†U·ð5½xyt¶Ï‡ÂôìàÍ®urzq¾ßЧªÔS$L\y±xšy*lªwÔúöب˜+€Ÿ\°§§‡gy0s}¾”ª &ºhYØ—õÄÓLѧ\=×õÊúÙ²O­ºDbK�4�U¡�'ûŸ¼z÷'w0 íSö©À’yÏýÏ3–xÿçyOÞ*JŸÐí…qY[�MJ ƒÊÿŽå ¦ö,]8«ª£*V/>v`Á�Š{6t+höô]*¡ZqäCyM7ªëÌ`—�×fÏòQÂİ e‡ôßç'ǹRʈV>²M¶7);ëÚÿLdݵe™|Õ¡{¥ê.­úR–É×ugó~nhx žê"DwÙ"†yŠÝUpFª%ŠÜw¶Q>æïlŽÓ�¡ãC®?z”E%° ‘+ehÚ±Ù©oF¹ŒF§ Ž îY<=QWrP+ê§ÅÙ_/ÎaºÐë�¾�šÂå†d»ß0nºx‰;M›CÍ—ïCÐY}ÃêYÕXëãÈKéì²ë¢„%šÊC%¡û-î´ïÒÉ�[²¢¬v¾µ0´þDLT¨<{À‚ˆ ÀŸzØZ-e’?]‡†E4EåvãŸb#ÆBÖŸ0‘üi”]ÞY®±§ Í>s�#4¹4¾·þ„ÃÁ+¡lXkáÕ’DlôEKbKþùžüÈ•IžŽ=yE°-E:*çZ·—ƒ-9T9äŠ}­^ù|ðËV !e�å£0ØçŠ�¤¥>ßXÒ>V ‡ÙtùP$ _1 .ñÀ!üUáµfzê&Ú{Ç Øq7)‡œö™pãûG‡:ÝÉìÕ WK ¥‹—v—[0K�?rÁÔÉ ïq‚27K/sü�eO&z³“?‚2pVŒ™wÛòË�xÅx¹Ä£�¶òÏAèûl––­0ªdFüÏÉ`å�"yåŽìĸ²ÖMžÎuæcìHV½íˆaû5ÚuâØCé/Í´‡WÛ²ÆÇ“Á FÌRÌF,EœUŒ˜‹<”y- ¦r¨HÀZ‹ZŸÀL©uµ);ãøjÐ$Ø•CÍ2ß °©Àƒ©;=-•2ja öWÀŽÒ\9è$ç­€œ.ß~ ÂSyµnvì] öÔYÂúQâ]4¼þ$˜Mé¹jx½×(R/E7ÉÛ«1ŽE>i±¾ïø~Å(¤ P> ¥äUé I†çÁRƒœ†œÂQFøÔËŠqHݤ|JqY1²È�Ç�pkͧœÏÈ&W±¥-a7ZAZÅrT¡;ƒþ�¥¸]eþ+»“£°àVô¥x§U§�ÿc‘:V Á†t`OV.vué¬|·’jݪ݊‹Ü}¦\ŠÛA×ì•z¨Ü_¥£™"džôÂN¤ð¹1¹çÔ neùh;ø÷öß· I# }9ûo{wo³`ÿÝ[Û¿Ðýo5áËͽOðlô'ÄÄׄñN�t°£�Š�N¢m+]3hÙ·Œõ…¿EÆ2L¦(eQ‚ŠÄåo!MÅx¤¬üh46׆žºÎ¶,_f�u¶Ó‚lôZRPZÄ2ëž[Š?×hõÔ0ýçšnX~­×áaZ».léÅäµ¾ ”[b½Œâ‰¼–«öʋ€| ÈEYõ Õº[Ù]a» –}úúäø§«dWbÃŽ²‘V©,“Ê«õG©V)IžD[EéßÙpÄÒJKj«K÷ÅRåfnYØ(#ß!wðý3[¶¥ûyUÚµÕïÔª]ѵïnØ6ë|’mÛlèæm³Ú-n³èJ#w÷¶s› ÜßÔ�Aéý­Ýfõåo³Ôcؼ í!ù–¶GP£–O‘åÍ[ÅYC3ÚYi>7 ÞbAϵ¹Òˆn–U—•- –# Âhény³ÆmFùÑLò™9y€U>CˆŸb˜¯¥–ùzŲˆW«jFSÑŒF™}àk‹ü³á­â·ô®®Íæ®eö’z¦6k h ûÝl²ùVl€Sò³RX¯[ Ž6…*DÊä @¥¼±.ƒu®Í4nß:©•·„à1Ã-Mòâ•`)cõ*�îh“`�Ë—‚Bì% l}~†‘¹béëKIš˜÷­dͦ¼»t‹Õ¸"~¶þ$½KÍÇeýãpx¯*ŒÜÆVEYŠï7�[ ™Á”Z†‹ ˜¦á”Ñ=)7«o¤Öá´v¹�Øl}¹‰xUÈ‹x“/çERXÍ‹”5xåj¿ÏX¦ñ€´ ýþùõÉÛà ›¬9$�«î6d¬�´–ˆf\ÙýšQÆqËBi¦-$ü¬Ä ›£ogy«s¾ \$½”ÂÁâÕJ@¤�yõ¢XbG.´cLâ¬ÿ«¼3;fÌ/¹‘³ªÄ—Hbui,OWUÍP$”xW¥[¸@Däš'!…Ä{Ÿu�(ûˆ¡áPLJ yoèago~<„^^›CÉ¥ Åu³ªj×§/_ÝŽitmOJGGßÊÁ±qþvò+Úàó­ S§|¯Cáx%dn¿Š2«z®‰”~—¡„¤çÛ°ÂôÛA*µ“—´õi+B ôKÖ¾»ËZxL´¤k!‹ƒ<̼ÌÜRP�fUÕr+áèŽH¶¤žS:J¥ý¹–5¸ä¼IÈB¶ìÀ ¤*ñC¥†•3D­¤­fŠéÙ@˜O:0É›Jû4íã(—°X.—H�qµ\¢ìþ ¡µë›ðþ’¾²CøþßoŽ^Ÿ~–ü_+â¿¶÷¶òþßÛ»­uþ�/ò+?3g€ò›7ÀkñÖ²bÿOÆ’kÛ› ñ߸ã®ÔÞÜÜ.¯!o@]__Ûu@ |î$Þ°¬‹Ã³·çâàø¥xqrüòèâèäø\¼:9ïÎâìðôìä廸¸A¥^�_œ}ÿŸXV‹ÂÀj£[Šœ…¡"‡PAS;^^C»7Ý)v£©Œ†|iœíIó˜¢ �J6œ“¾C÷äÈ$ïÅ:Ë%Þ�bg2H«F±hé=í/:XAÔЀÂHi¸…ð:ÀD›!YÈ+ŒÞ/ú.NYqÚÇñr]äPðA–Àyøªkw 2Ñ!5Zè~¤¹ 0n!6¡úçà Ø"TÁQÒˆ8Qè7døúá¬tŸÎƒ¡ Êê6”R:±#ã­,lºÉÍùK¢YˆWU5õÜÒŒTdA,j^�ëa°ù†”’m ü½A¦DLa"’«{C8jº. û3�¿PÁÁD‚ÔÀuÁp;Ô°ÆÆµ‡$MÔ<€�&#žxxxÿ(¡ÛÈl·¶³ù”¢§Rò·4:­à¨õ€®X5çaü¢>ð`â2MÊÙý)œW(0~‹*us‚ñ*~z»ƒD¤€µÝ’r× ¸˜9„虈‰iœ&!KOç´ÅQÔÿižœfǃ#ûMå o ˆ7ž`Ddl�i.ÓP’xÿ Æ(YÇ\‰ÃQr�4$ïûb`×F§ÑÛ†ZÒ¹0ÕŠœôéС1JôŒ÷b�J`ê¢8„b1-;ÀFœÆEJÅ'¾Nëáˆs 5²ãÂrCC±ÇÕXrhiøËÌ 5â€FfIzÀÕºš_º$=¢h¶á¨¡¶4�Å8ˆ7É ~aä›.�Ü<‰ë…¨zÒ32§d À–23@T*ÃË©:Sx¹ æ†Aóx«”ÂÓø|¥vÞÇ<ÓLöJ.�§Z° 5œÖ_”íü4¡´;­äò¦�l•úFŠî»LzŽ–HwÛ“EE�¦‚ ñ®¬ù*ÔpÑ€…ÀMˆö¾ã½`|:À ó@¢[ �k,»)r7Iœ®BxÜX¹…0#2[‡ÿRhf¾ÆšÒš2ö-§pðXó`Îè‚¢miòu蟥 -™Xn(Î�™p½2Ë`Îù樻)q>)Ô½'ö%·º‹¯²CTD#ˆgÞ`ÎcX•\ùX”Š.$ ¹œŒ9+Îa²”Ü�ùTŽ1«‚0×!ª×™µ™“põhÕêZ-�˜CN7Íu'&˜õÎÅ”{îÀ%n àš�Èc¶¡ ñ±CPõf!o­FüID'0–¶-~@i;LC¨)�GœÏy;”Yª?¨%drVŠFg …Lo*IV´�£‡•ƒR×ÌMDcÀÄüáµ7¤øA“&9†QâÏ&ˆ$ѵ”páøÉ¢9Š\ K Âf¶á<8ƬI‹S/±^Å1$1Rj�mI~<›÷¡" MÆÝh¤OTÞcz"·SCÒ"¶æ§$®ú*Ùz‰]ÀtlÓqêPlð?ì\Ô8…U«L†›åxB`‡hÌZ7œQ…/…”Óp$Ã�Ä”ó�ÿåt< ziK�UJjÄ6h@dý¥Qý9³™�ú\pPØ)0é5ðo˲jL€6jÁD§f�‹ÇËNäѲa4?¥D¸mXæZ®ÅuÁžZ¡T²¦ 4äråå*Ð8”kQ@Í‚W,Ù>&ÉÔ%3�©fÝ#žƒ$«§ Q�w�±ƒ¯ÙÉ›hÍ{OËŒ¢‡†…±Ÿ£dÿ)²¡ï\Çs/Áúî˜Y¸´¾—B.ÅSÎ0·U¬Š8:ƒ¬B¨ÉFŒ A 5…ý)É�ýLƳ5È�ä¥êÉU $ütýÈ}J‰<ÌÛqíá\M8±’£0h«¢0�Nÿc«{ú››:�:‹”GåY Æ� ½ù6É‹SÞ,H7š»"zA‰þ†¼{fuQ…©œ'5Ríƒ� égê²c,”ÁŒ‹¼+.Ô±X‰©óè0\%Ƽ%¨Xʇéó`XÈ~L”50üçkÏ‹ð¥-�zë½±½#fe£˜m!ÒI„y_(<-ih±ê˜+r%/`vßÒ1c£Ï�Ñgäb –†’\ �˜ƒ. cR]ê®ÒYo°ç˜ÚÌ’rÈÚ†.Š3 µÙSÖË$]Dr<©—r’ OQŠÙŸj€�ÝB’*akà04¡\G”(5@qC5°4¬«,J4ÉR³Â)­Ÿ\½8¬¤§§fV�xUæš1ÖsÉ( ’fGµ£´:LRì NŒ¥ÈÊ-Å£ŒÀãjdKÆDËœá—�ƒî€HÕF9>KI¥B¡°jC–O çIé(]ŠÜ+)éÄ+{ÿ«É‡3”¤—jÖ\ƒü5ÇÀímœnXÅÆ1B]­ŽŽx–ɇ²3#sÐ��hj µhØÄ±-ôLhÄÂp׺ ÃË_L\•ƒ%ÎáÕ˜ZÚÜY?Õf0K =R¾Æ¾òö–ºú¦öÎê�”{@Ç -�õY~ªÃ3¨ :>ÖÐGú Ÿ„R±"ŸÁ€€ŠçêÙº|Ä�T®&@vÁ»?Û 7U>Æ 4]/ä ­sA*MœS#!rǘÇ�²‹çd}´~ï;SÒAÌùëµ ; ]ÎZLâ®qÖ¡2O¸XÛ’™ÞcCÖ@ëzO \!4’Je°}I– Ö†´ÑHõs{òPwÞ8$Gs# ¡"xß„Bzö%îYÈG³eÁX©VŠš%•L«ÈÀ;{6&“& G˜àq±Ð4®�ì/X1$E5¹²iÂHSH­E�tzärŽS k%jßÕÏ,ŠF½Ì<ÖÑ »rp.ŽÎ+âûƒó£sÂæû£‹×'ï.Äûƒ³³ƒã‹£ÃsqrfžŸ¼Ç?‰�Ž_6dæ.\³1TÀ#&14̆éê »¡£˜Î4GB©QŽMö.Ž.Þ6Çǘ÷üìèø‡Ã·‡Ç ñöðìÅk€ïàû£7G?©¼:º8><ç3ílàôà ææÝ›ƒ3qúîìôäü�wF>ïòÑ�`Ï ;O¥Öº¬sd“…³ÈCј9’¹ˆÈR®iØÙ Çó)©Äo½˜ør<­x2K–§ƒdš4�‹J"Ø·¬Wák¼ñœ¾çÓéîî’‚swSÒhjÓý £¬¤,èә“ØŠwàŽ}oŒ!¶ë }"ÛÈ5ÙZr+9ë|-C×÷ú$dXcÔêÙF¯:K(—ß–“?3Á óGsM�O÷•jM³èL9Y‡aOBU§‡Ö”­IZ¢ ,,0ÙrŽÒ[7ñxI¶¨¸,¦b´×F|®‹;.ï«xÄ™W }sÍ3æüT{ž:ƒ=jÕ»¶òÜVÁƒ£õC¦Êq1÷ƒ²«}„ 4œ¡“fƒvî9u<ñ.âø*»%ΔT7dpÀ]º1ÊÇyk6 �ÈÎðÊ£¾‘t"ÀdÁEÝ´;–º HzF‚[Žç¦—±#é;JòÁéâôÈzNÝ& y±2ºu@*�»'æ–5šè�äw`ñc½ï(õSb{¡%Å’NΡ¹az.[/ÓÌjì: EoW¿ôΦä¡‹Ç¿(Œ¾§‡/ž©AÃH=3õþæ‘?ïuíb7/8£0J,,$)a¯™RꢌM,.iDy?YˆsŽ«$Þ�(‰v‘¿é×KµÇމ$½ñØ¥“”V´j °Â®' nÏÉ =�PKPÛµˆ4T«¶÷õÅÅé¹Ô\tâ+˜ð@Ü5‰Ò½ÞL&ßp“ù 0*ÑÑ뉞’%郮!›é@ËÐø°ž˜”þ|œÉÇAž=­v»½ “‚R,'úÐm¶í=û/ßQÊ [vûieÒNO”ÏJ±º—«[%jAO>Š#Ö©ÖltDù!-¤%ÙB(f Y‚*E:%;Õ�Âìà| Ùï!Ñ͵¡^cøâÍ9M"A¥æm3‰ËÔŒ½êf ܲΙž­Ár+’ʹ‹Ö&_ÏfôÜcäJ.ÁãÉØu¥»®jdÅdóiC¦a9ÌÊÎJ¢ExY½rͤF`™IWE0òôÛàicJU3ʶO7…¤.,Ïu�{õuʆ9´Rˆ®x¹z�°geß’6˜®p¯ ·¯<¶&—¡¶‰Î\ŒÙ4ò’ ׆ÍN>ÇŽÅ~ÑùDès¼2ÆîDÿín‹Û¶¤iÑÁ1"�}o|],ºe£œçþ× üÊ„sú÷ÿòç�kN2oç1I©™"š“Kø'Âûg'çG'g?áïÿñÄÑËÃã‹£ z4Ç{jgÇo/-qËçCs&NÎÏߟœ½Äºñâä-Ü^•¦ôýkÛöeù8AXÐGÔјS»u¬’† ðŠrv#ã,ÉÔ‡²×ßú‘Ûž>M³ÿê-‰­qÒ3%¸S³î�ÇŠ=˜q_ÕnÔ$C]Ž%Î#¼ GÍI¶Ц|£0£"YÏé�ºÜM§\7^’oäl.E�ñâN£½È Þœå”%Œ-]vcŸ´uŸsü�ŸZ–�aËË7‘:¶E‡¨õ¨‡w‚ý‡Ó„®©²³ÇŒ|—éjÂ.µÈ«Þ]º&ºŠH‘ŽAÉ‘.RB‹Úêá��0…ô[€PŽ &š; tÞ$êÑ�aPõŒ5¦WJaÁç!hò�º&-©WGo kiU×6ªºÑ@^ÌÓK}¯Ú]Ï\y…¾¸Ë -zGg/´Vwíö âØèŽÀÎ`nò7t` @¼ØÚÞB«(xÆ@¶}‚OA±h 6S·nvôƒ—¼ž÷Í9{¶Ò®aöS¢åëSoCî\-Ï´èJÎGš¹Þ}ÀÀ£†Þ6÷ÿ·³²9žÓŒèÒ…LÈ~%S¼+DR¸ ?ÁÉÙÍÝül®®V£Q˜H Ô…ðÆPsM‚iŠC>šg™ ’&@Ñ3´ë5;– ésŠqL¡úLE‚¯™£XKA¥çéØV÷#š° ¿ÊV~ãnI£Ó½f[¤3Ø‘7H5rÌ÷Þ0[kï¡B™�¶¶Må€F|àƒˆSÔ–´ ;ûÖ@‘�¤m[š"¡Ÿ8Zž(~¡ä˜¬Už\Jc½ÜúbÃI@}¤ÐÝ롱O<#ÓÙwœ| 湨Í#¿Þë5ä="²ž|(p"Ø—Û*ÙGÓá*w*¼&t:‡I<ÓêXßÙ�Ô¥H®H–©¼OS\Êh¡T[0¦–ë‡3bifô\òRGzg»‰L%)Í éÄB¼á59w#zÒC47 L� ƒ&+‡áþ©CðØ+×õЕ~dè´ÌÐV¬xT¶7cO”=³d*ãÅUc’zñj*:¤WÒ\ÌX�ü/µ„…áTú·gn»!)¹7@ÞòО}hyÎO§ùQ”Ú)Aúî–Nͽò‘í…n@á¨@) ‘=áÖ‚Å7Ò–O¡ÒiG¼;þñøäý±õÂÇ`t G  ù€Òˆn Dþ—L"Ù,«¼ ÈYxa8 ñ²XGÛ„ÏÕýrí¬~Œ%çÓ›£ˆõƒ1àn’)w2CçDë9]Äro�ÁÉyúåï·V@IðŽ¥ðË÷ç/ï^ø�ÌonÙÃû,€üñÞ †áu�§Q8ŽØ (®YŠi$½{QüÖ¾oy{÷Þ5öîYcë¾åíö½kÜ¿�í{ר¹g�#¼bŸÆÀ…'/4§Åv€3,Ö±ÿÎç¿é™ÿg8ÿ•G£%ç¿›Û{;_µÚ;{;[ø¿môÿhC±õùï—šKf§þ<ç™ÖCÎ/-kÉ9å’óIëqÏ#÷ò1Ï÷ÜñQÏùœqÙù¢õIç‰�xŽø˜ç‡�vn˜=/´ï|pÕ¹à#Ÿ>ê9à'�ÿÝéܰ¬Î÷R~´òïžçw�un·ò¼îÓÎé,+s—åÍ¥Ç2Ö²s6Ëâó4Yù¾çf«{»õ\Ì*?[ÙjÙ9—¥Ïµ,K�_©s«»7&Ï¥>ÿyÔΡòçOÖmçM_æœé‘Ï—îz®d­>Gú”ó£G87ºÏyÑ'œ}ÒùÐÝÎ…>é<è¡ç@ŸtþóðsŸ‡Ÿ÷<ôœçç;·œëXVz~cl ?§¹å|æ�ç2 ¥˜ç/ÙýëQÎY>í|eõ¹Šõ9ÎQïüäÏMn9/±÷|ä1ÏEÒó�ŸƒX�|îñHç¸^²ç�ü1Ï/óÜÂz´sŠÿcö?–Š£ñW_Òþ·¹¹½µ™»ÿ±³µ¾ÿñe>Hë¹´æ�‡ÌD×}Ѳ¬”5¡ ʾ7Ò¿´@ Ceq_X²am^y±½À,lÀŒm/ð ÆTÞ$¹´¤&7IàÇ*3»X<ßm{ËÞL5“0ôû®Ÿà»M{ßÍ>Ž)Ÿ4Bͬ¸F%j”ÖDå[¨ÿZPá\ÐâÉÁd@Å dÀº­®µK≾'ö¥.oF)S‡û|K~ yÕµ‰w¾°ø]¢�,‹õ } #’X1*:Š)f�Ò�&O>5¢‰ g‚ø:wÝ *¶°Å}ul;Æ³Ž ðcH=1b ÄÅAÙɇsæà£üIJԯE¬¿+Ÿ”3»j§žû�T…J–õWØÔS)ìù¹]dÇn©%{¼ºFš-w Ú&>tڗ♨µb¯Î–›¤ß(P©uµ ÉÞ Y‹:¬ÑOTBö9b·›x‰ïv»\Pöª_*(à5½¦wu™˜o^ª&0´Y×,ÎÜ VIq+uMõº,ϳ¦›“¿Us ¿èWóÈÓ�p´pýŠªJòéHºýÒõÚm Þ7w‹ïN%µñUîïºcÖ^á¹c»‹ãÎmåÉÉæN…¤ÛÎ�Ë’×έà–8í˜uV;mܽ$9Ôܳ¸½{ß {÷«°uÏâvû¾îÝÃö}+ìܯÂRW�Om=uC“«˜Ï€\XÂNjÚüÅ–G‚qE…–€IÍBÐúãý_SH¸xäŽ1n ì³]]³cpT**­Ëû"Û…ÍÏ;hŠ©d*ªÝb%õ&_íÒÀ†³ �;33-�ˆíªínÛêv—4ù›ÄD~ïØÏ?hXuëßHþ'ÃâÆã÷ArÿÎÎRýÅå\ü‡½­Í¯ÄÎZþÿæÿí|ïñÁÕñ?6ÛíÖÅÿioííµ7á=(�­­µþ·ÖÿÖúß¿¿þ×íŽæx,Þí*ýÏéÃf ãìªd³…Ï}fˆ]ËoY˜½&º ¢àÁ�Ÿê�0RTôÏ%aåf¤›¿þ•Ñ9ó2„…¹éGtXÕÅðÚN2˜t“t¬¾,Y›†ÁGwAo¤.¹P|H»?{À{:27ªùδ?tÐ îˆã0pùΘќ *"Æ‹®•‚ÖÀ„Æ(Vdû«+uʳÀµRRBi žL6’p—UåR–æ`sنܭ¤Ì@c¹&.M$ °C7îºA^Mb„V*Ï‚9¬®ë#‡7^ROïÐæ æô)FNšrøóÚûßeÿ—òþãÈ·Ù··pÿßA` Šíáþ¿×^ûÿûÿŽ8‚�é£"\oÿëíÿþÛa÷.nÏ’½öucÃAǯ.ɪ�˜N)»¸}=Æv¬˜³(ÄÌU QM»¨6Ê:ææ”?0€Qe6:ònÈ%xcˆ) )j“½¤ªÒ¤òqŒ@Ë>Oùï+°Þ6U«ûêKjj�Î]û8¢ô©:ƒÝW¸ˆ“y¿FMT pTêiÅìj®Uì3ÊÒczʃN"#L ÇÆ9«Udzqp•âZ®ò£“Ã( #£ ˆ',¦Àä.tÕjÆáš8ð¯ÂB½T¶I§d™„ƒÝd¥Md]d:]å}¸&¹?É!•¨ù«>€îÊɮ釀qò2ÍtðùHRΑ&4��w½¸ ¤…¾WJðþ�›|¿jÞ=è3–âb_{EŸïÊ’ÚfÍšþñ�¨b¥ê„>²DþO�\A¸Åþ·µ¹ÛÖößöÈÿíÍ�ݽµü¿–ÿ×òÿ¿½üŸ‘ëS¶ani°»võí™.&BÐ7ØÖ({wtåÕÆ†» lSé¹a5S»Ú�ÝZqjâÒ°ão™ê¨*÷˜ßxÃHæ3Ÿ¼K ~v–~…]5€Ñ'è è ù}Š»á¨Ëõjø8³³«ö`¯åkÜXΞÆ�ï� Õœ‰‡ßªËÆ*p²#'­¯zvaåtG¾ƒYßW y&ƒÀÀ˜?Te˜êå—£îÆ¥º¯Ëënw„jŸ\¼><;}spñêäìíï>€e—è²ÅóPø×6Óÿù�Ïéøùook·••ÿZ{»Ûíµü÷;ÈÛkùo-ÿ=¶ýWYyÃXŸÉº7ÉuäÌî~°«OIu/|Ž»Â¶l½}xø¦ûêèïïÎK,%Š÷íØ›ÍÙ¢ Ün«„ðÈ öõįšb ÇÜà d8ˆ%“$;BgL3¸“K:ŽÂù,S�KKGº�QBÇ•»ÅNÎú£mPKéê÷�èÁ]ǃ…ú¾TÖ6y dŽ;ÀWlgi™¥(ÊŽãÒ€Rhöåõñâ\z£nšSó;L¡‚}9{ð#/¢ÏCw¡.©îØû…ÁÇtE¸&q�Öª}¨rÏÝ.&æª^Ö‹°ë^à%Ý.aR=dçIªû ¨ÑšwÉgÿÊñ½aþŸ„ÿçøs—ŒÀ†OÂò�}mGý*ºP”v?”! D®“#›‡`@?d�»jqžå{ ÈgjA¦XÌ­1.WýÎØ»¤¡Ž¼q7ô�¿Ñ•èZ2� =…By㸺�Ž#ö?B/¨ÅèÕÂÅ¢b+ï¥ÂÄLÙ÷ãélÑÌÒ-nz¸®Ô1Ÿéh–ÎëhfãíÖÔ&c)ŽT­R©d�û�tí¨é‡c/¸ÌÅÿçX}Ùpú2ŽC–]ê>\©Ô¥…:s>!g‚ÿÔhøûôoÃPóö+8˜Jƒ"]°!\GÁÈÙų£¬£ “?ËíõÙ�~!Rušq¤†}…áø£7ëª(û¯`;Ïêýè.haá,ÀÂL¯È,Šl<äø°yIVö¬[õ[”7@ô¨bwU ÌÆ)d´/ ÑŒ¢Ž"<Ö)L"ƒÓE(€zãìÌ®gïóÌñ¡ûNUZ›¯îfÿ‘ÒÆ—ðÿßÝÙÝ&ÿÿ�ÝööÎ^í?;íõýïµÿÿÚ´öÿÏY�H×(˜‘–›�ˆ‘©v_¾:x÷æ¢kFÓKUþŽb5ÊÒÝ+TË•¢OÑ„>‹&£Ó¬Câi“ÕÔ¼°3+[•ë>xÛ†�?KU ù^ý,Ó†äþo NŠÂ¨ WCIêOä•N¥#Á»’©ËÝ•`CÙŠ‚;ITÁªÊèßi¹ßô±\©6„]�ë;*XÏå}çR*œäõ,ÐD±¦ƒO ƒ)!>™~'Ì-<&N²ooÁDê<ÙeЕe“4à|‘�øEÚŸú »>þ­uÙ¡º+ÍÂ…6»X*�XÝùް]°zƒzµèöºtZÊAúÌÓTUÌ.2K'¬jqµt꾓Öí©}FÐ H–¿HGÞZuà{,½ ­˜ÇŒß>ºd<†¡�é»2+w�Ô A­@‘_Ó5Ù­ 9¾ßåšš‡R†/I�-‰¿R(Ê`ÜÒ*iå–zú»¬ZÇx ºÎ s5�eëlhÄx‰’†¶Škc Ư{@K9ðiKµmýôu¶ð%ôÿÖîù´¶7w6wZ˜ÿeo{{}ÿoíÿ»Vÿÿoø�4d®…œÞ®w6)ÔhsSqÃîí¾Ñ0«9~Ò¼‡çÇ%ï$î�ƒá‘˜Öd´¹ts˜GÅ~oàM9sࣼa´863 Ëõɬñ @Ë6•ÐV­ÏÁÿéXùñyÌCâÿÀ6°Žÿó¥÷ÿ¼ËÂ#Îÿªø?­½-=ÿÛ-Œÿº·‰ñŸÖûÿÚþ¿Öö� º]¼aÝʃ¤¢ã©bì#6*ÿ¤¦üUGVåŸ*~© «Ã•òk•ÔÚ  u„Cv �ÈHˆ–Ñ;>}ú¾ôcòä<Šž2)êDŠØŒµWf© z@Xœ7=Vç™5,8ÎÁpîø”‰è0ŒbìZŽë ©Þw1Õ³½�Ö(Á+>¶eà‹Q±œµTÖ'ñÿ®û¿öNü2ûk§…÷q÷ßBgÚÿ[ëû_fÿÿ¯�ymô½`S°³›µ– ÖRÁÇ+€"Ì›WE|OÕQAç¤*Žœ­–UßuX: ïDã«­ù~{#€�Âot)²„â‹®ŽÅ1G_�Ûü£‹(áÿ:Û×£ÚWÅÛÜãüß;;ð~‹ì¿­µý÷÷Ñÿ.0ÉÁ¹ëÀò·n1sâ^\“âÚAVã�e¶a¿¸9È€ã:Tÿ«p>ä\±z/Akto‹#¡#j #g€’4fíÁ« 2a›Ž@ƒ nŸ¤‰êtÀrtŒGÇ!Þ¦aœˆ§#'¾h>ç"ЈLAaéÆ(‡rëÈ�Eáp>€g”û�ö¤þ3Þ¸œ¥Zî4±Jéí;ÀÖ&3gˆé 7dÛßÀ»�ˆ<;{›¶âcx�¥xW¹›²š:Ä’�x¨Üp+Qƒ\+ ¸ž<� J �I’�ë0áì¹CYý 3e@a[â�{6Òu¬�[¼æAæiç ï Öà<�41 ]V—d!¼çm´¤EXœ(@°žÊÜJ6Éb* ¨ÁßkL) !ZWN`)MÔe5 [tŸV$lDã9ZNéMm…ã}y¤hV±QvdÙÉ•fS‚k8º;´ªö+Å7*Ðnå)uT�ÕœbQƒ‰­WÔD–䤸d L¹èÞad*!ˆ|xˉ÷KÐktqí­c0£é‰ëÏöõœœ¿;==;-pvøö˜ïáY#—˜ãHËŽév3Åp™3¦d°¨ŠotiÓýh£ň¦ ŸCL-‘ÿ$X�*ÿÝÏÿkwí¿kÿ¯ßEþWóÿhŽ`·�ÿ¶·w³ç¿íÍݭݵü¿–ÿ×òÿZþGùí§ó;ð%ª|nþ¿³»·��ÿ¿·µ>ÿý=øÿúþÏšý?:ûϳóÂ6°Ì¼“ ÷‘óóQý11Ø1þ2 9ú€™ËA¬ÌRé­^U0 ¾^Ò#]Í—ÖÅ%kú8m¬¡/Þ§ñùth¾L¼=iVà'Ä‘â/áÅÓ´µ.EÝ’w˜�ºf�u£™æeåKÝh€Z¾ïýâšMÏ#¿fäú)V7Õ]š½ZåLŽœhG¢ȯ›¿iSBYKêÔΈI�ŽV=¬¥ˆã6”j¯ã¤uÔÚ‰b®£Rw©ÉÝ7浸²“Í,;¹£�ÇasÉPEL;“Wx@•�}X­ ¢RÛµ‡œ—1ǘ2©ÚÚÀ¦ºJ Z:¢\6s€¦Éå1Û4µJj¬×s¸²~»µtðØ�íaÜÓ¡‡©¨ÊF|&ß½tºêœrMŽOo§³„ù©j¿W~m!JæDR<ýhçb×UÅA"³m»Z_Enw3^Ñà&®<.þPÁ¤_ØmJ¬ôžÆ‰ÁP»£¯V7Ý ÉBÓ¹·±O3�»Yý"¼˜Ðl#L 8s?QE³6®ÊÅÄ5—¡ž{&’3VpM6”�‹)*OÕ«z\¯ÜÝæÖœ3Ô:Blšx™C b�ü�dawâût>ãÎKƒ¤�kÆr{ç(žÜ§ûw/×Yiïò‹>4„ -7¨¨÷é»É+¢iÔËÊÿn¨&e`ÉwjW"¦ ÂDÝIW•lÒ¸+'Ú/yÅ}#»â°×o0×.ÛõM2U~•;bcY$^LÞȳ*€’DR´SKFZÒ‰@ ’ é2q~5»ýúk—ì,%÷P‰(RBIëj7âʯÈ@»]räëvµ‡Ûo�oÒ=~ìÃÿÉãx…þ÷h9@Vë-t‚Ìë[{kûßÚþ·V×ö¿•çÿY�颲hïÛ¿¯ž¸,‰}—r£T1œ`Q‘(„³Æß¼@øGg[Ê1ç�&_Û …y®*È‹Nò]'òX)Å­¢¦Áñ*dÈÿ¼çÐÙlÌQûè.öeJ㛎h¶°½›B÷äj(6µ¼l6H0п^Þe¤§³òàÔ¹X½€Ót¬åîˆ:oÖÉP6 ,`†.ÏÙL½†ª +ynº\Hý‹úzઔ³B\¨�qæ‰øLŸpDêY]ìð‰8¢ žàb›¢KÞO�Ó«ñ„@¯²£b]+MhQ+æyá\}"‘ÉÖBâ(À(ÏÙººW+â©j¡î¤ŸkY=ƒ{í�ÕO…9OV~¤Ó9SºIËÖ„¬¨Ô«²Àá¹Ì ‘UÄ�üÈÊ5ÚL̇ë1ZZráÔYA•ÓÓøèZ ÏΪ‡†Ê6‚·ã¥±íÍMâ¤yU“ÊršÈKïv‘šŸÉ|[Kk|0Úºl˜-§›‰ëË>Ë›Ç75ƒ©UnŒ²hï”ÜPH��ëûùXü¥mãÎ{íâ•-àWÚäOâI8uÇAs"°™�ƒúIN]Þ¥c×�ÆFs ¹~\Ø$#ô݉så…óH]æºvaOTŒÈãT?1”ÅÆ@¶ž»C£5Ð40ˆ"p¯ˆ!¡ÀÂÞ›°Òêº�…(7\ƒèÜ„-íXc›Ó¼Ü|{îý͸ҊP-ša݈{CnAžá‘ƒ6öúââ”SáJ*S<ïètË5ƒÏ¼Pr‹@¢éœ¹ŸçPz†BLEx ^â샊C†HS ©ülóKÇ\àõ%bnf•´ÓeÓþ(�-Å16;@¡÷Lú'¿ç›9HÂV÷q¡§€£ßÈ–fa( ±ìláÓÍç<_ÞxÎýêÓš8k8¨¡GJÃ'ažSÎ-`â÷2%#LMÃø�AZÚXÚ÷§?œ©È™âž8dBë}ïÐå1º´(X‚z™…�p²>ÂXaÜ�ö�Y7k¿}í­\¯ Ü É‘åbÀ©í;!°µ¬ z`‡FùÔ�½òMÙ –-E[aÆLöQ §ÙDòg…Ÿ‡õ) n@l·E•Eσ"Ø~v íhñ2%(ª.ÿ§œÿÈ•žþ^ï|$�/|ôAjŠç?©™ô Åi·¶åýÏ­Í�í½Šÿ²³Îÿµöÿ[ÿü[ÿ¨¨3•Qs;)ÐW*•Cëvðže¢6Æ€ä˜ÕÉ8ô¯°¾QH*9è¾±§á˪QÒ­Ñé^‹Ó“ó UH8#<àw¤ d# þ®PtqHJ`ᘠÁ„ ‘‡ŠOƒ|HSDâ58;’=©j~xMµš�üÂQQÆü¶¨Fæ^Og`’ ÔÔVÓæ*�ç¼?Y)š• ‰Ö¢Dð+��%ªliɦÊqJ|Îídv€ÿÂÀ—Òóõm°¬÷Ëah G+¢Å¬ýìÿ8þÿj/Äêývþ-ŠÿÝÚÚiÃŒÿ½»»³Žÿ½Þÿ×ûÿ†ÿ¿¬8qâ‰ï•…À›÷a¸q1ò]i ÓeƒÝd1ÊÒ‘yëéA¿÷Œ^`Y/�Î/º?�¨¤`•>Åž¢ö*nª‘yÃmá;n&óÖ�á� Ë÷ýKO¿�I Oß�ó»ß$‡¿z¥ù× �ËE' ’l ô=×x%aÒ/ $õ˜ Ýÿ¥�/cÃ,À/Æ¿ŸÿâÍ̇¿å¥<ºª…ý€h#ÅŠš!ïSÔ²†(f5´cgCÆ"V1¸p²˜™uŠädœõ稀i7§é£Ú¢§US&­}\öµŽ<¹¯aÎÈÂOéÍÅAààä ðk¶@ìŒ\}²— ¢ßÔvÉhäaœ�‡,ò¾U4ÍTKkå0XÄjyíÌ9!ÔÃp‡éÄt¸ÓÅ%/äÊ·áQÍ€{â´ó%ðÙήQ(�6–S%ê³oªsžÒÑŒƒ�moþe7{Vr=Á�I–,Æ-SPÛóŧ“‹á×4ô·–\UŽÞ ã¡7F�f?…bâÞðÃZ~¾±{]>Ƭ@5¾£e‹žáP¯Óì™óÀ�KÖjÆáìð&ÁÄÙ˜¬Úx–�¢IÁ%oÐ3�höÔ«Ãè7S—&h2ëþ픣C`Ü)ÿ@]].ñÃOPÿÑZ9ï®WïÔü‚œý”ÌvÚãªPëüÈ0qJ<0âÐägl/Ù“Æ�ãa�W¼4[âX�íÕL?EM ©²gúyˆäF¹¼5œ¢»·¦6ׇ´—ŸÉLµ,S’]@ñµ%›Sè. ¸ž†ÖU´ßEÿ ´~G‹|¤]Iš™M$=Qà%›uô‰<Ä->ß:ɦ•)F³ûD.ōРÔa<éˆL¹ŽFóË6 ¶%Õ‘ú�OeºH{*Ù ¦ÇX3ƒ jQ£47„üó 2)‹*-æ M©Û�á%YL?Èäô*ªÿ*-ÂùT²ùY®8šäQ™qu«é“¥E³­çŸçªI•G•–?s…†n<ˆ<2#©‚Æ£\á�î�cURýÎÓ€ï$ÈõT1õ;_ŽvPG`þTQãQP`¬h>Co3 ©ñ¬0ý3é1Çošâ›\ò ³“ó“¢÷cU.}R ò÷h8[–ÛUÈIšñY1ã�Ýo[¨2’—İàúDè ¤Xº3?\¸CÔ³ô),ì¹xP%ú.Ú%ñнLLŠœž_‚b’u²™„MŠ›Ë£°så*,|åF�yXøéá©ØjmçèƒM’zÒÕïF>×2©äº˜ú�+¢¾é&i9ý Í[%™´ìv$TU��L°ô³FùºRwË ƒ.2S#ûtÉð352—U@‡ë8j¡’z±¬"óêB5~l䢶 ±á³¢þÒðæˆüÕLáê¥Ú 3� [2m zŸ-qC$�Rÿ&Áåªü³ÜU�{  VÑÇä\W8ÛËñŠÀpHÆž�[5ck…¥ V­º<ÙžK5ªZql ¬*#)‚ ;VjåÝ+ 'TöPÃ!I÷4}ò\Ðû 73„L¿›þ›•fs´4˜°OSÝÄ{Ñ<ÓÐ7ÐTG>Ÿün*¦;è’âN¥‘Ul µH[‘캯w¡]¿¦êçÕ­rË«Ñ�¥jò$¦tÑþo¸é}™üß;»­ÍìýÏÖÞÎ:ÿËÚþ¿¶ÿÿ‡ÅÿÑaš©eõ«‹ñyA�MìéÜO<Êç#+¼Uì!²,ëÇßޟœ½<ï^œt�O.º¯Þ\\“Œ‘Ô>T2<ý¥²[¡¤_Ê¡A_ñ¸Í‚�½Szù#g®†b™Û X/gºsci›ÐÁ‹å£‚‘�“š‰ïÒ«'9;]猙f2!_¾CŸpo�3 â" ˜¤Kšw¤ÆI7u“ù ÖQ Í«R¨x¤W<• Ôdž¸BYŠn9¡åz™Ýo¡b¡eÓF¥WèÂTLÁ¼�µÔ¨¯†¨!Ð A ×ë%ÖClu�´–B›ó®/O²A··`45Õ*Ï�±¼+¬lôdˆÁª¼™ÑI† `ª,Þ=�v%u¸_f¦ªg +«uÖUé°o+ýÌûÀ*EÍ( ¡oXk*-£Ðo† UHÊ7—\lXF\DÊw¢ÏÔ4Èl*ç9IÍl¼ž¿{”Y5°�s·2Ô:otŸ}Ùcö yé›9qÞ�˜}séJËþ¯Õ̯š‹™[í¨!Ø’�uÑ$÷[Ã*»Å{ŸKlgT·:âpðÑ5ìt{Dq‹�â»”0¥cìg&Ë'Òåz)­RV¤ŒÇp†`ïH¯'¸<ɧ·ïCðff.y‡3¯RS<ºPÛ`ñªU)X è:•’¢y#$nÒ)Çu¦•z¶�<Ǽëº+Üœ¾}ñ-_€«áâ§,ÆÜ1޹`þ}ô?¾û˜€n‰ÿƒÞÞ9ýJ¯ý¿×ñÖ à:þO&þOê–‹öC-RÊÈMÈóØŒ¤oìȇ|)Œ/ Y’�è™v;t}5rÆS§ƒ�sˆ(³þ ªÏwRqçÍ´—öFƒ„͈^è!Êß ü68ªQ ™ÐæöVpö'Þ”¦Ç@ç2ÔÁÊ ’ÖC.Þã¾;äåj4$5Ÿ´\¦CRGê"µ‘šmÉ2øÒ¬ xè M£K‘!�-•ú6¢ÂÙH¡"ò�ê¬(Ð}PŽüéø~Vp±ñBSZ¤2\?6qªt×â’u°á,Äêú©7JÑ[îÕ£=~e)Ko8’f‹ÛMfçà¥Lv¤B¹áê Gj].P“årgÇý䀓�â%dŒé¥(‘�Û º™ÇNà% U�w_íÈ�é}}t"�"}™"[ÿ¤b«µk¥ÑVùÚǾ¨I•„¢‚úø ÕG±™q#*d³}´V�'£o«ö¤xTpÏñkRƒ·å_Å}öù ÑŒö‡w' -PûEöÆûFµ­ªêÕ†�D=WûÅ&Sí·!ª‡Þ{^àl¨†:¢ZoXuËŒ*ø@ØTõ%°åéÇ„F½SÐÜËþK~û_Ðþ»¹³µEößÖöæÎæNkí¿[{ëüïkûïÚþ»¶ÿ.±ÿF:ü/ÞŒùœÁõœŒ„G'ªMõ{µ‘µë…… ¥¼�«Þš7¼2ìÌÝ/YÚ¼v…öÙ·a<ž²Š§>é¸xäÝËlÄ~}xðòð¬{pqqvÞmw7)úð²÷­n{ym}z^�Î*±²<ù¶ÒàÜ]r8Ž\”fh3åí­ÿ©ýíôî~@HÏÕ÷çö7«×šøÿ<Äßõ¿¥jV�_öçž?Ä×_ãkzDWˆ¨>ýtú^ú¯lÐ/ÝÎÏxú_?Þ›”nþÏSC öÿ;<ûþäü0uÛ5sFêëö ÊùÛFû(E>úUäe®Ëæ]±Ü•7óQ¶‚Ëw2�iÇ.�`©KÁ‘dQ}¡-ïÆÓO„¿/2´`Sû—ð¼œ6aS¼÷_«ÔMï%é"bZ:ÿ–‚u˜mÔÄéAaõ¼e0Ÿ*àŸAý6þãV!¬Kl´#žÆU ’õ2wàAzµŠXÍ5ìDƒ‰w…“,”ýÿy3ºO�eœÔ(€þ¾ªBâ6ÚÙk9ÿ…*š‚ZIÊëª-Bm>ó­7[ hìçˬj$êkJ26-z@Þ/h3¿‘öõêF•áßàÊcLVß^¼<¸8¨âã›Ë¬#Õ`Ûë/èôA7¶jj0ÓµPúoŒÆ¸³Ë,Vñ-�¡™åæCë2-/›½,1 ¾çÑ(Ðv6ªö?B/�Ç ež$ÄÕ„èºÅ²0Î~U­Ã¦”–ªÊé±Ü1мö’f™ pŠ¥”–õÞ\JÛ ÝØ¹¬YœRVP£XݪÉyuÂ.åF5Ž€A¬¦ÎÇ’yg§ºë¨}´–Ùl0¬wè’:Hµê9Sf¦4·>šå8¢qQ™a<–ÑÜäsV_‡�zˆÿ�¼iýØñŸ–ë[­-¼ÿ±½ÝÚjïµöÈÿgow­ÿ}‘Ï¿—OAZ’j¹xUì¯g‹TžüyøÍÏ6üSÿCäü¹bH„®ç÷’ ¹nŠâ\{¦@Wæ`›¢[r#’÷©Š,*«d .6ý¿+U­…Àû �Kœ\òIÆÜñ˜´»J-æ«Óh¿:ÁD8káñ³ � Õz:–‰‘kÉê*ÿé™ÝxTûÿÞÎÎ2ù�Ä%åÿMqB[»Û›»_‰�µü÷{ÎÿÐÅ»"n0X€|Œíä&ùò?ÞÈλ½Žÿþ…>kF½æÿKÖ¿€@×��“<|íßeý··rü¿ÝÚÝÛ[¯ÿ/ñù0ŒAÔeKZ|iqZ­} W%éàËú ò ó�]wØUéâ/- ^ÕU¯tqnDf])”âç²£õÊüÝ׿ö?÷ý¿�ÖÎ^NþÛC7�õúÿŸ¼^Ù-»e“+уe¼"±Î9Ž"z!ú¾t'G”LÕKè2:" /’ÃI-È›Ó\Z¯Ã©ÛÄØŠ�ÕNhÖEMìd}Q8mp€†ÈáœÝÙZF±¬Ñ¤(ˆ1¤zßÅTÏöBKz,tV9®X/ÓÓ5v¥ ïãGÿÒ_.8Ù{,9þEaô==|ñlÕ](ùó^×.vóbEèj¹@ŸX .&Ýb#£H&‹nI#úËûÉBœs.Ñ#ôÄ¥4Ó¯›¥Ÿ’±c ToLñ…9“µL¬Æ¨C¬PÂ£æ‘ `›iébò ‚*D:.f@ÅLœçÒ×ð„€ÓÔ’2Fs¯ÇHÄÈ.ó`T¢£×=D“q=`üÅI8›¹*"öó“ÒŸ�3ŽèäÙj·Û»0)hå8¥wºÍ¶½gÿå"J~#¶ìö72WëÔuȇf‘xSv�GÄ,‡7{ÃCÈùèl#ÖÁf±!÷fÆnÖëPÞ›JóÿQ¾ÙÔÖçÄ>ðF#whK²¥Ì9DMÃoγyÞˆn1@‚LPƒ½¦a`ûÇÙtüôØ()!½s¦nÈäK7ìõÜÏ`f€x9žbÚ¹´µ“MƒÈ6,‡�&ò%Z„—Õ+×Ýb�ŒbĤòÖö‚+™;63¥ª´õ¡i]õ±rŠëŒ‚M¾mØ[„þU2SN­ðx†ñSëfï#æ âæ2ËQúà2ܾb£^)j)äc–sø)2á{)*^¤LÂÔŠ›÷YOQ‘úîôzföU¡¤ˆM»%”<Â{½ºÄ§ {�›vRØÄ±2L§ Q´˜)úÈ4ŠÙ$rе€§g^‚ç4H˜š4#¬B"4y XøW| } ‹Í÷±C1æ\ä¨>&]Ž•˜K¨×Ã2± EÓÏÔ¡Ãó^oÏ}år‚ì—-mâžÄË‘ùà ÍöŒêÂHŸ./ËIÿÅx½RJ«É¬Žˆc¾Ö#Nö]7P+£Á4EôërvÆpd˜�³™Û™p ª›â¼¸[Á¤È / *ˆ§ƒ$¡} âú*œâ‘€—%ÌÕ{–mSçfß;Lô<)–ÂÏŸÅÌ› �»È Å/ïb3¸ý’Î[¶xÁ¼Žr™eq'Y_0׎a¥ô9^Ii:„˜½X±mKê�þ·8æO„"Ã#°ï�¯‹E·l”óÜÿZ�_¾Ü˜’ÿÿòç�kN2oç1I©™"š“Kø'é=Nüã?ž8zyx|qtA�æâÝùáÙñÁÛÃË[ó–hÎÄéÁù95úñâäí[hëöª4¥è_Û¶/ËÇI×Ôøf¨Njš?2¢& Ÿä�½S–vôâ¾_nØ;5«óÇ®Î[Ž¥pvŽš“,lÌ�›ÍñAyþ`;£7S7F¡-Ó®æÛ0i›3.>§/î4Ú;å#ÏwcŸ´uŸÕÂóM¤„ŽméÔÚêá�`¿%)w¾Ët5a—ZäU{%çΑ.RB‹Úêá��¸g’î<š; *Ø»|tg;�™Ž[¼:zsXXK«º^žŽ{÷?Óy½ »|±ð×¢wtöBku×n€7=Gˆ5°3àÕÛ¿Á©üxÿi{ ­ à#Ùo; X4›©[7;úÁK^Ïûæœ=[i×0û)Ñò•=Ô;WË3 R¼��f®w0ð Co›ûÿ[ç#ÞïîGN0˜€¨7BËÎÔáôžÁ�(ÇKX G$ŽeîUC1�ÝülÎÂLB9Z‰´@]o 5×$˜¦8ä‹,¬"SAÒðb€pÆ%hC1L'nÈ)Æ1HŽ£Œ"ÁQÚP¬Å”ªJŽs‡ä;¢ûMX�‹_e+¿q·¤Ñé^³-êË3Z#wí±Ý0[kï¡B™�¶¶Må€F,ïvF€©´ ;ûÖ@‘�¤m[š"¡Ÿ8Zž(Ƽä–P^‰:xwñúäì\n}*�£•uqA¡»×CcŸxF¦³ï†!šß‘`ž º¹Þë5P�XKêõäC�A~yV©öÑt¸;6ˆÿ'èêt“x&óòJ�%u)’+’e*ïÓWQ)ýH\CF#ºW®Έ¥¹Á•…}§OHïl7‰Ùä%Í éÄB…dî€wE$Ž,T*47 L� ƒ&+´×YaÄHé+×5ÞçáúÎÉ2C[±â!PÙ"$<µ'ÊžY2• *ª1I½}ÎŽÌ™�ÑCð#&QÁ…šv…ážáHq¾±Et2£4¿|Ó˜©‰/æóœŸ.Nò£(µS‚ôÝ-+œš{ñJ2"ÛÃãÅ í€É‰ìá·,¾‘¶|*´tÄ»ã�OÞ[/t~˜h€ :( Å(�ÊDþ—L0ôLYuJ€œ%f˜¬ªv´Mø\ÞåSE3Õ�Q¢ùð�º5õƒ1àn’)w;4Qç”I˽u'çé—¿ßZ%Á;–Â/ߟ¿¼{á7^0¿¹do…x¹¼÷‚ax�EçiŽ#v¨fâDIï^¿µï[ÞÞ½w�½{ÖØºoy»}ï÷ïcûÞ5vîYãh:ó]䟞¼xМÛΰXëþ�ÎUúŸOòý¸‹ÿG«�÷ÿØÜ[û|™�ºÖü|´‰MK%Àßm{ËxÒTYðÕ¦½c·,².RX ~¶Å×+êÿÌú??ywöâðü“—ÿm뿵µÙίÿöîîzý‰�Ô-Ãz{p|ôêðüÂöëìðàåÛC;Š^êö`4¶ô¡FnÛ@‹™‰èÛüpLuøQŒtiŽå¦ßM¡²Ýwä¯.çÙ°Ýé,YXd=ØÀ»Ý%Ì'*"}öi&s•ù‚­šù‡2ιùLža'¸ uñË|ÂîpÆm\HŸ)€éÇ› ¯ ýÌ„¿˜tËÊb·²qy¬ü5Mk‰ß–µ|=[wpõ¶nqµVˆ ùwI8ëú¨˜¥/7”ß_ –õ+å8XòJÏ隇&þŸ™´Ï)ÿÁÛ¼ÿokkíÿ÷åæ-µ­?ëÏú³þ¬?ëÏú³þ¬?ëÏú³þ¬?ëÏú³þ¬?ëÏú³þ¬?ëÏú³þ¬?ëÏú³þ¬?ëÏúóÿ�q¦0././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/fixtures/twine-4.0.2-py3-none-any.whl0000644000175100017510000010705215231700043022061 0ustar00runnerrunnerPKÝ �Uóä‹’?twine/__init__.py�SMoÛ0 ½ëWꥻ-¶bݰCÖÌX‘ Mº¡C±G˜,y’7ÿ~´b6ºC}±ýøD>>Rœó•©¦ w¨ 4y­6ÆÂª‘#ÆV[„Ìh�Ú;0ð[é ÙQ ‹ �U½V2ƒÙ÷$‚{:^ åè…Tn‘m½¯ÜÇ8.¤ßÖë(3e\í+û¶P,�«ÑÅ×ï@èÞD¾ºz1Î9;�Sí­,¶.Ï/>À­ÑBå°ôõfãCB©s¹“y-TèÆÊuí�uì„?È µÃj�£¥fÔ!½ºÈ~ uÒh¸ŒÎá´%ð.ÄÏ>Q†½©¡ûàEíð`ÒF’•ø’aå©<•-+%…Îê*”é’D”â¹KaÖä™&G3jé`ø‘ÂÁ½;MÓD"(�Œ-bu`¹ø!¹¹›/捻6ðŸ´BçÀâßZZês½Q‘˜L¬I¢ ÐÄDa‘bÞ´b+½ÔMÎl|Cc¦,¹tßFNõÒ¨á!�¼øl ɒ×Ù2YN(ÇÏdõuñ´‚Ÿ³ÇÇÙ|•Ü-añ7‹ùm²Jsú»‡Ùü¾%óÛ ùDe𥲭~)[1o ["Ž´KÛþ» 3¹¡]TBu»¥…Ù¡ÕÔThKZš¤k·‚²(YJ/|@^5±4J¥)|†Sôð4õÒ+LS>éW—¥°û!T[9üݶg‰Úo�"XÒ]Ý8‡PÖ¯yÏA¤’�.ÂõÛ/gŒ¼5ÖÃá¥ä:-éúæÂ Æú/ªð:õ§<\ONºØÑ':ÒÇq-Jä¿ÙÀ³Q¸C#X8ŠnM‰ÓŠâGOGœ ŒÞâá†xgøÂÓ ¤ãF´%Æ?PKÝ �U\¨¦åÃtwine/__main__.py�SÛnÚ@}÷WLÍ‹‘ˆÉå�*•hBÔª˜4ŠÒÈZì±YÉìnw×8(âß;ë �¤ªê—dgΜ9sfè}–F—\ Ql@míJŠ ¯WRm5ÏWÎOÏ.àZ V¤Ù2ˬ×#À-OPL¡)j°+„±b ýi3ø‰Úp)à<<…Àü6å÷?ÃV–°f[ÒBi�(¸�Œø’ ²À$r­ ÎD‚Pq»ªÛ´$!Q<¶ri¡á½²C°FðÊZeFÃaUU!«•†RçâA™áíôj2‹&'¤¶Æß‹��¿K®iÎå˜"1 [’Ä‚U 5°\#å¬tb+Í-ùŒÌlÅ4KÊ�Õ|YÚ#§:i4ð!€¼büqÓȇ¯ãh ˆãaº¸™ß/àa|w7ž-¦“æwp5Ÿ]OÓùŒ^ß`<{„ïÓÙõ�|¢6ø¢´ÓO"¹óSgX„x$ “� £0áOh.‘—,GÈåµ q@¡^sã6iH^J,_sËlù0TèQ?©mmx÷!󜸺§Ù/Ór v«\‹6<[¯ƒ8ßÑXãµÀŠ ìpIÁÿm®Æ‰ò<×�d]v�Ãím âX°5Æqßó¼3º.‚>œ|qíGÐ׃‰0¥vV1 ¬(µ–šÆ§XM�’ϰDr�NLçŠiƒu5© )2žE,K«JP7—³ºmᾚ”4º:Ål² Èš�è6Og£ç~ m �!áÍbñcR—2ã’ ºÄ}ÐЦJ'2EJ>¤»PÒmê õ®VšWà¶X·ŒêDpPÓÔ¾¶q=¬eû¨û2ÿÕuŽã¤`ÆÄqØma7‚×ÊÝþÕPïÀÇSïýõhŒR»_â=ð£‘)vo #oßN'\¸“štïÿ³øhîŽê2´]ótú¼óÛ›ÐhK-fºIžAW——àDZ»Ð8öî>ð…Û ¹Û¾÷PKÝ �UZ(N¹c9 twine/auth.pyµVM‹Û0½ûW÷bƒkh¡CÊ–e ¥ËÞB^GNDËHrRSúß;’,ÉŠ�nºes‰,�fÞ¼Í =tŒKT÷m%kDDÍÆŽÈ®î³a»mwQÍÙÉ¡ƒ5�n˦)Ÿ’¡¯�¤¬-› =|W¥�‘Õðƒ \iUœhK¬ò³"úªX8ì%T‘@8ZY$9ü¬÷ŒÛò@0N£(ª�n9Ù’VÒ²¹o»^‚ß–ÔcÚR‰q¢wÔO�¦ÎP/WJ çÃZH¾s_X ž(*NŒo—ϵ²½ý¨¿Š@wnUƒ¬]†V7Ø¥sä�Ö _ðÀ¯X[Ó]aXÊHÇ•Œ·z?CTûNÈ%¬F1‹ðP«‚3ýé³�ò@äžmÂjϘ IÕe^^V’�Ú'È/mYeÇ:¶¾Å�‚ÙóÁ]DëémDAÐ7N�¥$£ñ޳Žp9˜/—ÁyÃ{\•Õž$©e©×´iA g Ÿ�cXÝTqÁDzé‰Oœ�æl.`È à5¬b{'ÏÀ¯C'1à_wÃ*H#¬^fi¯!};6ï^�kà_رw®aÇʾŒE�„$‡k ˜ _Çܽºx9…–�YОµ!ùP„zh–5·—_¿7O½±EMœfAdèÖ"ýnÆX,Y£“ÎcÚyêý¦”^ð2Ä8ÿ—“=ðßÍU—ýwš¯óß�Póß÷;èæãÿ” ïp†Nûæ(�×{v*ÜÔ»ÎóŠ”…„•*Ã\l\ð¿jNŸýÆ ­Ø›ðë’@P³‚Þ¯ha«K–ó-SÚ±Òš–­…ßb(ŠÅ:/¤à*¶.ó×ÔFB4ñ¹6¡WŽ£4GùOë¶ã•Ùs…½7›MȽ§¡6éPVRvøqz=™-&ôÖËß) Ö2_Kað�«-ã:óº(ù†iÃxjyN“³#œPiŸY½vn­$Â:#V¥;ŠTã>¸-€±âŠã›.öûx1]ôÑÆ§éòÃÍÝ’}ÏçãÙr:Y°›9»¾™½›.§73<½gãÙgöÇtö®Ïã„×À·Â�ÿ褠BB[9°Ö•C¶€X¬EŒïRiÉS`©~£ð9¬“ K™´è^‚V¤È…ãÎSNvð>mã&-¸±Ðœ¥NS´ÆZ­EÚY�3·-èŠZb¬¶}4c]Ÿ-ËBB§Q­¾¤XE98žpÇŽqÖþfZIÀsD¯o?¢¡×ùá· Oè¶e£½÷áŒç`/Ðíu:�Ö¬z@i Ò¥+J×í±Áol¦\u~.°zj ˜Tê—D´Zb]ðG.¤GÑ£à•')¸(®øÝ^X[ië% ð�%:¶¡ÐCPCÉX74°X!C¡„ 3—ËïÒ¯¯=(ÖâûWv=µ2¹G'ÜÄ9B6¬Á/`™#&p+X0ˆ‡Ò)Ö‘ëlÈ–Xè-›±.±µ¬€­¸­ŠàýÍüz]ß|¼™/À·Ÿ›V™¶!¨Ga´ÂBÔæ„�ƒ½DƒU?:fË¢ÊkÆë6ÁH±<Ê‚0.á›+¹ÿ€ÑA^b#¸üù²N}Ž_:Zšú{¦Ò‘ÙóÇ�3]OŸ =è³÷\ZèTöøyÎ�áÁ8:à2\ÒßCðéótt¢OÐT•„G�a«2 ®X@}Iý—ä…Zk_É^–ÆžF�¶ ¥Þ¼¬Æ`P[èËÒ1uИËJ¶¨SÅÝþTÇ ³Jý�%°%"vURc¦>fDBSETø¤;ÑtŸm2Œ6+è&åpö`Y"Þ-¢¬¶GˆŽr-!Ž•¤²óÂn­ŸZ+ðØ–€½>© sÜäƒ+‡D'1À@½ jÜ‹j÷0ÏâÅ«¡ŠÜ×Ï86gY)žÁI]æg™•€äÖúÀ·ÚeHµõ¡2|&}^Ñfz9‘Ãy��å î²äZ%Ÿ7õpVJùápîöNÍìŽáò,NFkw>F­ÞïöÐÿ/k»|~*HL_”@8U,ÀFh-ªÓe«A³íÞ�¶{œúØ¿œyx¨†jÒØ9À$ؽA3ôZ¾À–:k›T|I}=·H´½ †í9ÚÀi-W ]›Y‰7^MaÑ_®4ŠÝwÑÇþ™YÖ�$~¯çw üE½šõà i[À©Òî•”Š(Ék]> ¿ØºL«á©í‹7oß’¥¿ò+6ùåÕë:ÜhúYp1˜Wm±ùáßZ¨VLŸv˜ø]¢�9wÝŸöùÚîý®ÏäþK)=Ê?Ö1‚;Îh<^U©¦üzßp³i|K‘�;Á ‘çh�²~&¦Ø‘Ì6*Ð}gH�.‹QàW•ðŒ­ ÝýþbÚ»ÌØ¤eŽ–o=§[�ÖÆ‚^K%äIñZ¶½Á é>Äð˜öÀQpÊ©)£uðƒ¿©g{ª5±Œ¢ë>§t×;áKž!¨ê‘»§bVx)Om³Ñ?§7/±A9s43�÷ºWWë í*~ÿû?ë<µ ǙƽَÎ$3T´zþã´|ý+­ƒ/z¿¢. Dõ«¨÷,lLIK±!m<•N„r4Wˆ¸ “”FìË"WB'’ÖÊ/‚›ÚH ŸkæŽ3$ùwó]9>¼ð¾poƒõz‹ill6È+)7ø0:¿O/Ïm�¿Ñ¹t¨“ü¯T¶ª”(L"îb.Öd,‰ÌJ¼ó†ƒ][åQå>93÷ka%¬¤Êy«îJ¿W©&4$¼+€Z¡àÑpJ£iDï†ÓÑ´ŸF³¿¯nfôi8™ dzÑå”®&t~5¾ÍFWcܽ§áø3ý3_ôI¢Np#ï Ëñ#HÅ5”)l*å^sSä ™¨¹J�—ÎJ‘IÊÌJÚ04…´Kå\˜¡SXÉÕRyQÍÑ“¤âN§“ä®g0Š@P>A=ÊùÒ¡j‹˜ž‰®sÉs/Ò””gHA ä’�Â⨿'ßë´·ÇÒü…)ΣÑ<в·¥ ˜@˜È vÒh0´y¦û&££éõZ_‹ä(glü{:9࢒¬N qaÍJ¥ ˆSæi8Oî˜Ú`‹)•AÜÆyžî`oƧ þ�o5)­R7¥«bezfð6®¿²¥€ìÑÑMØÌ\H€:hÂõæzôcrÒ54›8Êàº1Hl‘R³DE]›òSLTl  UóWÍhèÉs°ÁÖæÿÃ|ÛÄ~�gÒï¢ÌwŸpímЮ¸àtµv€ŽÈ&pBžÖã9Ï+C80~F4#f´nzÝ\ñää2ɦ*9˜^><¾<„u�GmEsM7ø„ß™¾èCMnŸ•87%4j·sqó$¦™åae?f©‹ôá±·“B€g=¸.”EŠôIì³söÌùÖ½ °‡r˜X€j¢ÿh,oT‡6šåªr†øv&m�•€¨,Uf#ëÌ›3NéŒ_¢8Šáu)|w;V{µ¦Þ3\r£­ÄnÇ;Ú¤å%äò (½ñjJ>ªÁ=Þ$&çÓ{®îk ±Ãì3"NfÓ…XÉ]­®Œ³¸O··Míno{§ˆe¤W"WéTe¼•�cOPYiÃv4©w¦Þ™´Â<¡Ú+¿�m+€oÞBx`L^ñöÈžµïÿò^&¥ç2u^KbKiD[O¡y^­¼5-ŸôúséNª…1y²«Á0T•¥Ó~.šmüg݈íÿS~ÆF�4ª.°n­Ž×®v�� mô™ÚjÐÒ¤²ÎÇ17SäïtžLgga@�G±�ãýr"ájçåï¶@�áë Ô¬tblX7`¸†Ä{ˬÀW¶Ø–)–«i‰¿Úšw[ÚTü8¾PKÝ �Uño”˜9 +twine/package.py­ksÛ¸ñ»~JÏ�¥‰D'¾KïêŽ;Ub9Ñ\b{"%é�«r ’p¦V”Lþ{wñ ’Jœ´ú ‘Àb±oì.tDž‹tŸñõ¦ §�Ÿ<%SšÀPÑ<½£ÞyÅC–ä,"e±ŒFÆ) áGÏ É;–å\$äÔLúàé)oðwÀ°%ÙÒ=IDAÊœ ž“�aB–„'$Û4æ4 Ùñb#·ÑH|@ñ›F!–h ð)¼­l8B Ið¦(Òüìäd·ÛùTRê‹l}+¨üäÕôùäj6µþm³<'û³äð¹Üš1!]‰1Ý‘ºÎ̉Ýe¼àÉzHr±*v4c€%ây‘ñeY8’2¤Ã6È Äì�gd:óȳñl:Ž÷ÓùËë·sò~üæÍøj>�ÌÈõòüúêb:Ÿ^_ÁÛ%_ýF~�^] 9Á6ìCš!ý@$G²6cÌ!`%AyÊB¾â!ð•¬Kºfd-îY–;$eÙ–ç¨ÉÈ‹KÌ·¼ …i1å÷`?‘dCóMÌ—æ• ó‹õ›W‘›'�˜~ÊËeš‰è­2±%Å>ERôì‹!¹¢[ÍË4S»N‘�Ã?Kö2$zêm3½Š ùd[VЈÔ̤w@ÔJ¨ý2nÌniÆ“¢§éØñ„™ e¦(…ŽÉ݆±¸kœƒPsÀw1�̓ùo7 Ísò©Gàã-ѹÔ;S(ü÷ø=tæ „POþ{žLá×�bë5@h®üg¼šÏqÞš›©¹Ïš¦É¿æàÒ°*ÂüÝ)rÔØ|ö�YSš¶jRRa‘d& šùË�§8©è±'ÖÛãyjµ¯µßѸd@«Ôô-¸Smø¶Xôzhr`¦çÆöü5+^ɱ~$`HA0èõz[‘ §+&ÇúøuFÅ€Œþ�¿gŠÏ{.p�Ý•fK^d4Û#) Ïà*4‹jÿÆHˆýžD2Nö$+Á� Z%"Ñ8ÝФÜ20½Ÿ„šÑ°€ 0ðŒ4¦!„=lQèxt¬ÑA¸'ÒÞ@¯ø¾(3p ¿bG:;TðÜ%�y±WØ0RmD‰ž‹˜0D˜X¹†ùréâ“tŸÒiÉ' J˜?ÿô£oä!3V”Y?>¸oß»ýÏxô;}|<ú›¿xä ‰7‚/¤EÆ¢Ó ï Ú\BÔW²•ÀˆŠ èËüä,^ «7<#*ÕÔÃ@å–%ÅY ¤òëyãðµá_Xº©áÒ}±Ip¯N¯ƒè� K¬@Ì•H4W†ß�–h]€%Í™$rÔµ‘ÃÒ$ÍÜÀ]¦y‡úÉ�6¬Ã|ö—gsÚL çš |tj“;·ÝÉlíw0�óu¢ÀbÝ•×#ˆ�{x�«Bk ²{õ:]ˆ�‚ÙÚj”‡Š#Ë}ÁòÅ�¢2{Õz<Ûd/Ø6ïê-ðÃW©>K¢Ã_ð4ÀðSdûö ~�8X}zßJÚ7�ÎN®/'Y&²n¼å9³R šÜÓ˜GvŒêw®Ä�§¡ÝãI:#Ç?äÇùá!±Äþtð笛øeÆè]ï0Ü÷1ç½Mî±KL‰lK‹ofËò«#2]™3€@– ÊD´µå™@È  –'ǘ’¦˜À q kŽ5¿gP·XX—`¥„mS8X«PGÆÊ¦ážÄŒB¢Ev�oÃ�Gàý€ƒÊ3Sk³íÐBºÛ`*Jpç‚,™Þƒ¯†ÚèVfº”¼_M/'³¹aèæI—³PæpN§~¸ZûõÙˆ ~²¿fq”ƒ}ß:ƒ†¦�òÇü#ëä®+ôº[IÇ£]Sî-Ðɰ¢ƒ‹E&ƒ>ÐY+cQ=¤»¯k4Zàß­ã+Sè›óÛ¶ ÿåd|1yŒçó7³ÁÿÃúLz‰U𦳮5ÁÄk­[yŸŽ‡äØÿCð¤ïò7øìÿ;i¯ð^CÈv3U›9Ö®µ—Ë´ìË )°~SúêÀ‰Y£®êX4”Ö|”’ ZË–.GïL¾ó–Új¶¼C¾–î$R¶¨(~~î”.Æ>Ž`íÎIãÐ&B,]ûhé*?ƒ#^dì6#pt‰F!�ãÌ«,ç¤�óèô—_A"þ„|qòôñ“–P0èœßš`³hÀ�Ü[‰2Råc·Þ;ã7Vôì�ˆ. £EŒ~=òl¦€ü|ÍØî,s³$]@ŽPå*;êåú4ׯƒ©†ä)¡4�lßÇF¦ØPIšS.jê .yͲ5$e\p‘D×DXsU1Š{QWTõN~ÍÇÔ8á=î�«¼¤æèæ×£éÕåõPgs¹r·*µômŠœrÄ$«­¤\G»Oޤ��@r|•#ó„9Ó*Ÿ5Rð¡ câô™ÒpëÀQûÈ.\(àX´* ¹Þ$üfóÞØ&Ý×uÍýhq$Y#Gae šã :òr»]hýÚÚ¨ÿSÈ/ X5Ф%0‘(õÖ °-å± ¨Æà0”`ë’UX둃 .öæxc™îohýÚÛ3.ó´†Àwl¿Y”HóÞ4�˜˜«0óÞ„“5žRY…Ñj é`,h”Y%{¬¥~sV5©iÏ´èRqʰ [�  ºªê¨Ê÷êR¬iè7“ò㓟’ÊÄ=xvžyoì¥s� ̼7ÀÄ2à“5\5Ð$¦Jpžø§-Šþ`a�µ©ªÆšªÖº±èp!Ñè¬pGpï¬p-€�eZZ‹ÌÄ¡…*fµ–©áƒÂ<õŸ�÷¥e=|Ø#ˆw›îéÌ�¬†Ž£=<¬ª×zýçúüƒ¢Ý0Át}·Ë…•Ù8ÀÞÂuΰ]“]Nof˜áõJN^_<•‡è3ÙŽ€d�ÞÉÞªÌ�eÓBŸÅ¸½Of±[]‚#l›Šœ"ã ,‡ ó;’”q¬1àu�º{I¼"“(—Y Æ·pb ±³vI~Ú’cÝÓé\=[K­kk¥ÕÏéBÚªq·¦Z œ<Ž«¬ ëÀÑb£O['>²M„y²n ¹UÇèPÇôÍîAež÷âæ™UÈBš(µCU |É;¸�yV#[ä"ePø´Ø‚º7[zBs46ª×6åç¤ß–À—Bp¡Q`¥¾دÅ ZŠ* ,ö]ý5W*ò檿ò�_ôžO­Îèg¯.U�X𭡍µ ¾ï/ á’lã�"ˆáf„ƒÐWE›# ƒ™<L°<P€�ʲžš�6!œ�Ó n´Ž³2A{ì›eÍjÛ^»:ÎÃåÁš¡foÙ=(ÀÕ´ú‹õ�§nXx`�jñäXºt˺&SýEì©^‰âR”IÔÑhíl·�ä/P »×nèò#mE“,, ŒÒÝíÈ•÷ÉÚç3 2ðÒoé=䍸æ·û –æÔ…�¿£òº�¤ÆDÆE�=1´ñˆM6â ªƒE§¾e ºÜíÔÈ£ZdO΃o”øÿ Hï6>ÆË{|8=¶xÏÌwv”æÙž`‰ì PÀôô¿"€ @ªC{Ù Jl¹v ªþw1’þ>’齃®ÛM}©÷²ºÌ¨¯èu‹α®>fÌ]ãêhjÎTûÔw/ÕŬz'PçËb\2¿Ä¼5—]Ë\þ"„p£Ëû9ü;'½l®ªì ÌÑz– ¬*#&2?›äa}åY�ê¾RÅkçlëp}X;x.»©’Ò­bB&<úfIíÛƒƒ7ˆ.D€ÉBuA%;2‡ ½@ÿmSŽ~Ëâe‹¥ÃÔeú–“­ˆ˜“ù�"PZ“VY+µöVeUë.,�öðm¼5–˜”e-ûÊPáløÈÎaKrrB~iqݯÙ’9dÙòqÐ!‚YCú6­[ ÒvPöe Óæ£³ù3u÷ø…´ÈVÛÁ¤¨ éë½ô.ƒ%õ=dÕgs|ï;©Ðyd‹ûÚ³[wÕŸ2ÐN¾SN¶�}EPè$¥ ¾]T"Ä‘•MÏÃ…¥lþ;¥å8ÌWÄeÃ~A^ìÛö0Z‰9$=Xdò†¾"«¯ñ¤©n`Œ,ÝàlÕ v˜¶J„Uê’­þŽ#Qá]‡Ã¦Óí2¢g«J.ü‹Éåøí«yðìíååäM0›þ> ÉÒó:n­kk*£Òöª¯€:6ÕÖq‡r«4Àå%~fý?ó7D”š#U­¬:£8ÜjEºõ—ašFj zÿPKÝ �Utwine/py.typedPKÝ �Uç—*2 "twine/repository.pyÅYýnÛ8ÿßOA(8Dƹrš^ _]œ›¸»A³q»W¾@ %ÊæFµ"×0 Ü³Ü£Ý“Ü õIInƒÅ§?‹ g†¿ù"ÏÈ•ˆ÷ _o¹¼xý–ÜІŸJOôÎzgä–{,’Ì'i䳄¨ #“˜zð/Ÿ�°Dr‘Kç‚ØH`åSVÿoÀa/R²¥{ ERÉ€—$à!#ì›ÇbExD<±�CN#�‘W½LÎÄ_sb¥(PS �á-¨Óª´À¥b9w»�Cµ¤ŽHÖÃ0£’ÃÛ›«éÝ|ú ¤ÕôŸ£�IIö{ÊÐsµ'4a<ºCº#"!t�0˜S…Ý%\ñh= RjG\|.UÂW©2,Uˆ × ÀV`fk2'7s‹|˜ÌoæàñåfñËìó‚|™<M¿~™=\ÏÝÅ̽›-Ü�·“ÅbzGÆä`­ãµ+ù:¢*M˜5 –'"BXÇ^ïvúóäê«{ÿõþh­é1ØÜ‰÷j#" u« \L狵�Û_榾ù´N »#¨ïhêŒlv{íþ�”  Ôkw’¢už%‘ûeú¡¥WFÓC�þÆÚœ5S·zÌv݈n™ëö{½žRð… É•Hö£�Çgq]q庶ÁG²0”oIù� ðÖj±€‹ŒJ/aþ±"ˆaÙ„Í“àþOܯîŠ&#²ä�NiqTÓöÉ«÷äNDldÈé€L@h Ù3I ¸é0<.‘] Ùý’ò¬šœg“M!àÊ�HC~ â! .Q„Ü?Q“ÑêÇqk¼>¤ª´ F Â ŸsnȧöÆ¥Åìz¦’v7§Ú¬+|Ý©T&ä˜T»‡�]_Ûg)××ï}ƒnñØÛ­²P†4‡G�°«ÚùCÉ©ÎUs:Ƕ±Ú¿??Z§øTyw8ßpßgÑ922E:×|Î�ï�O·6ŒB–Kë3Hðj‚áÉzÃh"wKŸ˜[.SN´®m?š]BJD-"bkWOÃ8Sx�ÕF6–ߊ4RvÆaP_4­.fo7a°.“v¿¦¤7‘ge*&Š:ðP/L}æbù°Ž²ŠÇnP­�mm°m¬bl ¼–à91ªï…B2qx*Ô¾–Q~ß–°û –ÒNuŒ‹›êŠÀU¸¥Ýô ô½,VcËZt×®RÉQ°“ uL–�FybPÕ=Ó0ÕAi®Ø`oFo@Š4§ `†]—<ý ú·5Û±C].j5ú ¶M¡Ê�áŸmWrõÍÍÃØÚæ�ª Ü( þªMòý¥ðãÚJ9 êôÕ¶K¦\v ‹oª l@>ÐGì5°ÆÙFùÐ�°iNz:H"x€±%£l-BdÈRH '^.EE~ZœF9JÚJ–„­a‹ÁO³ÕóydTÊÎ}öò~k)ÊÚå�ɺfB–Ê?wÀQ¨Þ À�º@píÄNû¨ýÁQMb�ˆ%Ó´<˜! (ተ8ûÚ:ÖÜ^7�ÚrM0@ŠõW� P,ŽõlÞp­,£½À�+áYä _×Âíæ×4T<¦‰šfDv}¹:Te\,^ìTaÊno%ä'3‡ Çq.ƒ9£ódúhOÉq­¬-ž¼|¬«¬­yµØÇ l›3tònÇ…€뙨ø…Eçš\þä�äOhÈ%ZÙƒ. Ê~)RÈl_hÖÐB'Ÿ• ø¢»_á=1åf(Ãl5¨Ýç*´ºYó«aÌC+^�‘ Ê­‰:ÆQØ\kЦì¾Ix¬mÌÿÙ-—ú¬¥{™Céó"aÙ.H 2ðâd]ôßA±M Ž#’��“H­óNÖ?£|ƒsPà} £‰ÄjlCŸ (ì‹ïXÝ"XDŸu�C‡fG¼Ô9{wìàó’ö®…âÎA„v�].s4°?‚óÕÏò\oœÌ“Ý ë8ZIzF žÚ’¯·ô™'©¼üK�–B¿G�j�IBFî÷÷7x'Còóuâ &£sUc¥ O¡Þò�&èf,  ÿÓmn€Ìò"¸¸•ÈË;Y•ÀMìÆ‹ …ÐH”Äú¶kWRžØˆq!ÐìÔskgF¨,J¦�ìkp9VÛ©ì�ˆôAmSJÃÜ�UË/O�âí�]®Ù7˜—ŸsÙèzñÉÎáëP3Èwex(ù‡¸�‰Ú<<ë+ž‰‡×žXW×+Gýͱ¨0"6J�Y¥ úùÿ~WðBÍvÙhjv+¿C¡ìþÒ*æ¬Çy^Éè`*tj?–¥íõêÙ|­/È`"k“ÚÍÊÉA= ËÇÚ®êˆܳÝÿ<*Ù¶Ž­šÄK«(ùñ°½‚m½8�°¢lE šOd§!Z+¼Õ‘f“ )SÖ� <ÙñÌœé¾G㈦ˑ+_íµqW8„'˜™w|�M’Ë&pr»HØÕv)bvºà}ÀüÅâo� †‡V9VcMd‡¦_úZ¨…ˆ’æXíQv¬æ–Ù"ìºv‚÷²C3¢œé˰f˜ø�RÉŽî3àJ–¥V?ï Œ(Žõ7ˆ…Þáê\B>�B×OV¡P¢ †qÊÒËÞ©9—”½ÿPKÝ �UÆ¿òEŸ í/twine/settings.pyÅZmoã6þž_A¨(*/l¥W @áƒç:ΞqY'°½],ŠBKËŒ£‹,éH*^߯¿R”H½Äiš¢ù°k‰äp8/ϼˆžç}ÈvEÂH”¥’ÆiœîI’íãˆÜgœ<Ðt—à+Á¤„ÿEàyÞÅ7d–å'ï$ùáû¿ýD4%kIó„É,…A¾£"Ê.¾�™7qÄRÁv¤HwŒùÀÈ4§üWŽ É/Œ‹Öý|O|œà•CÞàï@á”ä@O$Í$)± ÷1°Ì¾F,—$N�ùCžÄ4�9ÆòAmS €Äç’D¶Å# ósxº·ç*ÃRæb|yy<ª8 2¾¿Lô,qy³˜Í—ëù¸Uó?¦ ‚pöß"æpÎí‰Ð˜‰èXLè‘€éž3“2{ä1ÊrHDv/�”3 ²‹…äñ¶�ޤ kp`{È îM×d±öÈÏÓõb=Ÿ›Ý~Ü�OÓÕjºÜ,ækr»"³ÛåÕb³¸]ÂÓ5™.?“/–WCÂ@N° ûšs䘌Q†l‡[3æ0€¶€Ï"gQ|Æ‘Ðt_Ð=#ûì‰qe59ã‡X &°·*I|ˆ%•êMëPÁì—qI(ßç” fžÑÙW™Ä[óìq;\Üóì@ä)ÇÝÊ¡iz’Û÷ É�DTÈ‹rÞ1N™™F ùÐñZò×1ÈYž‰XfüÔ1XÈ8QBAxëÒ;ÆþÀEn·ÿa‘„ãR †—‚ „:;œí>Þ\ EIuƒTƒ µpƒ–�é¥h8K“Ùž©�4•1Eå+û~d§#xÊ®8°TŠ’Âu†¥ H6Ôo‚6ݱÑ6É¢Çñ˜ä'ù�¥z ÿ ëþ†à‹à`<¥6ùîž>2óôÝ@/ø' á4Fo!cuöñ—Í)gsÎ3þ% à”ÑÝP9í1+à8ÿ�¬ˆxŸNÎðS Z¯Þ±{†^2 ýŠž`Éý°zzWÿDúc²Í²„LÈ5M„œ±e<&àp0îíó½W�Ç;w,OãÊê~…y¿ÁÄe–Zt «çæå CTå¹yi–Â%ã4’ñëc€íá5m‡!¢¨9§²èàj~=ýx³ 5®ïÃëÅÍÜ’Ícœ‡ì+ ¨©—1~ž@šT†/™Zûa¨Zê% ð:§<9G@k›‰^1Ú"|‡9Ïö�á–ò¾¹ïÞ�Édýá#€9€€%5< £¨�ÇÕl°ÙØiL“øŒd¯Bkéæ‘Aü2Òƒ6Øê%þÍJ �x÷À�#¼@¸zDPVÑQ<(ÿüÀåˆëTµ©;¤7@mâ#ûÊ¢BªhVÄp¥³!Riѯ\¥EþýÝûjTƒdÍlïx¨Ö•›µö0#AŒ¸e0ŠÏH»6â—û8„bˆZ_­]+§míjFþŒ]›ହÊT‚d�Cìó=@‚„qƈã²â0¾/™Ñ‰‹C-‚È5X:ÄFT`O÷-– Ü´äPèŒ'JŠ]™˜1H¨ê\FÁ.µÚøÔ!cÌ𲮀Šv#…h“tÁË!ºVYÍ©v%éK“Ät$N °£<ÉèsÜF üÀ³BÚc„&¢à‰¨íD {Îãå1¨E9&‚Ü�î¹ÅM:Æä•!ÀV&ýE_Ök;ܹÞgŵ…4,Qž�s1“³•±*¢pææfíZD–¦�— ”KZ“mn,€^ƒj¢ÍOÅd”`ºÛ‡êýÛSÊùP%)JÐd“ Ê”5Ž$/‹PÊn •JZm¢8~7ÿ0b)¦-;›ÅÖq›AêYDíðÿÒ絿hÖ ªÎäP«ì’CdʳTà „­�8µ£Û»¡²ÅëÇÕÍÛ°zÄ| h¯ ˆ:ØÝÃk�ªÊ­¿|iðË—×&d»®û�ÍDS™²µ°3˜»à©g”¡g˜U“ÂT³Rä•�…P�XOÍi'ÝaÃùÄœÁîâæv½v:`+œgwj¨j|Z ÈtMä;2j(hÒxöM ›¸�õÔA7+¥¡…˜¿.ª8À†­×*™™T¿Ü &ߘ˜]l|C >r %°îÖÀÇ ,×Ðêï>ŒMÈ×=´Ñ±!Ä:Pþ¬˜È0Œ zÈÀ6| �$`Ð’°¤­s¡HϪ¿HÁg|“ « f`ËGýü'Ø@²œí!è0š._Xµ¬|õ eµ ¦åÐ�è–ߪ$¨dUÑRWѱ%¦7 ènWíìB¦7âÞ°ñfTChcŒª$k¢›%óô)æYŠ4¯Ø=-éNféÓÄÛ|Z,çáj~w»^lnWŸwzá¤ÙÎÀ¿–ä@àl¦+')—Y@<÷TëªÄ~.Y$~ÉѸ¹þ[32ƒ€ø3šB�#2U”0IžbJ¾õáÈ�7‚iRy‚“—`àuà¨ËRβ‰7WPéo�’Üþ™êyÒh ½X}˜k÷ª°!»²A\æÓ‚Ø¢h©ûÅ úãmÂ$¬[3žvY(yÁz¤Ü+Ê5¶„tñ]‹¬À*;JÁ« ™a¼ïóM·\3„},•„ëNTÝ·Ò¼ Ë™ïy-§M€ÞM6ÛË_ÕgsTR|5E›“x½‘;~\ÏWË釹÷w«ZçûoM§ix'=H×ÂÆ?ïò¶˜Mæ÷Fb¾›®×ŸnWW¯sÕIüÃbÖ탿NÐ#H›GVÞüBé^'tß+Úåí2\,7óÕt¶YüÒ4d-ÅW7mR©>>÷õmÿBÙFm#.[!½ÙN_óOÞî¿Zé:ë!‘àÁ71—ó³89 UÙ�~}tzŒó‘éݼ@Ù™þðhK›5îŸ{šÞ~ºs$Ý6P·H]ü&Pu ü(C¢¿¬W%þ¢¢€,‚éî–¹:⶯޳âªÔ|˜)úƒCÙíïϸ]–rê½Gø¸÷ 5ú«×h�{xÍ º(—g¹oÍðºèvpÇ:õºÝõJ„ÿî�™i7ÿÎ5Îu{ª¾÷2lÜsöÝkQ::1¼U-�®Øý­__²z~‹TE޵æof¼u­XiPQ7Ÿóö�!f€ *¨*ga£�®HLŸíuþrTýv§T•Ó¤Ú«-òç>›h©wÝ_>]¥OêÝíýêòΞI›åEzJÇWë“Uï·›svz¿ä´x´�EÙ»æ8E¨ÆfhèvùUÍp%µ—o¸°õå|ŽihªóêÒð™»JÏêISs”£_U_�ºOÓ¥Ý wh©÷š^=¥Ÿh%‰è�E�MyW ñVÃõ}'àÈTA¡/áªKÂlO£“Nñ¬ KÊå…n+–çTyá&»b9g(ý®¼b’Eð»ëIyÁÉùІݲXw{�ý-~-дOˆ_`Ÿ]Ø`ç „¤\ „ ×R}«!w3?�}ï>ß-lw7ï7óõF ÖúœAÏçe˜¨)ÚPÚpÛÆe¿º÷iFsõëÂÖ¦µ�´}·¶#ëÀ«ê§cX3µÀ”m«Â×TAS£ ÇNÒo„•ÛUDÛC¦ïêú2ß’¸;~m±á(ÄJÆ!Êø~t­©Ý½\ê7‘¢•:àâ‹ÿPKÝ �U�¾UÐs*twine/utils.pyíZ[sÛ¸~ׯ@éS[‰v²éN«Ž·u|ÉzâØ[ÉNÆë‘`’1¡-ZQ=îoï9 Rr’Ýçæ!–Hàà\¿s�vØ‘Z®K9¿7ìåþ‹Ù±*xž±kSÍf¦·ÓÛaç2…«ŠL”ÌÜ v¸ä)üqoì£(µT{™ì³DîUÔÿ'PX«Š-øšʰJ !5›É\0ñ%KÃdÁRµXæ’©`+iîéG$Ÿ ug8¬æ°~ ßfá:Æ-Ã÷Æ,õhooµZ%œ8MT9ßËí*½w~vtrq}2niý‡"Z³Rü^Éä¼[3¾fR~,æ|ÅTÉø¼ðÎ(dvUJ#‹ù€i53+^  ’ImJyW™–¦¼>»�_ÏÆ¿\~³_¯®/Æg'×ìòŠ]^Ÿ�Ï./àÛ);¼øÄÞž]˜=Á1â˲Dþ�I‰:*ìZˆ3eÒK‘Ê™LA®b^ñ¹`sõ ÊÄaKQ.¤FKj`/*¹\Hà =Ù*éÁyª4Œ—ó%/µðßS•ç"¥]Í£b&íªÒ?›UEj”ÊëE¹šÏ�ÿUéæS²äæÞ­ ™ªLdÜðÞ¬T fÖKäß½>,ÖvÄóí7`ÇbÆ«ÜËÔÀúÿr‰Ìñ|jú½àtðxdÉUežË»„¸õDᙕñk+ªÂ®ñŒ¢K mj9ÊYúã?þþSÏq½’E½ÛF© ­w/d2Y,+ƒ^â¨3@ $Íãó j�ØE½ÞñÉéá‡óñäêäýåõÙøò꼋|4TË\ñ,Y‚ªl8ˆ9O×{Qo|rýÜþPï};ê1øEÑ•àì_»˜®JR&Ù p™`=–‰HXíý¤G4NU¹àfT4`ógN/׿^äe ¬‰ö,…=KàÌâ}f#Ú1`�Ì”Ðè â Àì}¨*1O´×Ö̪’õµ$?AW'O÷ÒÒßRðÊu”øG±ø Ђ¤Ó~ßR&¨ƒ}!ò%W|eµúž¾Ç}+—)×VËø�ÒŸZŠ"öGõ!TجYÑÐGn² * žõ[ï­S$²˜©x}Ðèp[ öèÏxŠì~G§@òB™S¨R–ªlN‡„‰’5þ, -í!²â#EXˤíA†¼ÖT ÒH±ÏÞD“¶¬%—ªöp8òÕäƒD ¡ªî´Ê…q§‚Ý¡>@è ¸ÿ‚ìœiª.@ê;™K³&BÎ/ôhÃáÁf�õñ‘éhäU1G!Ñ X6`3€j<ù�©?hHyPø©zÙvROVVߣ0»=¿ÁrrÇ:7'Nì�s¾¸Ëø¨ÖK‚VÜw*!÷™XN5P øoäç22×(íº¡(FYn6Ìú‰†ÂÅøpØÁ*t×x”%X¥¤±àEtèìÃÕ¹öÅ� ¸£ÎYD j«¿ÖÛ ¼h¿}ÚRd:V‚~ 0n?ÖЛ/¯©ü¶Ç:< s[ ¦�aM;@ÞŠu§¾ü>b—‡s ÀæÝdžõ§](Ó Ö D|J~+¢. °ˆóŸêPX;ž&¿\^�/ß�\×õ$Õ !—ëâaË«NÓî>¹.°ÁÊå6<²•^àï¨éQ2àÏ7âÚ]}^Î’B˜\¥ˆß�H~ÔÌ>âØÁð ÏþêhܼÝö{Û—Û!¡y&„ˆñúûÖ©%Z4Ô‚Õ8pí'ðúöõ#ùÝk”äçöbÿå+xX£B}HÐÙÁá­cûl�öÑ|3© ipÐñöuÔójj(ýL«ƒÚ+8£ùÐÜ ûÎÓuzšE�õÆQòböÄëõO‘Ó¤³ôóDn*=Á¹à�†@‹Q=€J®Ü£À»SøîN©|³ÜxCÃ(78»ùrVå r¬ÆAa]!ØÊÀ´¡Õ®\»E�ÌY ¥¬©#ÚU¯©š LÆÆ¿NØ%–+‰LB©UP�æ´á΂6XLÄ-VœDÌV�}¨:fÉ¥"”¸Ù úe<~OhÓîò ãíI GvpÀ^½Ø§šf#vÑÉêM­´°™¾)ˆÇêX@¨!²÷ë÷gÏ&oƒÓga�só]lh?É¡X‡ƒ.»@¶Pú ŽÐwP-©�8\ã$)¿‘dƒÖ‡ðp ¦1Úàšh×%l †(ÄʽijKÉãU=n© vC~¯ÈT�›ÙÓV!;�ÕS9´ºB͕̂7²Zh#¸eR‹­Ì…c¥º¥Ó€¶P«„QnàfcßWż’™Ð{ 9ǤU̇F Qæ!¾lÕ_ø¿È¿æ‡û üð: K èuÖœ á·Ýêgv§Tï}hCœµ@$è£CJºòN!¯kFE+Á hã.nð*h¥HߣMûns0ʆÝv„hmÞ¥26°4ľÉX[¹ÿtŠ#§VÒä\ÁÁkÜ®í=¸Ö°B…QÔ2rF—^s·MèVœ._ÚöÀ‘%™A³fp•@G@Z‰§¾5Û€H®¼@ÿÍp8–ë¤MÃi 𜲠Òq{4í�\Ú¯�Â~ÐMu {û:ñ O×X, ¨sÎ|Ðó877·vßiZ·zÈ8¥È OHž³X$€/¼¹Pe}'Ðo’Wk ãò× ØAcL…ý&Z¡¬r¡Göõ‹çÑÓi­ŒéAÑU‡"¸»Nnl¶yé×ÛÙÎ+¾±ãG·££¤­»¤Ñ¾jpwHáU�Uýëéµ8�Z)FP€ñE`ÑÚÆþF†š~¼W ÕEMÃ&,iÑ Z8üwH4TI×µÏ^ØN®E =¥ÅM`VÀ¢\©Ïˆƒ^©Ó逑½§Ó¦YšROOêi:m�ÓõÁ†ù¯6«œI7Äta‰ÌÂéÍ�•;•Ô\SÀâüyíh㥦'ê“�¬UžÔѧ •ðŠ)ôã�j¦¶&: PSâÈãî�ã'šâß­kÃZè‡)Î{jÒ½&iYåÓÌ çlìPRZp“³åÈ&`‚CŸµšT¬§ ³Ú8èîŠðóXÝŽ‚6ÅYô ³ŒåÄ@lÝœBŒñb7Ô Hspø8í't÷§ªãè(ê4SoE§v�ö?ü5ƒ#KÅFÐÚ3å«W{;¶lÏ'Gî°ÉGЧ½w8™Cƒ§Üþ,!úíË‹Ÿ"œúèPt3 ý*‹L­4;rñž÷=jÁ"�CÉEJ•Cƒa¥n©£–¶Ýþ¯fEð÷òü±¤ÊŽAhà]-ŠRBjòõ¿¢­·_ÏçÖP³tO7i`™Ju"ÕÞ‚E[Œz[ö­³+z6Þoby>Ãí�9, Ü{ ÈÁ;#öÆáÌÑ!Ô\EÝíƒäl„P4šnæg@6šép»£eüo,ð· (C¼¹‡¦ãõ¿ß:×Nñ¾±3�ïd€¶ý &‚K ¤9¬`'Ń,U�@ìî»bÿ›“ä� ¹ßªü- B‹höÂóRbMAcÚ@óù‰„~z2iÜD‹|ÖÌÏ€BP±X+ÙŸ Ù&/yÊ*¸¹F⹩§_öÃx;9‡Ú±bÇÐí~< fçN‚Zø<ðoû-Öq¬‡ÿÃpó\µ\³þÑÓ; �j‰÷ÓI­·ýÀýÔ[ü‡A-Q?Tn …Üóʵ÷5#ÖØÓe\{CÞ,Ĥ§¡ÍÁÚ ÿ¬YfM?²?ç±7¢þ7>7 f¨%Q½·Í›é0# }Åd[lù‹SÆ5o«èú@A–•­^|šóùó.|�¹K@s5ƒ•Ø‘IÅ�ß¶=í«>FìãÙvú$âM·ëÓݾí=òs°ÿŸiû ‰ÓÄ¿±ç”éB˜{•ÕºiÛ8ïO'â¢æDðºd»û»4DØ�a¨»Ï…Úu¿Ç"¨'¤ø�Ö­q€íã…}Ç…Šžº9 E†ˆ,üMðΜ€ª*¨(,±~ïPKÝ �U­ôÓé twine/wheel.py�Vm“Ó6þî_¡1eÆns:¸~é¤=hàÂ�’Î%ÀÐãêQl9Qq$!É—¸/ÿ½»òKœä äK$í۳ϮV~@ž+]±Z;rñèñ�äJIVddîÊLã„‚‹� µt ´èkØå}=ÂjÀkç´žŸo·[Ê>´^ÿ­,¸µÄðÏ¥0�ç²"L˜”-bÁ¶DÂV†ƒÌ)»5 ¹«r·e†ƒ—LXgIJtLµÐ á¾pÅ$ Gs2™‡äÙh>™ÀÇûÉâåì킼]_�¦‹ÉxNf×äùlz5YLfSؽ £éòj2½´=†öÇG‡|ç K]ÛŒ°…²_µQpß\ÕQ¢«Ö�ೃl÷ÉÕãïÆ%9hºÁ†�N’ÛcyߦÑ!ouI]i`(1Y…�€ö~µ½7º2ªÔQè›+lÓ³86R f­².Å\È,Ia´¼,IGâµ"¶C?|™=~×Ýî±@+¾…ƒ‘ä­a01矈%'oÆ‹ÑÕh1ªET;3WjÔ¾$7;jaZº(<c?w8Ú=¤+l�„x¼» ޏÙÝ<¾ÝÛY.<‹" >ÚŲëù¬cÆ·AG„á,ÛWyY9H¼ �–½®dK‹ÿQ»—ÊlüÁa—”ÚVŸï€<�Óø¨�LXÞŸt"ïX!²þ��©‚Ñ Ã ÉC’‡°)s(—™Å—; ql„GÁ˜I×âo[óÐß…~�ÃÎ;ë«Ö¸lM(î ÀõböyôÝuÓã>F�J×zöU¨ïÉ×þ[x: Ä9øLù$Õ¶ ˆ7Õ7ÆŸö=8ÎT‡hÐëê{®î€ÿ½Sñ)� €þ©„ŒÐo|¢êŸÐËÕ�í©.ôÂ2lGÝYóYè/œÂèöW@À¬/ª{{ˆ¦…²¼ß ßÜÉÝ\]MN{ÚÏbf,oÞ&�¼î¨û^œ¦xÓÚu{Àr Ä EçC®&3/§OUÆ£°tùÙO!|Ç£Œ½ ‡78…oæ=¯»:þ&©åúè]ʸM�ðùã“dWtÅ]¢YU(hò8øPKÝ �U¨‘Þ¨twine/wininst.py•T[oÓ0~ϯ°,&9°zⵂÁšÛž@ZWEnâ´f‰mlwMöë9'MÒ\*~HlŸË÷�‹�*­q�©ãÎÉn÷ªl® åΔ$ÔVé-iE÷6(£E…öy«tn:i¦|pj³G�V#”–�\V©lø(‚{¥}H)q’|<5¥…3s”¿µ5ûôðÁÖ/Ò]?eïž8|â'.+ù†ÆQ¥…ðžüTúܰ!4ÿ28ÄˈÀÊdN’0C’0/‹ü’ ²¥\о$¥ "A$èÁpÙ»ùÞ-c²¸n6G·¸Ðïœ�Z·+L݃âôjl «àD¾·J bFÙg댕.Ô}X¶î4�5 �ñ‰` X“tóR„tÇFÔãÞ@å`£ü$N\N†½Ó„ ]Ó^ ^­ä[gö–Ñ¦Š´�9;)²ÛM¤?yÈcvŒçV„�Ö�µqes1&?b¯MèíeÝà8�'$…òrГüV¿ˆBeÃîaôοOwMU—äÂSr�ÛXZ<¸àRgþ €Å¥0áÒ�zÁöh_Tö{½q6TÅp<(¶&ÏpbÌa›š²¾“ÏetR—ÎsS…céÿVÌÉÓ ˆ ÏÚz@’CíDÀß4Ÿ#û�àê1›°·E“žÕ ²âÞ*0zEã™1+¢ô1½31±•p»]àh£1AÃ�èáÛ×ÅíÝÍýc=:ùtwîhz|2c Ú”Uñ%©â•c,c?(ľEùªZ½_ŸÔ[§ëy]S¡3Sû ¢ç¿ŒÒ ½Ì󀳤¶]ãô¶s]Hʆvhñã8d(RÁ‹9 \m‹¡ÂéA+˜¢E}öEð´0^{ûÿû iW–«¾tȳ9×hqôPKÝ �UyøÎž€ twine/commands/__init__.py�TaoÛ6ý®_qPPÄ9é¶ó�^’bÂ2{ˆÝEQ8”t’‰R¤FR•õïw$%WF7lú QäÝ»»wïÇñïªhB®¤e\rY�= UñJ¥ýßË‹í¸Ä—0mv�«ºf²0IíÁ¥5 J2æ–fÓRY`д™ ¸Õio ³VtT æ bt°¶1ËÅ¢âöÐf …X4}Ã>î‚Ó¢YÜþø=P`ø_ÆoÞü�DqGp¯š^óê`áõÍíwð $lm[–6º ƒ'ž£4X@+ %¯¨ ú 'Wð'jÕ„×É ÌœA<ÅóŸ¡W-Ô¬÷õ¶%'bñ˜cc�K"ªng2Gè(sfIâá2×b-§´©';`!á‘�®ëæ3M”®"X™ÅSzÿ¸Þ>^S¶Þþ�h hü«åšêÌz` %“³ŒR¬ê «4Ò™U.ÙNsKb î¨ÒvÔJB)¸±šg­=cjL� žWLB¼ÚBº�á—Õ6Ý^Æût÷ëæÝÞ¯žŸWë]ú¸…Í3ÜoÖé.ݬéï-¬Öà·týpHŽ¿N¯äãÙlv6YŒGs2�‘ÓÉùx1žNà킜Mþ$¿�'ç>a'PÃJ�öƒ‘Æ�Ű9c0ð]–,ÊȈœiMSFR~Ï„J—’‰M&ñ$%˜ƒ”<Ûd­ÔÊ�SAôqQ*Ò’ Éì{”fö1ãö ò0%öb–¾!Õ®DÕfõbä“i‰iî“E]æ�_•U¯á¤ñ†…‚¡=Lö”(‘Ek+¨Y,Z…ÊzkßlÀ7yd Rå„Jûb’özh:¸>´>)«.Õš†cB¯×ë…³Ñä|4Íæ@ú¹Gà7á³Ø'䄜³„Ö9)!1“‘È”ß!0+ªB£…(iNŪ~™C8µ¬ÄÌ”\ŒbD ¬±„fy‹áá:ÇÍh‘m¨øóí7ˆþÎâ^ZçtŸXÿµÔ%ѲªÃ$­�zZ‘˜Gu•å’@Á`ºI¨¤Šƒ É7 AáÐQ²®áˆP£Î9Y�œ€,5à_ιdùެ¸| ñ$1ãK�ñ0Ø— åQ¯8ð¾T[§`�yì¯r¾ž~ôê{¶z“Иý‹¾Z½¦oböúůâqüúÅÛ7Éó×ô{ÓÄò¡}ëIPîN._¼}‹'5�-àœ¼`¨³„ÌqUt…óò‹ôÝÀ}õS)÷Î}?¸�¿óÞ{âª[Wí²{–¿;ý|ýËãxr1},˜Œ'¿<Žf³éìq>ú2Ì뻊ûÖÛs#ö]ðÌsôÉ‚-çÓÅÙå%yÄç߯/ãËñdä÷0a£œn„õyÌ+H��›ñ`®¬O½�³D!s!î‰O0[\òÈé;6¾¦1¨£@G[ŒP ¤� µŠq� u „X¹¾†\z*fh£ \ òÜX%‹*˜`ãjw5Vã�À´SsªvNÚv;?D5ðšþÍ &™„ûÑÁA.ïêŠl¸0×&`çuw=‘Ñ8fñ�€ÆìØØQîJÚW#f¿ü?`¬ì¿4Ö Ôù˜@íþ«;#ÒI5R¡ö ÍÊ몬+ Þ] /qjn·w$ ,mÍ×ef/cœ@0Ÿ1(¿jp¦cƒ—°aèœh˜-'&^P` ¬I9ÃíŽ›Û î&Ë¥J¸’Ú P¡óp«Î‘©ûKf.Šj€ q¨èªÿ=ÔˆCj€2‰×H× ¨ŒïΖ¿º‚j¹=઄† %ßù]‹ ~4úøÓ‚@yâ:“ÂÖ¼`*¦UC8BãpÜ),tÏv5ôÒÚ¸—£.4n¢kÃøÙ’Ã°áÀe°ˆ‡NGï\@ØìN{Þ¬ùûºoOMÇ=*ØkFœr…¦êË&Üku—ìƒ*\mRg…ʺ¸ÓH�í-ç®éwgãËÑùMŸŸÎ†íóéì¨ü:ØkHk¹+*ú`òoDué PH³U@€>¬lzë›=bQpÈ•8·Åç£ÑûÒ%n|Q}ÃÌpNªöâw<†$®&¯Õt4¦ rõP—¶cyηwWgó9ÈS_pifÓi;öT˜„ŸY #禯ߜ#éÕàQR™Ï—mX_vèÏŠ¯ÄÔ¤�Ùt ½×i}&¸¦åmà*îR‘¶{Ýa?=0¸0êï1Ë¥ùüf�¬Û”¬–²Su×€íçݯ‚/r°‡¬²Á0èD«±Í à0�áPe¿ägFÑ•Úq¡}¥C§õùМ¥f   †Ö´¦Þ…Êæ&Š¿½:ßµ–p"¾§b¨h[ëk–—CçÚ¡OjYÓš8õŸþo1íôTWOK%UWƒ¡#+èöa¥ÓÚ4£ñÐÌ,vÙ~3|º®Í¿€\A@°Ù¶·¯1P'ÕUE™«ga\SieHOÈpS�¤rœ$u™�È|+ÝgѶšÌQWêv ë9­¥>På@¶wô’×ûPKÝ �US�ÂÁX twine/commands/register.py}VßoÛF ~÷_A¨µçÒØK† ð5–9Fì®(‚À¾H”}˜¬ÓîNuŒ¢ÿ{É“N?ìl~H$‘"?~üÈSEé¤Ìb�;©r•oÁí2½U1¤ÚÀfã*G0¸UÖ¡ÙlDEƒwp£‹£QÛ�ƒ�~ùf2§G&‘6Öƒwd¿W1æ(ó��:)dLÿjËþFc•Îá£øCvˆjS4ú�"u {y„\;(-Re!U„_c,¨œpï‹LÉ=|^Á—Éããd¾šM—ðð7óÛÙjö0§»;˜Ì¿ÂŸ³ùí�x¢4øZÆO sˆ ¶Dì` ð½-0V)i"“ù¶”[„­þ†Æ ¦@³W–;i ^BQ2µWN:ÿä¬(1 |Ú8�f[Hc1Ük+ évƒÔè=¸cÁ±kÓ=‘0†XZ7¨ÌFÅ»`,ŒÊÃóJ¬µ¡’ £xÃHþ‡ë�6\®Y[oxZtÜ6; L›I†‹up¸j\Ų¾‡àdsf¿Ã\çx5úÑ - ^„8$Ï*—ûZÐ’òÚ*§Í^�ú�P™– Ó#[yP™bàö1�靖gÈ–×Séhq\ÌXˆ¾3›M;ÞݽÎGß©2Ë<²†ý´ÌcO~óÅèáT+`�ô œŸøãnA"¬¢ÁOPKÝ �UÖ_¹cê -twine/commands/upload.py­YmoÛ8þî_A¨Nî¹J®í‡[/r@¶MÛàºI'[,²�MK´Í�,éHªŽ/È¿™á‹(Û¹½.(š˜/3Çóò �$ÉÏuÑ–‚åue¸¬dµdf%XY/eεb³™ÙÈJ°¶)k^ÌfY’$ƒWìCÝl•\® {{ü·wìc]ñ²`Ó.fð |•¹¨´(X[B‘ÐÓ†çðËÍŒØ/BiYWìmvÌR\�¸©dø#HØÖ-[ó-«jÃZ-@„Ôl!ÁXñ˜‹Æ0Y�Ù릔¼ÊÛH³"5NH"~u"ê9ŽqXßÀ§E¼ŽqkðʘF��Ž6›MÆÉÒ¬VË£Ò®ÒG_Ï?œ]LÎÞ€µ´þ¶*…ÖL‰µRÁ9ç[Æ0&çs0±äèñ¥0gj4v£¤€GL× ³áJ€”Bj£ä¼5=¤¼ipàx`Å+–œNØù$a?�NÎ'#�ñíüæËåí ûvz}}zqs~6a—×ìÃåÅÇó›óË øô‰�^üÊþy~ñqÄàjÄc£Ð~0R"†¢@À&Bô @ÀϺ¹\€K”¼Z¶|)زþ.ùK#ÔZj¼I æ ¥”ki¸¡‘½CeÐW+øZ6\iá?ƒÏ-AžÿXë¬áf5X¨zÍ̶AUnê£ÌÍäiø?çÚ ü¼ ¡�¶›”ÌW~K£dë¬0rh7´«õ�)ëfxŠ“à!ˆ×þÏ)úæ�•Z¼öCBZ#K=àɤA¶æ+�¥ÓiÅ×b:ƒB,˜~�ÍÔÆb:`ð—Ø€…bŸ]»¡‘]-(�:fóº.GÞÜqÏîìÊ~ø„g²7ÿ ÅcRÿQ¼e´|a´à°¬p9½ÜR(Îÿrø0—á Qn™\VµBç"Qc¸q¾îŒ¦Aü¹ñ£Œ€âƈuc(#Õ^Õl挞Íp”Þ¦ÖÒÔj›Å úZÎ áFµ°}är  ®Ú’ëR•Á 1›ys¬ª"F¶™ÁJ%x±e¤Uc¬:S°µëz„ÃÕ! ° 3!×;–¿ºlÞ³â†mÀçA³‚;ä•0­ªº­ˆC7zð G¬ÆÌ±‘€Ûlö‰—Mæ=„~ƒ(ÌÖ/ o-`´Óå ? WŸÙ‘i^¹7×å ƒ��P© Î�عþJ†YYo \†´ÉˆGsx Îìl ¯ØÅåÍYªårÍ¿KÕê·ï‡cvµ½:g˜&Á'k?x {|ìmGKCíq²l(¬!½â½Hï t´ˆÒyÆ.)cökÐà¦C‰;Yï�ßaP½?þ!Ö˜ "@Ó€ð+Ö@¢ÔBAnf©¯h8Fµ¬Qõï"7GÝ¢aØê¯âUE ƒ#v™ÅýùùÃ;UF.xnصXb�Ú†`kË:¦ÀâÕ6Mz’ .�T`ñÏ;‹Ïˆ.ï~8ŒL¸� í:D»œLºÓa½†�J„È ›U¸üMxoj­‡ÿ‹}mSpÊ7\CÎþz4ж`×ï�Å–dï¦�^6ä’`YE¢ Åm#CPi¬ò³4%Ÿ³3ÈW j„äY!Ñ�:ø¼nM¶¤…hÇaîg. Aþ ªÕ¾M×09uyÇzÖ¬Tcp$…rȇÐSµ¾³£FÝ�\Ž™ú²82›øJIè¥òŠÒ0qÛQÄ¥Û2bsŽÔ³^,0•{Ûb³ì.¯Øç/ŸJO^Tžamšz‰i'zçT8µË/¨XTË;á™Ec½ ŸFã pž»…'ãE1]6ËiX“v«ï"9÷£Xª½QQ‚®]ÓqÕ¾M-�böÛ@p|i¶< ˜øoèËÍÍÕÎï¶�H8ƒ}ðóž<_qÈZ¾SÌ )TÅ”†Súè¸ÚÇ�[ ÅVÔ"j,H�É�N¾±¹¥ïÆ¡L8ŒOØ“ë€3¬Tm à¤1e UE&À._ ®l}éQõ Ò¡l½ý¢6@÷0¢zé�‘§°×•…~� S,¤Âǯ,4’‘8û³Qø¸äÒàRTB!ɪ8ÈñÍøêÔ*!¦ l‹Š=y�٠̪v X–b>½ƒ`á‰ò¦§„ROýÕyÖ2÷PwôbÃé�,�ohØ[ˆï ²jE+f$ꇃZÇ-¼¦8Ìuïíä Ådãß*û!j¬ŸÝ¼ä9Æ�Ð RþƒG"maIÏ~j—쇷ôʱØ{S +¤‚ŽGôªnK¨äs|øòŽ ëµ0+[ ±Ö‘¸E[=P­°xMyÝ*ä ˜´6êð.Y9zàõø·©uÛs†ùÖ6²Îˆ ¹ x 8|…ñH”@U]it‹rnò[ÃLÊcÛ:u�ÚјlAp¿ª$©èi1»v˰xä@l‡ÁÕR÷CÙzGœõFæAõ "W(}—”uNE2¹ï¯Œ®Mö_qÿ~ÒîEEx?üÿù¼ml�‰ÜôkàææµÑ!öiFÅŒKÒ%8¼@ät?Äâ™tOZh*âeQDÕ*ù­ºÃ·þêþ)6Œ›qV˜€°2’§’ÑÉ�ñð$…õöïà”PV jÌÙ9°.ÁÑ�‹olôîÄ*ÿí @\�/ÁÕ;IM]—þÉ(œ:/ÆÔ÷ôÈ­SôºÿÚ›œ=м5öÝÔÓ| ŠŽõ©0 Ûá¿´ìM‰ ¦[ì�uŸmÑWøî¿ÈNÝÂ+šI±¹9Iâ/¥Ò{ÍÜ+>N 5õ²¦Aijõ #�ÔHû]ì%Ä—’.„*<ØIò×h’ÿÎÕ ­�ÆW¢lN’?ÏÑYWr“´ã…xfìV·ûyôšeìghûÀvzþ¡÷e^,…3dwöë3H?1^_î ê¾J‹ÐNkoæN8 GÈizÛ ,Üšœ*fB$(aC*@ZÖþúýÎ9 JŠ7¯ëÉ´EœûùÎV꥟«­.7F½·¥é‚9ûÒkÿ2>Xשoo õ›îí÷êÛ7o¾;}bÓ÷Ûðãë×»Ýn¡™ÃÂùõëF¸„×gg�7÷¿?¨«Ûkõöîözù¸¼»}PïîîÕLJ›BÝß|¸¿»þø–üÖõòáñ~ùóGzrvöÍB]›Úv¶‡PaqFRœGÎUØè¦Q­Ñ�ê¡[o|”î*Uº®’#ªv^ ÁÊ›­wÕPÒã‚èЋ• ½·«�*TEÌL¥V{õ`J¡ð ˆ{7¬7êåj|°xÏ•Ckº~&‘óG"•n»÷v½é•ÛuÆ+ƒS¶ß+=ôçí˜9õz¿Ñ½»µ×8Õ­ù¥¨~bmÖºQ7LôˆýБ^,´Qºd‰?´Ç»DÃáÛ(—5A˜Â‚½wM¡´7éCò¤=º ÇJ×¶®#2ñ-µ³ýFˆ«…zçz4O¢DEÉ„ÛZƒ�k8àqÆ®mÇî=TFœz–Î…:4Y´ÅkôÓŽ€ïM«­d�ÙjÏA¶`é[ãM³G˜wŸÙX+DÅC§[s™ük�*¾Ö%ã{‘JÚhÅ#qÈ"ÆÕÑÁo …c%>éÜÃ1q-3)Õ½Q¢4óGi;&ãÄ|_~Iæ"‹ùžÛ�oà†° ‚Aê 8„X`,F:sa>ªüìP®N/¢|ÞE¬2oŠè•�kXà �Å×Õdu>jsN„¤*�¸Š¦Afy4-Èì+Ýp¼ì<ê¸9ºhnEq>ZÙLÆ!ÛôaÊ6x(^,!D9uü›¤¶Ù†N6hð@*«3cŸö¡7m1õq0„þ%—´øµxšj•tc ”[¹HÈ0sxf^²zÍr\Ž™]ËÈ›ºO _±¤˜ç¤ø\ÅtÐ lm9¸! +[í?Žù©uáNÈ»î¹oä¶äÉp#ð9¿…�µÊópq~�›î¨mÊ®—û‘Üb„tí;µ�+ƒ¸Ag�!nÎ$&X0ˆ“†––ÒJ�g–Z–oêêvˆáÛQåÔð¨‡AÊa È“óCJ¡Y êšÊŒ¢ *wV\ÀÑ­A9t][ÓÃc±¦ÚYj:×½b'hI_¡%ñkšRÜ^7ýþUí >Y4[O®$0žWÞ8`«4×àuäÏ–"õ¶"o‡ÂlÅm£Çãˆ*Å1ð“Xþó il±G<åvõˆ×‰ÒËpwü5sÇMÀùÿë‹ œ1Ûž2�~Ÿúˆd¹T[Q1óºePÚè'ÃÍ‹Âé«kê½á¦„Ê�Î÷â†1µcÃ;5† VˆÔ�$~z»mhžsüËf% ŠB•�¶0°¼›t‚Ù˜BnÎþ:ädÚ[N»ÚMÒa,¬<—/Â%ÆK×™XÆdhƦšÏ`=dxŒÅRKã5+Òß‘íS�Z¨eM®–Ù#s(dGôv-ÌõZÓ× Wq¾˜ �4¸Þ…ðŠ-DÒ—n ÎF>ÃÉZ5zÛ“†�Y „ÃJIìX¼Àí%¨bD‘Ca#‘rrÅ>i“¬ßrßÒ$ÍÃ�û™4êÅ,Hþ”?±N¥–G°�r�|Å1¡Cê£*‹J­ý&ÓôÁFˆñÓ#Þ¬]ƒD qB¡Ïdˆ¹íÄÒšÄ$©¤Ë‡û,Ô"øÉ[P™ÀèçH9ÍÐ~ØÂÿÄ%�¹­2n²ï˜šYZh–]ˆ§8Aßn; ™ÐBbLH5†+¤IxÍÚ!’ñ,3žÞôH›"u®ÙDÌm9d9Ô)±YM^/(o¦bVÄÈ-Ú*CíL‘Š=ý`Ž“(ê#ƒü If˜¨f­”À_"ÀbUŽ»J”ÒŽì'yäûXjTìž•ˬT]ðŒNŽ“¹ôüöîqùöæùôܳu)™"uêx‡‘G¦dï$:iªÓp—®x‚›ÂÊœ´#Á‹¦�g¢�‰Ó\ägÉ‹¯1d¢qÚž' É�Ñ�—q1ߟý Øý˜ÔIºÉ¸“U¦Ð /rÿ)ÇáY$�©:_×([OˆAåm=¬câÎfÕ©óÊB±#?a™:Ï.ð±Ä5 æ«W¤Û~ôDGK,L¡Tø�ƈ÷¸‘a‡�èÀ®™k¹¸Ë|:®ÁжOs!µ™ 1i{ö³…ô÷˜©éwOÃEv‰D’8Zåk½sX~T…ÇÚT•骡M½ã,2PÈ€•œ7C'¶hÚ@õ“‰ÂëL'Rªý0 21ÆÉýI›L�<÷޼¡–}°#J†' Qö\RÚ[Yjg}æ‰Ö9.¿NÜ„�ìÄÕ'ä(bJÔ<‡í¿Ðúç+¬1M˜ñMû®‰õÑ Ì¬\Ž/­T¹“¥x™ãXpÐOæÿžÇЏø–pjÈÂB}ìPñ»È<ƒEii¦drÙ5€ìö‡Ý\¶÷É–>_\ôÄþšxî?¤ëZåدbßÃf�!祬ÒåcƧãÕ×…•“Á‡’qÍóU* ó`*#WåÉ‘…TÙÂlãð±ÆÐÄq½� ÀS�y6ehFÎÑÞ¬µ—ë’Ã^Ÿ¶ßœ¥î ´e=låýziw³»²t¼’Þ‚÷õº¥åÒØkÐvÈø'ZdÇ��&F©¼œÂ2ÉZÄMÿ¼ùs°ñR„*o€¨ö²÷Pž]Kw¨$ÌŠÖ „^ÑöÒäÓÚòhY™2%y)ù ‡uþ¾P×6ð„B—Œµú„>¶Ø�1> ¹ÚË`ȃ,M21³Ùa<)LÛ¢brOLç0 yARÒô=ýòWi©7óã%­€ØçWjùp®~¾zX>°5?-½ûø¨>]Ýß_Ý>.oÔÝ}~c|÷N]Ýþ¡þ¹¼½F/båÆò™†!*`$ªlm8eï u�=&G6�þ&a½Çåãû›6¾}µ¼}w¿¼ýåæ÷›ÛÇBý~sÿöWÈwõóòýòñ•wËÇÛ›¹Ó¾"®îá›�ï¯îÕ‡�÷în¤2Ê}WC‹tˆ½;ËKv¾|�™+ 8É»­·Ô³’5Bˆ¾ç ›P3ÛÊ.4,¤%ã­ ŒËÁ•v<’ãí ¯&óëÁã!ö�>$Ò‰÷V¯l÷»Kª’ ½I׳B�ÞÿA:Ì®iI‘ng(}>xwfÝX4E¥¹,ÆÙb¶Ô”mÉÿ ç )ç´Èn슛,kMS½ì賞îÄ_ßžÁøÓ:ƒÔXfGkö¢nõz¾¸¦£éªzº´[C—¿éžÉ‚S6çÔ]Èv“®—"Å„²´˜‚Ä´¯õr¯KWê*]q�l¾aÄŒAž`´×eð8ŽÞ/ÞÛ&yHÛÆIT®�«v¶÷jŸQ@Ýv«iƒF•{ ykm›ÁKÑM=tSóÁ•ëð�hïMá™Û@Xš€¡`£þøp[EÆ%²®ž,_ðÕñ�àQñt×i#ÀX¨«’�4OèI<¯¦¢šÅü§ uÎó<œÝx½xy”zÂrãœ,yÿ7Ýó½TmZ,›îJ#²oe3qlÏÁeÚŽþ´!.ŽÄŽMY¹Uw6ÜW¼&¡Tn ¥Cfl˜î2ÐÖÿêv4yȬ6‰ ˜Q�Ô⿥蚴ó»Þ¸üçuf|Lx8¡!KÊ�ÈtW!yÚ®dQPl-Ki,YÌö¨Å•©1!ÈëhO«[cí[F–Ô᎖‹I:x?ÝýÄí)p£.�‚²T,Ž×§«}l ¢{Òz2âØKï²�Ëú¸QŠÅÙPKù �UGŒ“©» twine-4.0.2.dist-info/METADATAµVßsâ6~÷_¡Nz�Áv~Ü¥‰§É4…dÊL˜#×ö „½Æ*²ä“dÿ}we G.a:åÛúvõíjµßÞƒãw<üŒZ%ì$:x sK¡ x^øE'Á¨.KnV ëj)!u¸ÄtÎj'¤p,˵aU=•ÂBÍXÅÓ9ŸáwÄ WÃ~ð‡.!¬ðS ç*›Ä±ß(2À3W@¦S ×µ+´IXO+.36ruž;ÆUÆ„ÊÄBd5—,ÕÊ1­�6¶µ¡äB&,óv¿Yo‡ƒ¡Ñÿ ãðË组�tmRè†>”G¤—GÕz-ªV‰Š´™Åk˜Ý,‡UãÑÆAWrkE.ÕWT»®3*– gX€ÔâöN¤ ¬G F}v]¡Ëšâû5îS&(wKn` Ý1à®6˜þ;®f5ž%Ùݨún€c¶0ÚÑÊ:( wÏÓÁhóð÷›CdøN=ü>ê½|'Týô6e‘m1!ôò–Ÿ^î¦�xfxY’ÙvN†þ€ÒÓé¡xŠJ®µ‹~9Øâü`‹‹ƒ-Ž�4é—•„ç‹I_ºmâ?Ã×Z°aóž°«KŠº65¢"tØÕtm\ø¸ª¨çÁ“‹ŸBc]ÐÖ}x+$.Üõ»7£›�Çž°.aÕ/d®Ù‡«Ëãè<:þù%€Ú6;[ÌÙyPKù �Uôú»–#twine-4.0.2.dist-info/top_level.txt+)ÏÌKåPKù �UÀð~΄twine-4.0.2.dist-info/RECORDuÓ¹¶ªHàü> œf‚@@Q@”# ‹¡„b*Äħo½Ë¾Ý'©ð[»þo<Ãüǰƒ8Ž¿ú…¸• à _θ¶ÈÆ8 ß´’)C"÷ÖÒfÚ�‚wÙyZÜ{Zã{Ù&h–cá—Ô&°û—¤Q£;¥ýz>–�¬ŽA3Ǻ¶5ñ–Ü ¤Æ#R‰d¦ý® šù—”Œ¸üP¸¶ºš~|5ò¸%ùEà©ói. �<×üõn;TðêpR¯5ÁÒŒüR²~Ž2�šH\¤iVBðÕÅj‚ŠeZï4-Þnœ�©ˆCÀÉg‚Yé…€{z Qwû°²z­†òXïD½v ¹xW÷X8Ä1•Äá’ŸêNå°�!Š`%š{Y}’ÕI> j6иGŽu2Û#Í“ÐÊá­ÍÈ“tìuÝ›‡aöÍG\'DÐ4Åü––/¼ô ÿ��¨én_IÕOÈ80Û�ä·+0ºÀ«[Þ±ûm­ÙÒ�#A½Œôè1–��¶;®3¹¤F“Ýz÷ IÙK¦¬#W[i™ÒËúoÊÖ$$‘~WîøÌÈzV¾DȾ¸‰!m†p5SÀä;¶æ¶âsNnŠø4oÜåFÐ #¼[1l>�î¨�`ÝKÕu˜¦Bµ¹‰ç4Òs€C¾ðý\µVq0šÏˆ(I_Î\Ð|8rSËÎËh‡:ëzë÷Ó‚bó~Ø9ÞEŽu®¦€ÅúΨŸKDqïqžìnøB}“ê3Žp®:iã]¾ùFZû­Â)ÈY!Ǿ}pîʳ|Z”ß;�¡¶Mºüö6%9^Å9-ÜGs�óªL–r(Ïl~”³`wU²aùi¯JÏ3‘(æO2+AVxécmº·CnW(V(¨ïF¸#ê îÛU'çTä»­½æLð"#þé  €7 †Ï_wO‹DÓŸ[Ý„æ6/E¶M½PyðhcµÔyM02ÅýIŽ}ƒ’ü„Øt6WÎ’AÄØçêÒV¿ö Jº”QA5PSqãÐhq6!rïõ ¹/ê‹ùz^ &awAYæJw|ý­{úغz!’¨UWÛì#-qÀp¡%éXYìgç@ŸRQ'dá]ÎT[M ”7 +éa*U[EŒÉk¥=&’F0î :z¥2Q…ÒÝCÎGËð? ntÝz›Ì z›îjNó9`ôç!¤ƒàzÈÐÚ¹lÊrC¾å²=lSDÈÌ"èð°Ä=‚¾}á;~ãE¤=vzGñA$Â&ØsšÚ(�zõøÖ×qŇi˜^ι§?7ê§0êãLÏ;ú w«0 äC]I»šÝ§�;_„šò‘,Xþîñ!ˆ¼ØA3!ü{újïiñëPKÝ �Uóä‹’?¤�twine/__init__.pyPKÝ �U\¨¦åä�Átwine/__main__.pyPKÝ �UZ(N¹c9 ¤�Õtwine/auth.pyPKÝ �UG�}š ¤�c twine/cli.pyPKÝ �UÙ[ãĶæ¤��twine/exceptions.pyPKÝ �Uño”˜9 +¤�wtwine/package.pyPKÝ �U¤�Þ"twine/py.typedPKÝ �Uç—*2 "¤� #twine/repository.pyPKÝ �UÆ¿òEŸ í/¤�o.twine/settings.pyPKÝ �U�¾UÐs*¤�=;twine/utils.pyPKÝ �U­ôÓé ¤�pJtwine/wheel.pyPKÝ �U¨‘Þ¨¤�±Otwine/wininst.pyPKÝ �UyøÎž€ ¤�‡Rtwine/commands/__init__.pyPKÝ �UȽòà _¤�?Vtwine/commands/check.pyPKÝ �US�ÂÁX ¤�…_twine/commands/register.pyPKÝ �UÖ_¹cê -¤�~dtwine/commands/upload.pyPKù �Uä’$ŸÇ ß%¤�žotwine-4.0.2.dist-info/LICENSEPKù �UGŒ“©» ¤� }twine-4.0.2.dist-info/METADATAPKù �U¡¥1î\\¤�…‚twine-4.0.2.dist-info/WHEELPKù �U†¢éœa¹&¤�ƒtwine-4.0.2.dist-info/entry_points.txtPKù �Uôú»–#¤�¿ƒtwine-4.0.2.dist-info/top_level.txtPKù �UÀð~΄´„twine-4.0.2.dist-info/RECORDPKÎFˆ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/fixtures/twine-4.0.2-py3-none-any.whl.asc0000644000175100017510000000001115231700043022611 0ustar00runnerrunnersignature././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/fixtures/twine-6.2.0-py3-none-any.whl0000644000175100017510000012334715231700043022070 0ustar00runnerrunnerPK¢|$[š%wR'@twine/__init__.py�TMoÛ8½óW ˜‹ 8rÜÝÛ,ZÀMRThà±³EQ-�dv)RKRqüï;¤$GÆöP_dÎß¼yCÎùÆ4ç ŸPAmŠV!”ÆÂf/5&Œmv¹Ñµw`Jð;é ù¿¢BAšv«dË/i)½6ä*Ð ©Ü "Ûy߸Ëù¼’~×n“ÜÔóæÐˆ¹Í¥s-ºùâíŸ t¿üæÍë„qÎ;ƒ+Ó¬¬v^],þ‚k£…*`íÛ²ô±¢Ô…|’E+TlÇÊmë�u쌒oeŽÚa­.ÐR‹Kj‘>½gÿ uÒhx•\À$ðÞŧS…ƒi¡‡HFë°c©”Ä%>çØxºž®­%…ÎöÔV¼¦/’P‰o} ³%Ò4QšSKãÇ8>èÙï÷‰ˆHc«¹ê¢Üü6½ºY­oÎ mŒÐ �‹ÿµÒRŸÛˆ†ÀäbK•Ø�LTÉçM»·ÒK]ÑèLé÷4gªRH×ñvÂÔ�WB_®!]sø°\§ëÕøšn>Ý=làëòþ~¹Ú¤7k¸»‡«»ÕuºIïVtúËÕ7øœ®®g€Ä]ƒÏ� ø ¤ b[#žª g×`.K£ºjƒL+ó„VS;Р­I=4ITAU”¬¥>Zþ×T²L(•eð& èdzÌK¯0Ëøl0¸¶®…=ŒM­•ããS§ž±I´~gìØ‚5-ËØÐ�slÊ™Gã”±¡ä'‹ðö÷�¶ˆ¸5ÖCÄ‘´ž6w0¹ƒ#w¾ÉЋԥ�÷ÄË3X\L/#Ä>¾û(¹Mjz á÷bÍ+Cåð×™/1ìXãÝ/ÜÇ&<>|Ê„sH¥Ži¤Ì°—+£àŒþ;Ì/áÃâbÁ�ý<©ú�ñ�kQ#d£Ùž¸{kŒˆ£&¯Ægßi„žJ{H ÕOø øôûâ1:‚B£3lر� }Ù„±ææþüáþ–Ò&ÓiGL9ôÂzžŽ ÿdjlHÚÔí”�ôu‚²·F”ƒÜfpÅŽF�4Â:Ea'/º¬óÆÃMGMRv ”ýPK¢|$[o2äì+twine/__main__.py}SQoÚ0~ϯ¸…— ±ÐµoLLb-ÓÐ*:5lÓÔM‘I.ÁR°=Û!Eÿ}g'ÚUË øî»ï¾û|¼×F�×\ŒQì@ííFŠ«`×Rí5/7./Þ]Á�¬Ê!±uQØ`@€[ž¡0˜C-rÔ`73Å2úé2#øŽÚp)à2¾€ÈÂ.ßÃ^Ö°e{ÒBm�(¸�‚Wø˜¡²Àdr«*ÎD†Ðp»ñm:’˜(~vrm¡á�Šs°VðÆZe&ãqÓ41óJc©ËqÕ¢Ìøvq=_&ó·¤Ö㿉 ��j®iÎõ˜"1[“ÄŠ5 5°R#å¬tbÍ-åŒ,lÃ4KÎ�Õ|]ÛgNõÒhàsyÅ„³IgÉ"Ç�ÅêóÝ·ü˜ÝßÏ–«Å<�»{¸¾[Þ,V‹»%�>Álù¾,–7#@ò‰Úà£ÒN?‰äÎCÌ�a â3…l…/xFs‰²f%B)w¨� õ–w“†äåÄRñ-·ÌúÈ?CÅõ“ÚzÃûÿ•,Kâê�fo‚BË-ؽr-ºðLìG�1cƒèÜGcMÐÁ.°Gg%Ú®+‰›öíãí­�Ei*ØÓtAŽmÑÞ~p"&Ð7€¹0µv†1 ¬ªµ–šL ˜§ÎÉí X#ùH‹¦KÅ´A_MêâLŠ‚—D‘ÊÚªÚFÔÍå¬îZ¸Ï“’FW@Ë ˜Í6ÝîáÝä÷ÐC»7Ñ^­¾Î})3.y"À̘zë|õOŠÖ@IwÙ §)Ö¤–Rà±æˆ™zÿ£c›û.1r=âÖÍq.¥ë�¡í¨MšÉÜ1öUñYø%Tm4óíÝÖøáŸˆÎj†q‹:Ö¶÷{ Ñ1ê¾"|rzÓ4«˜1i÷÷}˜ÀÓåáxj©¾àñöt¡ÖÕá—x :å52#Åáxv{§åŒWniçýùŸK|ÕÖgóþwD—¡ý1¿aw[m­EËL[Ï è+`:…0MÝHÓ°á6¹�Ú—1 þPK¢|$[:S~`Ê + twine/auth.pyÝZmsÛ6þ®_�¡ç&ÒCÇisÓêÎ�óÙJãkjûu:×CÃ$$!¡ZÑdòßo/$ARŠ�ë§ótŠ‹ÅîƒgäëB”ФT1Å×lÄÍïE•'JˆLºK¦ *ëŸï¥ÈÝs&–Kž/Ý϶µ- …PIÔhQе{a›*íûªÌ2~´”̵Â;ý{䤕ì?“JF´R+3ާ®÷Éúž³\�–,…8Ífe)JBpN6%|™‹’uG¥L±DÅI=j4: ضD-W 7Í ÏS^B/è\°ú%["rrµ==!I¹-”X–´XmC²Yñd¥‡åä%{~_ñLAc™$ P¨(’¿~›1œùÍ÷/>†D2FŽŽ^}�ø‚¨hþÛÕ,>}3;ýéüâÇéˆÀŸU×j¦_镨ÃÅJ×íBüdÞkŒ`jfĨrkvÈ|š\ìÍ>&¬Päg‘V»굨òT7OÑöEI—k:{�D<°²žÅÙø„æ¬~íÏàZ­ïlx^;‡™—‹\4VŠƒçŽÐ1 ±�¿ÕïÆqœÓ5‹ãÉh4¿üivÿònv}qòól öÍsšÝHUÞÂØ Ž•øÀò8FS2Çg Óà„.LpôЬy^)&C’1õL©(¨A³Llp‘%ËÙ†f‡%+2š°5¸ʲ£_¸ÑD­(ø Ýpƒ # ʳÖðJš2ð­¦#Œ(A¬8µ´{î…?¬r2"óU*�0Q•$©JÐÃÄ'ù ÌÅqBè·ă$ßкb(lY2ŠÊÁ@ÐÑ §ƒ&#š¢Ýµ­¨@‚Y3x®¸Ñ¥áHmÄÈZûzv1ûõäm<s={÷æòíYcv‡Dþ/e™¢¸ ý·cí3ÖrǯÂlå(É£HƒçyQ)ãñ)[�8æ9Wq<®ýM²l’J²ýµ¸Ô^Õì?:`ˆ¶‘Q¦»zh�òüýkêÉ�œxèëýN:tp�õbæe%K¯ªûŒËØV{à5S%g寀réOÔÄLžˆ–ÚÅˤäZoór¿ô¶¼vNÑìø5±Â×LJºì›Ë ‚† ¯ d«›Ç,äֈо2,]VI³{�÷pP™Ör¦1Z"@¯”*äôðpÉÕªº�±>,àø9ÜÐ’�ËJvÈ¥„åðè»—ß¼²h¦#Ç“Îsu»Û^'p¡¯%TÁ>x'T„mÿ¤µÕöx`ý &àä”×ö1÷:—¾ãG­é¡¾Ã[=a–Zã58L&£+[°OzmÞÍÜ_ìÚ(¦÷òŒÖô‹•1`\Ô4ð:ž4çÒ Lò¬-D¹d-ø�z[ò`³*ÙØ†Áìcç8K %VÒèa'½¯€ì¥g<“Qýó€œç0œ¦D,À˜ è…b`ÇyBpïÉ z08>~�ÝÍéð¦n‰*Y¦ ¢$ž¬Ì4„‡ M ô–|¹B|ƃ¬}�“ÅèmzÿÛÞ÷f>¿Òº *¾1 ûg_èurHt¬íÒ´MZΪª2×,nüç[šº¾“±�¯›­ŽU÷!`?<ŽwÅoM$šÀ؇°£ ¾œ&]³BH¾=Õï›~\0ÝgÄÙ ’yðµ@hÓÿ}ÿQ”¢`¥Úš_55�²²‚øNVÌ.Œzx„";Áë!S+f[‡”whÕZ‡”whµãV74ÛçùÄÌ‚ÜyCïòé”Tæ:z¨ƒXrՉ̰#mÞÒëO‘õ¬°)ZŒ–`(æAžm¢Ž¸_™!Wˆ’TÀ(½ÞVpé86r£‚–ç YGÐÝ^œ»³I‚œcEt ìˆCró¯�ÛµÙØ TZ È¡W&ÖuͲ­¿2¤_<¸Lk�ioi혰Ô4hhŠGÃOÛÝŽÝCØëR�{ð»´p¨{$œç@(xj‚µ2|ÕW!H´saRÀr�ÙC“šT-±ãÁ¿Ê‚š€Ô¶fj%Ò: “•’�“ ùwP8€1!WÑi(ÖMsÌßN{À zÀ´ÆLåÈUÉÀ¿œ±`èp¬i/š¹Œ‘5�'ÚÐ` NÕ Ñ�ô3¶ ä½®ÝwÉ*þY4êà 1¸}ŒcÐ%bØäªs̈û’Šã$¤Ç�tŽBÈ0 ƒ�ÀìËí±�1柱ÍgcQƦwØó“§l”sùGlÔ×Úg8ªöÛÇ�yŒ}\ßž} q·Õôa½¢z1<5vJë¦1ÆUÏ&þq„0›[f§�Ù7 „Ù¸N1Ç“ äÈ;’ÕH"ãK}¤þ»áA %ìÌ‹›2,5 \«ý=™ ®x?WrŠô¨Ð{¬9Äu5-â:ïsèñ%¤_çg 7g˜Œêƒ n¶ÀÔjàà;=o ’[�³õ¦Œ¯h¾´'7Â0%WÛ«sW@h”sˆ§pöqÌ7ô€†uíP›*2ò'QÎT&’Æâ2I,eÛèÑVwÇXl[ÇÞŸrH@ p[ÌM:$]V….=ݹ¬ó“Ñêóa|(xšÒ*å,OØ]Ø’f¸€b ýåùÙi³ú$Ãj¦´=Ü`œ »±‚kˆaÁ°ŒEPOß³OO“ eKYhZ§¬ýµ-8ÔµQ©ãWoL¤OÞ6 bŸªJ¶²¸Zåö¶èAX;On‚Z�Û–…½ %þ¡Ê±K({åÚZÏfb[‘ÜQ*Ø;­Îuµ 9$²¬g°»àÔm+ìñé9)2ª`­kí•68;¢šP0Þ¼±¬qçžÁ–pŠƒà|kIm‹XêÂI‡¼ýÉî/9½Ïl�P×XôŒ—õbÑ_ZжòåOìóÎkŽÍÆ &ò¦:mh™ƒ´q0Ç‚$Ëx)r¬�:¸n¶ eö–Áćñn¢÷¡î?¯ì­ôÉøí[`ÚhAT:à ¯�¡C.œ|À�1gÚÒàœÑw K€Ø•¶oÒñ¯ùªNyvV0šjñ=#è�ønþ´âà™´)]eŽ5P§b2úš6¡©sÔÚ>â�¨»twùGØÑ¶¢b)þ„ÖmÚ/'ÅŸu@ëÕs=Öcëß¿x±?høõÓ鵟s ¢™ì U[IƒÜ»$m×]5­rOI·ƒÉ'šÀpâÇç—­Äð&h•Cn«ºä³—?®Ü஄÷±ž/Ä8p=×úÖÙú‚œ§Ã†L؃Vc�¬¶S‡Ýºü»bæÊÓ)…AQ×ü�È[ÑyÝí:¬Åe]„šöH�»“^èÖ~ÍÖqað¾RC°œ¢É^ÜŠ’ch rô*zé§úøHGüÌœfדý<ÏðpWï­ @íÏ€€˜ | ½¿–�V,6üŸ¹]«VÖéíZžê¦u9gPóf’¶¿6•)ë­õä=Îá̱ǰ¢V ©1Ë‘±Vy[¯?ÀÕê Œ¯wµ/Ôü‡³í¾ZÙ�“ƒ•÷=†«%KH^½e îh�ƒàn×Ð*‚š[¨V]ê«jz;¬Ò½OÚÆžU†¯ <«Ô’g•æ“‹Ö4µïηÝŠõn…È]ý•Œ¾£¨£åÂr¬�vô"ã WÙ“°ž²tÒ%ní_ ÿØŒžZã~�•LÚµÑ�‡L'µ; Wz5”œ¸ ~ëf8+0µ´)gl°œ„½î ˜Y_o4Uæ½~ŠC÷]bdÿmûjã|¡ž]ù’•Øàýð)$âˆÚ7Q…ØrÛwQ;+Œ/‚^Ã�-~Jõ Å}ž’ 5�[ÄŽŠt'³è—/^~ûüÅüšº¦5•lLhnŸ Ó–ŒÜÍüv²²I¤¿Ç’Þ½}¸‹ÓÅϳÙë“_ÞÎãëÙÕå»óùåõoý+9Ós>{·»›W϶×ûöÂjìîºZÄ|a—êc{hÃܵþÀÆu/èvÞ\£Án6uAí÷ú¸Ñf7{ÁÖþPK¢|$[˜…pÇiç twine/cli.py•Wmo"7þί°6ª ,w—~JËI4!:Ô" =�¢hev‡Å�±·¶7�òß;³ëeÂ)w|±=3?óÌ gìRg;#ÒµcÞ½?gWZq™°¹ËW+×:k�±ƒ²�°\%`˜[f<Æ/Òeƒ±B+ö!|ÇÚ$ø£ óZØéœmøŽ)íXnMËVBƒ¯1dŽ Åb½É¤à*¶n]\ã�„hâ‹7¡—Ž£4Gù W«¦ã¥Ãkç2{Ñïo·Û�ž†Ú¤}YJÙþÍør4™�zèm!§$XË ü› ƒï\îÏЙ˜/ÑEÉ·LÆSxæ49»5 •v™Õ+·åÐJ"¬3b™»¤*×ðÁMÄŠ+ çl<ØÃùxÞEŸÇ‹OÓ»û<œÍ†“Åx4gÓ»œN®Æ‹ñt‚«k6œ|aŽ'W]ˆ^_3Cþ£“‚0„„›8°Ò¥C6ƒX¬DŒïRiÎS`©~£ð9,³–"iѽ­H±Ž»bçÕ£ÂÞ§�cܤ7ªµÔiŠöÂX«•H«]»³­•ÑævÝæ·‡j×E‹ÖuÙ"Ï$´ZbE²áSIªH¨•f¬}Þeïßu.Z ?^¹ü’bnÀñ„;θ­w£j·ÒÂiÍZ¦rÔˆxÝü?\czHJ0ûþ�{ˆÏö¦ÜV(\!@– ö8…¾‹Ì„v§Õj%°b%T¹�Hç.Ë]»ÃzÙD+ïõæ©—(B�J½ä’6­–˜�ü‰ YðõIðÒ“\—çíNè­T¹QHà KtlC¡û ú’;°®o`0ûB ®ÝFþ�¾¿¶VôâûW¶‹ÝÒä%14†È±a¶?‚edŽ�[�ìÃÔDæå†¶b-‘Ó±NÀ†l�%¥a3Ö9±%°%·eº]Og—£èrz3�Í)Un¿T ÌgmCPOÂh…)-¨͈µ½DƒU?#‡ó¬Œëš;Ö®ÀH1ó%Ò}Ó_ÀW—s)þƒ¢>¦SŽ%çü×sú¾t°09t÷‡JGÅË?îœiyº,¨öƒ.»æÈæN­²çç 8© 2j^† ú[ƒOŸçƒ}‚*%<� XæipÁª€*è¾%OùJâK™ÃÛÒX=©ö�¤ÔÛ·UÀ„°X¿-S­Ž¹,¨¨{­ø²_y\11ËÐßY"["bWf µª˜F$Ô¿DÉOºMwÙv�h³ŒnR»¦%òk’òöˆÑÑ›Xˆ ,É�ìÅ<ÊWôÇ%Ü–€]%))sXNÃÚ•:�‡A °ÕP-ˆ*÷"ïbpÄ�BÜWZ<}t²Æ6 KÅ<ñi~ò°�ÜÚøF¹ )·>•†O„¯P´k½�œØÀi�å2îÖoÈ5*8Jõp’Kù©^·;¯Í¼Òå'£µ;�QÁû=`ÝoY{©ÈWt‰á‹È[¯ŠØ­E>\¶ìÔ:ï‹Îy�óÖ/gʦ�šÔvî÷ÖƒGØQíÄ‚LÔ5v,µ4(­zXI�ÎhF«U°M=ò´Pª7iVBÛS{=§µ\‚tÍÃÜHìµçÍ-QåàCÙ—Otýß›~õ&œê’v°o⽪‰>o1åré.N£C˜”—ÒËÑ"õ2\_ãÌîâ FÞ/T}=9TN·ÑFg¯UͶ¯Åo Ha¢ÝµÆ©jDõì[·“é‚�'óÅðæftì¥ÊVHÐÍÝJ«³ÇÀåFyIÏ(”;â"rÑGná?Z¨ººÏ/Èí— D¸6ܵÙSrש ôkBò-ÖPKæo¼¦6÷äãEA*|ÃÙ°ò-ÅœIl‚› Ú³§#�E×ì¢ Ýw¶~@jtž ‚b OØ š%¿ÑLs\š4ß åÛâ¤��zcA§¡ò$‰¸—m ×ëU¶f?�)÷Áë¿3X?7uìž Ïå¢ Ë(zaíçÃ�¾t‚ª�}�gJ÷üTÑȰGÂÏÕÏ£ãýê%Öiœ^�9húk�ˆ»oGåÄFãX1âþ€Ÿ>N ÃñZã�;8ÌPÑtýýÆi¾:vyúùÝííl4Ÿ7æ3’¯f£¿†økl4;qañEWÚ‚Ý]¦ª¹Ÿô­çÛëѿܧYxâ…÷¤úÕöUž´sžt‹Á1,/úPK¢|$[à/e§r™twine/distribution.pyM�1Â0{¿â” >�Q@AÃ�ƒ7’%Ƕö.Hþ} HÀ´«�y$¯*ç¨Æ8­K�“NÀ,„;Eš÷r8ÊÔ :¾ÇôQ!·b×¥&,Ȇp! ?ÿSe© µ¯­¶ûÔžø9{öÏ[™eð¹ nPK¢|$[ÏäúÑš‡twine/exceptions.py­Xkoä¶ýî_q«|ðL0–Óý²Áõ®wÑA7¶á7’ÂKKÔ˜�†TIÊã©áÿÞsII#ÍÃvƒ ö¡yßçÞC%Iò£ÉëRRf´J+½ ù˜ÉÊ+£Y¡œÌénM~¥´L“$9:ú†>šjmÕâÞÓ÷ßýù/4šf^T¥ôFã¥Í…ËÌÑ7XùEeR³ˆZçÒ’¿—tV‰ ÿ5o&ôOi”Ñ÷éw4âIó*ÿ kSÓR¬IOµ“¡ &G;Ii¿¬J%t&i¥ü}PÓI!âçF„¹cI`}…»¢¿Ž„ß{_¹w§§«Õ*ÁÒÔØÅiW¹Ó/Ó�Ÿ.fŸN`mX£Ké(ùŸZÙ*QÁ˜LÜÁÄR¬ÈX +ñÎ6ve•G˜'äLáWÂJHÉ•óVÝÕ~©Ö48Ü_€X!àÉÙŒ¦³„>œÍ¦³ dü4�ÿýòfN?�]_Ÿ]̧ŸftyM//Χóéåî>ÓÙÅÏô�éÅù„$â5ò±²l?ŒTC™sÀfR (L4ÈU2S…Êà—^Ôb!ia¤ USI»TÎ…²:‡”R-•±�vœJ� ÏXO~]ñváÈe¥€5s.¶OmŽº«ñ»#ÂUøAÀ¬¸˜�eùjÙòÎ :-×2GÊ2.=þ•ùh¨v£ì é�Ki;óf=Ü>ø„º´¤‘bÇ9F@MùÃþ©F¢—0»I(éÔªc "< Û+aËó?�)}0â�næË4‰È*Ž'üU¼L”�V±V8�rp|˜®w‚˜'�¢à± £5눮&䎯�Éð‚‚î¢g¨ÝNŒÈ²zY—ÀM %�6%̤?Rõ0h¡Ï D¹_ך°Ç+bÈ«¥t�8î  ç™yf½‰ÍvB"ýLûQî�á¦Ó½kcôŽ|úƒã¼ü×f+÷ÐÛ[œÎüííÈɲ=ð¶ ¿J‡zè=%Éî¢.)ïé—-Eœ’Û¡�ÑŽ€½ÄmƒªØ¢QÚTcòã‡öÑ7޷ǃÆÃQ´¶¹ë‘Éßc]5œ + ¾ÑÜ�ßfV¡õ_¹ÉàA#öÕƒ-jÝœ‡¶¡P{TëiO ÿdŸA š6µ‡xÞ‹Ù¶¯î4X˜Å›òÝÇ|ŸŽ'tœþÛ(=Dqü•âóâU{ m‹ê(žÝEßÔïðånX‰s?Ÿß¯;�j�çryššÏl ¡Þd¦äc\¡›žz†&3¤Æ8&0­YèíÉt‘Nèë×vˆ~ý:~‰aNõ 0Ÿ©ŸØß6å>˜æ{ŠÃ®˜¯}^ùuló@�ùcÞ8põz”Yí9Ì•7+1uÚ¥�¦@ÕÊ2~iøù‹Zßæîuürý°¹3é�’^ÖsÞ~©y«±ù¸ó&=hûS�¨ œ»Ç®QÖ �>Q›´4¹l»f—WìéåÅÙüf~yoçlzñ3ûq~q6fäÛˆwy�ô‘1ÊPD(°k!–™$¨ÌE/ãøJW5_ ¶ÊÞŠ"vX.ŠM\âI–@^X’xW¼¢‘SþöËŠŠ­y¹Nâ…~�3ýôk™¥ú9ÉV+ØD¿f¥~é©§²^äE3ƒe‘mXµË‘,5;Mwcv‡Õ(áß ¾ÑM�' �—9É“1S7»\D=�¸@îùÊB·�xÅ÷L¿•*-g‹8\뉼ˆS�´ÚÆ©ÐR—QT=“%ž~Ïøv-D2I¿ÈÒ{±Ëy;½P¤‘=•uŽ�M¯ü4}>? ^Ìn¦gÓ›iðÓìêš”ã {3`ðçù½±~<2�ÇúñØ€cà‘y<�ÇÛÁàl~}Üüür†;}�“ d+ &¼SÉŒÿÿU‹‰m˜¡_ÿú ÏmðQášýýìP‘®úÛ5br+\~Å õg%Z=þ>ÎíA@�ººúD+�¿ÕsA j£Á`‰% J¾46Ä€Ôª±É_ð÷TnàyO³ô¡B›åÅ"® ^ìõ<‡g°^DÆÈÑ"B@H@yYQƒ�ËJ³t“|ÍÓz#@µ}®yÁà t6`yÂCðä =l®èÁä�B>ìœIÕ½_಺ËñvÈâÑ«‚ù.â$®vº«uV£ù"&ôÚa®`¾^ø°è0U<$M=�Pƒ×üýÉ#_˃~ QÕE ?>ØíÐ{óÏéä>yÿpòGÿö[o̼ üƒ” ¨AÁyÎîî-¾â[­æww D•(Ë»»—dŠ¢3ë±Ù]­‚–’ÂÈ/oÈvÐ?áP‰~Q„èlÁaã(ø…ŒG(–�?®¦¯ƒ›Ëàåôé�Óg³ÆšM<0VæÄ&ìåì%;>9’"ÐlÊM þuÆ”~"58O¿jÌZÖ‚†ƒ¯�Äçô º’¼*˪7ÐBR|õ¨0D¢ ‹˜<ÎÚ¯ Í"ï­º^©M‰í‚Ì6–×i/жÐ׺×o Òžø¼Ë1ÞÃPÕö(fÆq!û(ë�týÅ>f•“èÙP{„žMOpJeÎ�Y/û]ǰÏö-ÅtlýSÂíZ½²ôO/jÛ|׸;jIÖ܃uE±¬‡L™Cj–m6À™ÅÔ®c€ˆÒå|•¯‚2^¥ò=¦K w±ƒƒ”òç<ªRÑ,-×üøñwpÔ+QZÛ.~/ŽƒöTË�#sêt Qâ4®‚`H#„\$Ëqó†t7¾Å 7,ë ‘eæµ|NY_jjà¤B:îÌÝ»‘™¦â"Kš^_“ ‰¦~t¼PVú�Á¯i˜ª=7r—)>a…zr§5s0ïhDàò`î@— äW1��.€©BžØ–Þú�L†o[<ØÂµ}ÓäÈ·­]@)ÁÕZÒrEü-‡¼ ½ÞU-9weß¿ºe �Vu¬â�âù»Ë]3iV““Ó¢.ª¨AÐ`À> •µ?Àà žr,h»ú Á|üZ£â�´5X¯!ô€µdvp Gã-um›²/g%ò¿’q᮳¨±j¬`›†I9nò&óò,Gá;;`3tÑaEe_£ù²ù#²×c epêc‘NC¹ØjOøq%6åpd¶À¿xÙ�ê‹4*±ºžþU�¸tI1 Ì´VÞ ·=§ê,“–LM•BðhØ–ï,{&`3ÒY Z±³ËóYQdE?½�Kauºüyú–'qtfµ=†½+ñÏSÐn—Dư¿+xìw_âïì¿î¨€µŸøˆê~°î·1ç½JïSHêZLAêÇ«¯f˲Ã6µÓˆ¼2Êì;ñkÌê4GàÈòš>�ÈÄmˆîRTu^eYR2±‰«R÷t'hK–ÍÈNËv�£‘ Æž—…Gʉ¹8òÙ 6øñ?uÞ rÙøÝðsÐô¤É-B†,Jº*àI’m©¿EVßMÒ4&Á(Ä.êqÓi‡Ôè=dWßbšúñË¥(0fVk.[D¶»>P9áBv�Tfæ³²­CJŠ 'µ×¢lÜ H+«$ͦ*ËP? Ø>~+|vVdù‘AE XwÕQœÙž è‹Ä;tHOÇT7(µ¦ñ¦Ã‘Ç#;1úH¸¯Ž”¢6vP‡=-.æ=ôF#öçÎ"ê·\!!ʳ|Ø*OÇç\K8`ײ–"÷ÜaÓe]XÍûfúÄ?j¡,P’AÔc¯×"eh¸¢Á,[­)˼æhPŽQ¦-TZ­�´š’tÏ…·%%œ{DŠ *,»”]l�íFÿ1Â;ŠóMö– fCð6ƒý@1°‡­T®…f™¡¹à\¸á½ïLë=éOT/@ ßÖ þ_ñ�½NߤÝ>z¾ô–ý�Ãûäa¿fñ'âÊÒ«ÓB„Ù*�ßãåè O'¶ÈñX؇{±û¦øè‘2Á3Z´fò ãÊ^GÝ î�ÖÎѧ,§à[2%ƒH…Ü~¦Eû¬Èêœñ’­ðáÿëJêo ËPŒdî„Ò$J}CÖè+$¨n ûkÂb“÷�U„3™‹ÊÓÔzL"·d,hˆÉ)%‰­¦–¦çy/DÞsS'U �‚©;¼£iÜBœÊ yc™¨!ž†I ÁÃt­=·aäå�Ï&ó‹ó˱J»UèjŠߦ¨yV)^»%gd¨4y,wÅ#p ½þœUZÔd{®?ÈÍV÷¨(Ó……Ý´ê€MÙÝ]Ó'¼»ƒšìtŒ%@DÁ²8º”�ð !ß{PÉkt xmE÷ä §èêv,DÏŒ¨BfCÎ**‰b¬¡¤|0ØQ…bÏ ñF‘6Ñ·þ·vvt ªVÄ‘¼I’‘‹Xu¡çl£*Y]A6µ®�Žâƒê*µ—7ƒcOå-áu½mÀ› ÜÚ±ÕšH#/3àF~3�_À:XúÃŒæM•/ïØ &�b"ÏʸÊ(7‰2+AKë$Qª�Dȯ;¤`Ydá‹O ÚËÁ8 ™Z ˘�Ï_^£ƒÂï.Jö=ÕŽ`ÿüÞÍ9)É‘µ¦²8DêÅUæï;áÎi Ä2ùt[7Fþ ̈_·[:íú3H�6™¥LC½ÓˆøÜ°ÙÀîna7+>·ƒ Kà·~ToòrØA6êîÕm ölÒ2¬t¦:á…òè&f@BØ$‘ãV³i¬ötó°‰/\øÆ­ã ­YôÐv‡?ò1Y.Ò¡Ùu±ðF˜ÀèW´zhóyØ£! ÎãF=AZå$ö·ëË‹3È«"ñ?k ,=�Lãf-AZb½}ôz±´[W­é…«Ž ´šË&HS¥‚¼êJ¹î�V�²|¥Ñ~‘L½g/Ÿ±ë™!eµ‘¥¡ð,ñë²e©ÐèRÓCù6ìJ`ŒKU?ÊÊÎphÄ JŠb¡N¯v}Í=W*ôÕÕpé!¿Xã|èôk?zF™‘X^¬Jç²Â÷ý[M8�Ÿ¨L"�áz‚ƒ8¯†6G3û0Áò$ !‰¨K¼šo–t Qà€œu«^Ô)êãP/ëèvG_ûúàã½}îÑ':±zoêÁî`ÏÑt¼�ùŒÏ§R4%Oަ“onWD˜0]dÕyUt�ë�ÓÑØÞ<¼eßhÉmzwÿŒ?‹<<<ßçð°ÏÈ[‘¯%Ÿ/49:|ñ’œÓ ¦dLU$ÏÏÈ{±L±˜YÌ$Ñ FNrÁ»2"cRq‘‘£à�øàÙ%oøgÀ°YÒ5É„&…b€‚+’ð”ö5b¹&<#‘Xæ)§YÄÈŠë…9Æ" Åg‹BÌ4h ð9|%m8Bµ!x¡u®&ãñjµ ¨¡4r>NK(5~þöìâæì9Pkà?f)SŠHö�‚Kàs¶&4b":Sº"B:— Ö´@bW’kžÍGD‰D¯¨d€%æJK>+´#©Š4`¸ ²1{'7äüÆ#oNnÎoF€ãÓùô¯—§äÓÉõõÉÅôüì†\^“·—§çÓóË øzGN.>“_Î/NG„�œàö5—H?ÉQ†,F�Ý0æ�ˆ’ •³ˆ'<¾²yAçŒÌÅ#“°Cr&—\¡&�–”/¹¦ÚÌt˜ pž�š¤b>‡ýƒDŠ%ÑëqÙ¥“l="§<Ò#Ø¥àß˱ÑtÂç´ÈS6¨¡ ˜Òj÷;ÔB¤3–êz�G‹ —bŽœ—çâTuj.y¦–žÏX½@£䙪êgˆvØ‚ ÍSUÁ/é k*ÀxP8ƒÁû³ŸOÞ~¯>_�“câU—ëA¾Ö ‘‹ó*ÀéÙÍ´­gw¨þ làæ¬ B4 t vùþ4ü.(iÎîEÚ8k ðÓÙ›_%Ìu fp\)=˜3ýÞÌùa˜Ñ% Ãá`0ˆR &yÍr¡¸r=1KHòŒë0ôÍ ÅÒdTÉzOXÈtBÀišEˆ™Ôvt ëw @Ç® zí/D·+ã gTNÈ ì xzGSgvHž¿&"c‡Îh@—È� bÍÀzÍÇÖàÏj nÊÅ€üÔB)"± &o‘ë+ÈΈžHw-\o ] £A% ˆ•âimÀ1½<½4ÁÁØ}Ðhì­È>ïå¬$ò˜4*Äá·Ïö¼Qs¾ùžô“Ç@è.i�„Js x–?ñõojLm¬ÓÁ+¶ÌõúõÁÖÛ‡§±’W›ƒ�c– "—¤ƒç`ûð´¤öIÈŒpT ‰u¶™C4.fä±qéÐã2Ž!¢‚ŠÇ/~:r… †‘2 " ¿(‘…1ÕtbBe©âæÄл;÷fë"è3eë›Ôî¥B1÷ï³ðJÉ%¤eý/ Ó@´dÀYÜør$2H:„yŠ @ Sˆó¡HB�æª)l‡Ýàªüø`w·<“ÅmËà� &--L±†ˆSdµ šÜßc·�´Öü�òb• NrYèïïͶæqÉ€zLdm‘Å20Ž^"@·�סç<0Èq�4-âD×l©üáÄq00%ÇYçù–wMÜû¢½�e‘ˆMeÚ¹5ŠTsÈú¬òÛg´m[廕r¡Ò~W÷ŽFÎ,b<¶4¸+4MÅ ’} —ËH«ãV‘Y�£°Mo<¨ºÀÿõóé:‡¼P1U…™¡¤€hØS'aõ7'G?d*×xKUhp™¦P„+QȈ}¢å-®·eå�æJ(¢¦G uöo&�ßãZcÞayÛø­­û£ÁúÿoÛH°Ño`à¦-"Ú¯­�r6Üj“|Ò1Ëݳ:Ñ¿;ƒÃ³Fç�z—ý>Õ@ÊÎ�Û>A9ŒE¤™~!–Ñ¥7ìâr“ÑNjûõ>íà1¢szÁ•ýÑåÝ»­�r~“iàòn£©zhML^ÿ!8L¶¿ë‘Ž{Ð*ߊ´X½ï{ §b•¡±ì‡÷þó¯÷Ä)_²k¶¤ÝÝâêU!öÜÀZ�§²`ýJf)Í‹ÃÕ‚eàÎW ï�ÿÕäJš©„É›œ±o°n¯5Ç{¯>îã Õr·)�V£?�VäUK^?ÄÒk5ö¦ÙŠÿ=i¶‡×Þth³¡ÛØú~Z‘%ýÚp0�:ëÉ—Ímø93+–3&1OÕ`ïa“5Øgïƒzµs¬Ãn[‡U–·ôïhœ'eÁ€ ‚B…hÉx«IÆ \þÒõÁ¶ w0¾<<$¯Ž»ˆ_‘?v1õ±÷ûcò¢hÛ@«²ê7ÈäàšEŒ?²˜x›] ¶RÎ�“(‘m½ƒþÜù÷Ì*ؾ(0*–f úÈHBA/qàõ“àAÕ¦¡�Ûôp¶Åg��£»mž§\;VÜ;‰¦]Ù.WÖد±óÙ;la„o2nÓµ±uœm(õ<ïB”\š&~ålÕÂ_@|Ÿ1–‘ŠíÕúê"F°uаĪ|2iuŽð=J¹éôó,f_aN˜7½ Y‰ âûì7ø&Ùé²æØ‘ë*êŠFóÆ¥±?µâiZâ^qŰ[‘ÝÔÝ ;ýH'¦Ÿ>¹ÿgùFQ•^-áÞïçÈÚAöNHs1°.ZÉm—�ýPE pi/;ñK„²£ÚhËêÅÕÚ¨Ô#›b$ã'ƒm´î?—Ó3_ñù’>rY¨£?'pŸ×˜P¹<åRɬ‰@íoŸHŒ)dº…Ê8(…+Ô’§Tb8f2¡Qùˆ– 2{sª¸³%»jD¬ë²É CˆÔ ëPßo½�Hýè2"ΛÎn£Èzeé,�çÑ„™W—Öõ§žkytÙh Ófߥr¿¦[;÷õ¬ñȯÏ:ÈëíàqnoGù”’x›–@¶¨•ñ¦Æ·ãAnt³i;âÞI„È�Â�;‡Ù³Ýá¤Ìœ»… R�ÕG]Ø¿nø´Iì&º£¾„Ô’[½‰ò‡·^µæÝ9ÛöEèÑÆehŸ>nkÙÝ™ÓËõF-ÖLTkÑ¡2kË#r{×R¦y!².Vfϳ[§Ó½ |ëUwDïÅÖw{Ü› 0štTËÊV›aÆô±Ý†„²�ü}y©ìýÝ0óÜrçôÿúü·qÑ>svQ›æTº™û:Û‹Ñi¡Üµ+ÊìVªœê¶÷?ÞtbǶ™³±»^˜˜'¼Ž!Ô0ÛF5e«6¬kˆ§óVWøi}#7~<3—»ñþ$†B Rýº´WÅÊ‚+¶÷j'fãph°A䋇º2Õ‚Ð9žyù¶ÄµI_©ˆ —¬ƒ–')5ø/PK¢|$[¼ö¾³+f twine/sdist.pyíVM�Ó0½÷WXæ@"ºæ^ Ò²! .­›L[CbG¶ÛÝîjÿ;3vœ6i�e÷Àrˆ’z>Þ¼73©ªc=3n¤â“—v¡*H¯×ª ¯ kjVíáÊWjÎÚc·n Î�¢�¿TÒY©œ·j¾öÊè#ÇpU@Cgè<**éûtŠ.Ù¾Ÿ8Ý{É'#†W 6�j¸œN³¢rcFµ¬aÂÐ6g'Ï‘x´§K-š2å˜6>æÙÒeÁ¯­ffþ /öÂç£ÎîìýÇw//>±gì¦çË’&–×|Â.¤}}Â�&H$žUÍàôva,²âA;,vLn,,•/@—îRùUÖïJÁ³ä™wVV*{Òˆ7z#+UîË�-øgM£ŠP2GzöZú »I1oyË\”Kiå‘PÕâ˜`çFCO-’É8ÑH¿p…Üí@¹»A>7جÅ*äÀL‘œH¨rzŒu¼h¬iÀúmWU³�nÀÏ¡®PæÜÁk¹æÎ¬m¼ëó]—dáŽuA-Èrn¾õàvIÝ4¡ÑÄÄæ1ãv‚}˜3é¢.}ª±‹´‚Õk¼Ñ@Kl1ÉœÒË ˜7ÍI¨pz-ÆÛ­¢Ï·Aižu⦮�¦Ç,Kð„;5vjpÅt7\pÄú”nüöH�ÞEÒ/ºüÂVK½Ý+¥†zŽ:QæX»´ÅJm¨ zôÝòƒ˜ù€?$£å ùbÙldñ].“xYJ/Y s6ûðöõÉ›ó³÷³Yè£>‡AÇ´;³·°}e­±G¦5BGº;nã/Q),ÑÑdHdΞ Ÿ)'ϵ#½…r„i¸KN” ÁHëVbS/ו´iø~Çú!ó!pÙñ€ Îʇ"bI‡æ8h8­äÕ ƒ G[¶Þ6ÛôA>ûR„!ùgŸ“âôÃOÈŒ;€ F¶fûÜ_;^uja†Åô Ù£¿VŽÆš=B~ü ònK¥oÕ}vTû·A`Œ3©—í¯®'PQUÿ×ÓÃ×S;1‘ÙÐ÷ØKÿÀHýPK¢|$[·q•»d Q3twine/settings.pyÅZ[oÛ8~ϯ 4Œ\Øêì<-²ðb=ŽÓ16µÛ™ ( …–iGYÒŠT\ï¯ßsHQ"u±Ó4Å䡵Dêðð\¾s!Çù˜lòˆ‘ ‰ ã0Þ‘(Ù…Ù&y¤ñ&ÂWœ ÿsÏqœ‹‹ŸÈ8I�Y¸{ä·_ÿöw2¥1Y šFL$1 fʃäâ'˜y,ælCòxÃ2"¥4€ÿŠ‘>ù“e<„ï~ó~%.NpŠ!§÷ pLr²§G'‚䜉�“m<³¯K cà~ŸF!�F¡x”ËD< ñ© ‘¬q�„Âüž¶æR³øû›éx2[NÀ­œGŒs’±ÿæaû\ M�™€®�ň’î2c"AfYˆÂìžlÅ�f ¨lB.²p� KRš5ذ9dwFK2]:ä÷Ñrºì�ûéê�ùÝŠÜ�‹Ñl5�,É|AÆóÙÕt5�Ïà隌fŸÈ¿§³«>a 'X†}M3ä˜ Q†lƒ[2f1€Æ€Ï™§¸�ú$ \\óaÌô4š‹Ç–×Ê€�¿–ÁŒ¥ E’[sFüââ"ˆ(oY¸Çå�?ð‘ùú?,°]*Àðb—{‡½mÃ]žI¡H©®�ªw!?\¡e'êÓ4œÄÑ‘¬a͘ ‹�¢ò¥}?±ã¼ e—ïY,xAá:A�RP$ë«7ž‹nØ`%ÁÓå%I�â1‰ÕþiÖÝU–ƒ/‚ƒe1ݳá/[úÄôÓ/=õÁ}E$£!z ¹”{¿|XS6ɲ${ðÈ8etÓ—N{Hòøο‘îâá~ A«¯7lK|ÐKø¾[Òã,Úö˧wÕO*ã…a^’u’DdH®i`T} <œóQ—œÆ�]ºsªñp* Åñ²´ÌÏ0ï Lœ%±AGoçܼäŒê>7/Nb‚` DøÌºØ°D›9GMÙª�H«÷)­Þ»š\�înV> Ëõôƒ=½™²y SŸ}´Uvr@–�gÐ(þK¦V¾ê+�zI,œÖiy�# À¶Nx§‘âý4Kv¡þšf]sß½“I <øOø¸äp� þ)¾,gƒ]OÁ–C…ÿc$ɳ2þPhÄÀ¿K@Oº·�ºÄ¿ûG&¡Ú¶ŒŸüQz( LžF Ýht1Iyõu¤cXôǨAì=¿~Ž1¦…ªr)‹ô ¡Bu¬f_Y� Ys®*~i-ˆTôK—l�ÿpû¡U€mˆ¥k ÜTc�Ò�kè‘’ Æ#\2HÇg¤]'q‹u,B!DЯ½Æª%84VÕ#?bÕ:ÔXß\%2Y3Æ!Ž�›ì!YاÅñ¾ä0Ų̈$Ê¢À ä< â#2Ɉw –4¬5äP ¨ì+ˆòM‘$2HJä6µš8Ø"cÌ6“¶àŽv#9o’´AÒ"º”Ö±r%™u&‰©QçÚ_1Âe8ÊÄ"¤<p!Ê "‰\Ž{*ÉH³pO³ÔÂóNn�·S�ÌqQ‹Ž6yi°”NÅÑ—Õ·-î\üIq­!%Œ¤gã\Ì*Á,yi¬€¾(œ¹ºYÚ‘Ä1äH(å‚–Åd“#�œÖ œhòS²Ù-˜îÞô¡jý¦ÇrÞç\JŠ4Ù¨@C‰2EÁÅ!b (å—„äN&Ð&Q¿�|°S¨�Ébc»õ`xQ[ü¿ðyå/Ê…¨Ê=YÔJ»Ì ¦I¬`•ÒNŽÑî]µ�Üàõnqó6¬0wÅb%´WÅLT·ð¤*óü‡‡š\ëÔÀvÝÇä G j‹4�[“<ÕŒÂ"Ô,³*R˜ö– €×3 Rã©>MáàP#¢=¬9ê=ØÃmœÃܶ×ö‡ØÁÖ³=Õ— æ HT}æZ2ª)hX{îwM Ú�ÕԞ͊™Óæc;Ï…Eú˜+ÀÆl†eEƒÿô¯eÖ3,Ùtb2Ô?Úøý‰h, Í Çf�BÈ ‘Ìð!e'í›1 ¹ Ìû&ŒÖ… ˆB‚ÿ¼ãIä� ‘‹±¢–-ôjÂA†Zc¶ $éq™ Lcp.Wçý2Õé™ò‘?ÿ† Ø-Že¡¨¿rq]™}[e@å�yW;ôô—]”5g)ËôC œFQrðA@†|ÜÞeÍ‚kŒè…Œê·•NɇS5U¼Ñ˜� cõê£lOdŸ±ùâXEÈïG\‹“iE”0¬õ1?‰ÍxIdVÁ8<{@AŽ€¸¶7$nÕpñlªýæ0 ¦*3# ª(âöê*1v£é»ïª•:U_[%¬ÖÚîkT¤Š'cF‘ Õ öí‚ñl]Ge_ R¼5媩‡~:”D Êk ½ß Ô2Yô ²CÝ7óvLÜÈw®#g8½Ö/ps7 ’uW8�]ÏQ/zE©=x?Z̦³Z؃=d­É¦”OÆv€ó,óu“Ï/;V®|†ŠYy£bèV´ËoQ”²*iɪ¢¤cJL-âÑͦ\Ùg�9ýÚ›AFµ1*óš¡êƒLâç0Kb¤yŶ4�„=™ÅÏCgu?�MüÅäv¾œ®æ‹O5‚õá°Þ©À¿G¥@àl棊+‰G{W˲ª=•Ÿ‘ÏG—õïv‹‘ÿâwLc(-x"ë&ÈsHÉÏ.l¹ÇqãZ0u*ÏPu`¾àõœ¶PöuÊ@sù�qv(ÉnáŸ.X‡µ~ϋՇém§ k²+úÃE ˉ)Іº_¬ 7Ј™µëÃáÀ$óE–³Ùv ðN‰Fëh¡¿pA) T7ÜjU½vu¦aK˜�½ùV–ØFR{¥ó+CìB½š}ÉìsŠ.p±ÛÓCØû’‰»ê^U½.Å7ÐÀrÿ×rZG8à]'¶�ü•½9‹A)ÅWó‘7ùйÝáÉÝr²˜�>Nœ×àEÙ.<ß³«{} ^(÷È”CNØv‹çJ€úØéÁó¾xŽêúüuØ‘65¡“Û7ÒÄíh¹¼Ÿ/®^¥‰²AùÝšP]‰VÉ6íi( FmðBé^Gt×)ÚÙ|æOg«Éb4^Mÿ¬Ûº’â«{Á5©”çë]íà¿P¶AÓˆ‹KgF×Õ“Æ�7[Ò¯VºÊìH¤#6¼àHÏæÜÃL5 ¬¤±hs¿>€=…é@·„^UÏFdÅõøt×¼Ñ!wçxîþ UÚ¦n?²ê­ÈÀ¾�¶¹¼@ÁpƒEÛH·ÊkDж¹¼Èò�òaäx²X½IÎ å‚r†6²¥ŒÛ¢ÝN#�ŽP:é£78wjBlóàÞw8Šl:´Éôå[Z.oZNúµæ¿qÑ Œ¦žâ×ÎTyfG@Åv;ùˆÈ¹§âõÛ- ú—»Ú)¥¿Ð ÛÚã¯Þ@Ñvè¶ó`Mëuü�ÝMg›ÞÚ’j�È‹2µÎ5”]7ˆxŸ¨‹ecQ‘CÁTOߎ±[tX qU(>ôuŽQt|«+Bv'©˜:Dïá.®ÝkŒ~vj�uoI”—Ó$u�NÕ@æ<²àɸ5 µóá�(W,½ŸÃM±œ¬�ÑÛuqlܳ’®ÎUËÜ3<þ..Y¶Ü½í&J¹‘Bbjˈ3�d„‘ )e!PMï0Ðö3aÛ€ñ¸—ráJ—l×§ óΗޅ �-G¸(äØ^Çã‚f‚#ó¶+º¿÷£Åä�ùÝrb¢ƒ·š,W~9T™lý¬µ]‚÷~ÏÃhÃ2·ç!;Ú:[à·^zØ8«¾¶·éÚ�=oÆò<Ç5¼BZ`ýóq`¥1¨¦`Äv48ªã%v(ë’UrÅÀ“6øå,€ß'­Ñèc`G:T'*Öú?ÜÒ^aN7“£ñ'ÿöÓíÔ2¨â½´+ü“:)COgm¼iKµ°Q»+[�/èÉ\õºá+S‚ÚT˜1¸²#cËò§eXcù1 1;˜:–�]Åzu�‚[IÛ{þFU×.>à'åñ}sH7ƒZ†Ú.œ´U¸:žhšAÚÇ^’éðTDÕ¶oª X|êÖãg#uÅ�/þPK¢|$[a¥¢•´µ2twine/utils.pyíZíoã¸Ñÿî¿‚ÕOä«­$»{EëÂ÷4›—½àv“Eâ½Ã! dZ¢m!²èŠT¼~‚<{g†¤DÉÎî] ôS$±¥áp8¯¿!ùŠ�Êõ¶ÌKÍ^¿ag²àyÊnu5ŸëÞ«Þ+ö!KD¡Dʪ"%ÓKÁNÖ<�öÍ€ý,J•É‚½ŽŽXˆ�}ôÿ ¶²b+¾e…Ô¬RXdŠÍ³\0ñ%kͲ‚%rµÎ3^$‚m2½¤i,“XüjYÈ™æ@Í�~ ßæ>ãFà¥Ök5:<Ül6'I#Y.sC¥?\žž_ÝžAZ¢ÿ\äB)VŠTY ëœm_ƒ0 Ÿ�ˆ9ß0Y2¾(¼Ó…Ý”™ÎŠÅ€)9×^ à’fJ—Ù¬Ò-M9Ñ`Á>èŠ,8¹e—·{wr{y;¿\N~¼þ¾¸üpŽÃþÿ�ˆË$À5L®Ï®Gì²`°~Q¢�3Í”+…ë™A¦PªZa8/¹&—~ä9h” !*á ª 8e…Ò˜””É5Æàk0¾Å�fW²Ï±I u†Z/øJ`ø°5̲‘%…ã„F�m4mdiDIrÁAH–YþQ rÓ;H�aty?`3´Äó›KS(+0@¿Ç¸™ݬl 2䔥֥œA,l�/ш”b�×Bè☘TõnÄZªLËr{J1 Úm¤q�ƒßîï{=ŒX�}ìB7ŽèYÇ(N÷{½^*æ4—É!ÆòWÖgÃ<îÝ©ïG=?Aܞ‚Ýk›Nª’”I¶‚’@%Ì"±ÚúQ�x\ÈrÅõ¨® P8I¼Þê¥,ÈË\n4&:4 ƒKSjR“L¬fÊT …Þ ¾@† U%–¨6m-¬,‰!pßfä'èêäénµô¿ªõ!¶ ûÖA3€_b#©.žüÍú[QùÃUЈ¡á/%À(«ÃÍ„Y?R€™´ ‡W€´Ë²”V©h¬xQŸ&õöùæƒr¸J‰&=eBy ¿£Yƒû» É_Á=k|¯©=ë�A-"‚Í–ïtÜkÚÝ:åÕ­Ð%ñNPÚhJš=f),xwUøÞËÄ/ ˆ„‚(îZ1åylû¹'~çMã˜íç �Qê�Ç€‚ Ý}ußmJäKK®bIJ (|¿=ÄÓ{Cx„÷mÏì²qª‡¸%i¿”`Ê­.ªœ—Ì=XÿË`ª*×eFit&–ü1Åæ€ ´¶TPÅ¡`2â¹¢«x�Êÿ‰@f¿MÕ…ÿuu½µT ø´­˜õCts¬j+ùˆ"ðbKQ1ª�d¸YVX\ƒ VŸóÄ"“•È µ€…<¨ˆA®@™iœ 8Lep�@ùÒGè‹!TïdIM ×À¥°R‚%'$ÞX †¢•0“ÀÄôé”a‡­µ‡Ù–ÊQ¦Ôù)VئjcŠL!^¬ Œ$k×îªÌÀ,Î�\Ӏ軒ՈU²‘ÁZ›��R*’a:ŒQ‰¡0¾ ØwìÏý¨*lúêûžY@T—A£$âÆ%ÉÚ_¿Àt_;ÆÏv0Yd‘�7"kŠlj @% Úë•0vÇRà HáN¯ØI—2'¢0Ap Ê'W¦)Þ,çk$tå·Zc¿¢ØäÃ-Àô€¸Ö†¡Tš­*EUûb È@ÈÜ€8Y•H„HM q֪ߪèg÷1lND �íjBêW°èÐJ�ÎV�Ø‘ˆXΡàÒ0¢A-yˆÕB8qDØxS¶œƒ?ÓŽû- åF4ý]tsqŠ�ÎÝrî/‰ç€ˆw¼Ž0ú\vJ–ØJ7å4y¸'Ñ" [ r\$±_"€zÄž€gÄË…º;ºŽ‚z”ßx‹A„èÚ¯˜Þ$gXw#ö>4®ÝjZÀæÔµõÈÛ»ˆ¥öü÷Bïöèà€ÒaŽÝ³­—v_Åö„;=ïÿD¦b§.Ù´-הᾋÿtм,—&:k©ÕöãºßÍ,±Ó#ê÷Cwݲ†r^3ç¦ïWE߯o ÈÛÛ¬÷œúÀa4mçþIl;ÍÁocÖq×�¸ßùüà©ýù�~â5*ÞŸ£¿A—‡„”Fyü_i/™ Þ[ÍÛNtu=9±_p^ )ŽüÀé¼Â¿+®È)}�fdX1ð˜��[$ mJä‚Ãpù l9—¦&ÂW(Š�Îñ£Sñ<‡2‚{0m ¹ú?¨ïNï ç· rO�8iªó¾‘íJl( ÇÅ?^ßN®N>žßÖM5žÀªÖˆ~Ï«ÎNš?ÂøìªÿW¢½Uõ÷çArœäÝE0u&xò?BÊA«s5[d~ŠOž]~3SDnÀn{S³ªvC½C\Ïâ»g;Äs“U[žF&Þ âöA2ê“<Ö êÿˆ�¡É_ä¸ýÅÄ�u ¡s™`h6^¼ƒY›�ï0´hhÐg´<îŽG÷]ˆêÈ …0òc‹ñ9ÆÞÆÅ4¦_ÀVEÎÝ6 ÈúÓ»Ô�ïp%?ŒÙñÑë·ð°.Îõ$ÞLÞš¶Ïi PÔHAg€Î?½ zNM §ˆÚë�½9šÏϾ_«§yðTEÇógöTÓ?Vg€*“‡Xi®+ãÑÔrP\�Þê…ûhÀwÌ$¾›I™ï¢þ÷´i®±\Š| 5…ÔUxVTuÐ CS%Û�ÝöX7T8K‘MS‚Ì.B"µYë´…×Xj6 -oA�²Õ†� ;ÄôaK»"fºø>€ÿ9‰R ±8ÙN”á9 UÓvÇFPËLyzdã1{{|Dep']£“ÕƒZèlESÞžÈ3í$‘~Ú~º|Ck<€xtz€ÛP4žÖ!YG‚.»5rO éGxŠzðˆjID†‡œGÊoV²Ãë³?9pSm0jÖ ŠØ Qˆ�}‰}^—“+Qõ¶w]´`t‰û 8ËÓîÎØóÞE>u6¸žû(¡Ñj®d ¨…Ò‚!•Ø+œ¿½_oíMè ¬�›ˆìàzgÜW1È¢ÊR¡WÙñM±j9Ä5ñe´ýò¯ùáÑ÷žþN´� ½Î˜Vøm·ðoÍn•ê¼mˆ{ÞKþ(è$»Ã*³] n'ÑÙ}½e¿á¼�mìÙ=ÞØHÒ÷h×¾ûŒ²c÷ˆ�b46ïr›´4Äí sе[�N­VÒd ®`ÓkØn±]r­Ó õ8¥‡Õ³9Ý{p9·%œ=)Ùp:oÛÃ2­h/Ì‚¿Î‰ !„O}qb8l'DråúoЇ¹ŠÚ<¬<Ï)K¯·�m?žgæk§¿xpg÷€Ã¼}~»®±ZkP/ÖœÅÖãçÎðïîîÍAh§{oMÜjÝ¡â”"ÅÆˆç,äÞÎʲ>›í7ūՇØúõY ³E9—¸íƒV(«\¨‘y}á¹àtZ+c:Ťh‘ìu®Mµyíè ¦Ã}ãoŒxcGt”´wT¦•C ö,9¼õªª{=�¢§ÓÖjcÑÚ&îdœöÞðj‰¯.Ú a¸µøxþœÐÆ4ª¤;/ãú}¥å…žÒ’Æ3+ä¢\Ê ̃N©Ó逑½§Ó¦w˜ÒÖ <©„é´5O×á ¼ÝR­`Nº$DwVPX˜B£¹;`g%õ€Ô°x¸µ¼ñ^‹cêÎëÝvw'º³PËŸ¶ý£~ß�wÐLmMtL@ Äó2�=£}‰Ÿé4u¶­ kR‡¿§éú Ǻ×-£|:ûÀóŽß7))Í;Qß3e0Þ¤/ZÍ&TÄÓM¢3ÚwG…^òs¹ºmŽóàW¬2FzX¦fÛfŒÛл¤!@Z€Ã‡I?¢;ŠzÇà4èSj¦ÞŠfí8™{\þ…6Ë–ÀFð:ÔåÛ·‡¯ lÏãS;Yü3ÅS‡ßGÜ GK:f ÷þþåøOn¾ã©]óA�~ÉŠTn;µñ‰÷[ÔbO_}(e „Y‚÷™‚–¶íø¯VEðûêüYFÈΜȘ-«¨Î·ÿì½…ðrmõ5K÷¡èFX*eòðM‚ ­ Fµ¯úÖÕ=ï™`y¹Âz)Ú9³ ì{ ¨Á¯Fì½Í3§'€¹ŠºÛÇŒ³¦¢Ñt·>Cf£íYŽÇŸhwÍ/ªáÂÝ1tJ9®�Sû­yÍi*Ã7f;í7 @Ãþ!¼Ã[·)°â¸¡ao¼AQÆKŠ!�€‚Ýš§5øá�8Ÿ×$Ì’ dîz)ö?æ6Ü�¢Ä:¤;söø±Þ+À‹݉\â( {�Ý=¾ˆ°Xãoö ,^ h R5¾ ÞŸO‚ûfÿb¤ß<ÒRó|||Ô<±½¸WFéñÝ÷GGöýÑ1þy�ÞXžV,tJ{0±l¶dÀÓë³*üìe­dUèÐ ø÷ Ãÿ[m�iµîn†Š–2Õw^Ìž îhc«Ys1B"yWe9 Ò<ˆdâ/ŽÍEb¼5æÆC¹Hò*1^nXˆÍ~²G1Cv¡˜fcH�û$98;/³R8Ü^[ Ý­Õè„*z¿ Ýe¬L¢ ÏË !)y  }Ö8+2Çͪ•Èç�õ€ƒx�€æ²±ÙƒÁ»eåíÉÛ>ô¥³+GöÝwxÉh­Þy5ëooçxÌÌ6—] UjøX‡=Xw|V1ÃG¯%Bs–lj>+-&ÞHŸ$âkJËf†þÛRK¥—îâ ¥~©ùŸäÿéû�¶?4>ÀºÌu<¼ˆ¸L ½ÇŸö€¿Œéx!ži(ê5ý"ò+Ú^ÎYWµªÀIcÑ9CLA'f—G×=Áö:Øat§t�gWÇž÷¥†ÿ7<í…Bâ,¾´?KµjRGàÜºÆØás˾Çp²åU’Ýá Þô¤Õå6h Aåw­Y5Ê?Ž^¸OXk€ápN¢ß”�ù ÷TÉ›{/5im÷u±efþûÂQý-åæŒ.öatjG {òm–ð(Çn| ²À²¬|±7£8S†w»ì¿WžÆ¥]>bSx¬F{í>8ðÛàÕ.¯úào{;44¹a¯„Ô,40=ð{u1¶Sï¬�ÐûPK¢|$[–!ÄÁ É twine/commands/__init__.py�VmoÛ6þ®_qPPD9í¶ó�^’¢Æ:gˆ�E`Ø´DÙDiR©ØÆÖÿ¾;R’å8Áæ²HÞ=w÷Ü †áï:«$‡T+Ë„jvÍAê•H!×¥[-v+_,ÀTË‹To6Le& ‚éÚkre è……�‚¥_ÙŠ+9(m�AQ-% ÿ%ð17�2Ž¥é�áéÁŸ¼4B+x—\BDa}Æ?#Â^W°a{pe¸g"È,ߥ¼° 2µ)¤`*å°E×�™$Aˆ/5„^R&�¶Ýö¬¶rÀ¼Ã Ûí6aÎÓD—«¾ôR¦ÿit};žÜ^ ·NþAIn ”ü¯J”çr¬@gR¶D%Û¦…­JŽgV“³ÛRX¬L�Îís‰(™0¶ËÊ1Õ¸†w�+¦ N`4 á×ád4é!ÆçÑôãÝÃ>ïï‡ãéèvw÷p}7¾MGwc\}€áø ü6ßô€#Oh†ïŠ’üG'qÈ3"lÂù‘M¹š‚§"Ç‚“L­**Å•~â¥+î‚—¬̤¡ZB)6Â2ëvN‚J´§K ¹Ú0›®›åJêeó®MR0»òRoÀî 2S݈ÔöËàsÌ6<›V…äA-JåÚHú*!'‚`>gRÎç§øˆŒÎà gAd<‡ùªÔU1ß®9—sª0ƒÏÒØÈ½w”b¸øå°€?‘û–Tû(Wè�ãÌ¿aÎɸ�„c“S¡Fa²]Ë0Ž=ýÎàžÛªTÀY)÷¤ByâçáÁ¹æÁZ…Ò‹ûM·ë^ƒÁG_ùþJ²Í2c°ÀÅ[ÂÛ�˜.1É—±W?ôÌäBesªA¹ç��¹Ð,3ž\ç‘�€ ¤�½ªs�ð�³Òw˜écgÄAêHˆæ¦PïpJã´µŸ‰L�Sa %lBÖM‡%Î_~�žÏÚ•ÛLèñ‚Á3¹Ì¯©8]%sœ¼u¾M§B|19\2ax§P“‘zbRd7MÓãft¤A¿ðš)ˆ“Û ÄH»†¦IŽ\ZlÌxçoÌyoÚð�pºqÜQ¡m�dïþVHI¬4šK‘Õ¼µzMFøÎ61q·Ž^íªZ«.H%Ã4RE…©?´s�|¼kî9�)w"%UŠÈ4) Îá´w!‘ôóút›F¬#%<¡ÑA'=h�‰,S�ªùrï*½+Ø‚µ£Â଴soÛ§Æ¿w ÷×>ª6÷?!m7 +Õ‰GRAâµä ê$¥~™¿Xœ‚�¯Ç+}+ât�7L½å¾<û<Ôªá"‘ðÄs÷:´ôÜýßå¡~èjw��á]V8¨œç]¸SÉ:7© p±X2㛢¾_,N=xÁ\ àÀ ã‚´ å±£ê6g‹ErÄzǽ+ø»8�'‘ÀfnR 7²ý½”`äØI‘OoÂïfÒ0þvR8úºjDÃøP©4#Ûfʺ�EžÐ9㑺£Ä�Pûãid¸�Ñ%8<ðC-Šcøè¨ëeìmÏ‚W ŸÈù<÷àÙĦå¼M✰I‡éò’�GzÌŽâw¢Î,#›GìvîˆÖ‹]LÆsr=#ï®§“Åäz o—ä|ú7ùs2½?Á1챨?(™£Y‚›3ÖQ3ßeÅâ<…”(h™Õ4c$ãLè|©˜Øä#)A½¤ù&WT镣œDž"Td’¹w¶¡yn@)�ïsîž %38ν‚÷RÁ7Dí*T®^ä± à$ uU°^CO“ ‹C}˜…TF€Èãµc¯D^ªž¬ÓÞn@žmÀ6yd Rå:„J÷a’öz¨0˜>rš‡SWzÍ�¢’‚2Q¿×ëE³ñôb<Ïæ@ú¹GàgÊK6<¦q@È ¹`)­ pMJ&c‘Wèé+˜•*‡èÄA!Zš§Ø£TT€7ÔËZÌLËEßÅ´ÄKÁý-†ÇS8Ð;®F‹lCŧ„o@ô×ÇŒeuA÷ù‡õ_KS"1­T-&œ ©·¡Š$<®U^Hƒé&¡’A’o@‚, £d]CˆðD“sR�œ�,5à·à\²bGV\¾…1’˜ñÈxîË?ƒò¨W!| õÖ)h`«‚¯€g¿ø…­^¥4aÿ /V/é«„½<ûõEr–$/Ï^¿JŸ¿¤¿°„¦Ž-8KÂjwruöú5Fþæz¶€yÁðÌ 2Ç×ÞÞ¿ÃÊ/³7CÿíÍo¤Üÿíð.ù©ÿ¶?$ž¥ºóõ.{`Å›‹ñïþø2™^^,˜N¦|Ïf׳/óñ_�aý�¯¹ïú{näµ÷&|Ö÷LdA—‹ëÅùÕù‚Ïï?\-&W“é8èaÂÆ܈>šxÌ$ÈÆÏy8×ÚN®ûC-$a©F<æƒßÓ€`¶ ˜Ô'§o0ˆ†  η x¡qI(ñÚG¶þžrÞ7ÜøƒÞv»f‚וïáª×"z„÷¼~yðUäÿe~—Ùzê�Ón¯À˜ÁÊ�¸+ŸmMµZä3þý:„ÿ{�ðÍJú ܽ=·`P%‘5@­ß�W�cö®Ž"89Š´³µ‡áux 6†hQƒq¡QƒªEh8)›°iL½…·@C­y‚?÷÷æ,˜&ÐMØØ!‚#LA­Ç[Xœå¡9f ¸¢Í16\"°º"„RÁ2Qk^ê&ü"<;ƒN¼Tì(b­6ÅÉSY¡SAÿßÈ ¼Ýi*áßÞ›[ »õ¬É§h²wlÚì^Ës@†Žë:§cG�èF:§êÁI·SØø„è¯�ƒ«‘E7 "”ÓðIyõZÎÇæ†~¿ËxÑ8þD²ü�•ÍÔø:�ìtDnïõJ.#þ ^¬Õ+®›�:Í,¼1/—øŒÀ9+|÷è ^a°‰ Mªh#.tKQùÚADm$ZÍ +ÌÒ¡×}¯µçPw[ßwX;„žÕñÚhSGÎ…0!V8¿SàÞ²àÂoI[=&Á €‹-\7/N–M¿…ùz/²A�ïØÚîÕÆž6A@L."²)îoÉíÛÌ7ßÀ²�Q´O»G4›·è¬ûƯØúÛQ…a²õê°²OF`LJéŸÓë�SïÛ¾>toãQ›¬€S�Þmô³óL1G§iÆýÐ dë�nP�££u�gÏlµFm¯Jëàƒ„jWá%-¤-C‹:Î�¡±�¢ÅhŒu.‡¤Á½Š~�¡Ù">8¹�Á\z‚v|jC\¦ŠÂj %½ÌN°q³»™è Èa¤ÜéÉÞ'-’»‰+®�¯4|ð 5‡ªd®”�\˜ÕŠl¸°7M`çu·*ƒÂ4Ið‚¢Ÿ‡ÚãÖô½# GÇ&©ÇJ”¶Yy­ªZ9¬íŠ`x‘¥&]Ó–uX«†¯#ʺz/c’‚y§hÊW ì �Áð)œ£†&Ê ³ãÄÔÁKŒÂ‚5Id¹Âqs›Ãýl¹ÔÁ…{y§÷iß@äݵ%¼.“H/ûú¯ISh(œ& 1‚ß=7WQ^‚½[Õ–qh¦}�«0®» «zÉ�†Ìí(Ô&ùÞô€°5<Ùbh•Þ'ð¦Ð©¼«ºn„×m§ã^޾Ýù©I{ôçgG“—7ã2y�s�”<¢awôí V°/év÷o:åQÁýfÞ;!7¨ª¹yÃEºÖëاÝÕF †>¬ )�ƒtkoo/x·‚%÷—ç“«ñÅퟟÊíøtvt¢Áâ5ä·Ü•Š>ÚDÅ àõ°®¼CD¶ºÆÑ†•Ësó™a&<äJ½»òóQï}í7¶è–àówÓƒòýÿ÷!Ij†ÉëN:êS¹zx–¶cEÁ·÷7çó9ÈÓŸ³ivÓköT˜„ßœJ+çv`Þ¼#éÕSªì7šïeÖ—»åå7|jÓÆnú–¾ßéjÖ¹¶›ÁH^úTdí6vتÆ� nÏæãÔri?F:¼ê6-«ê–ìT_¼`»Æ9õ›(Œì1Wζ 9Õêfg¯pVrŸµÂs{Ð�Þñ+Á³‘×ú˜jciCèp‘S­©7OC¯½–ãO‰Ö�¼ŸZK8î>P1Ò´­õ5+ª‘÷úb@jYÓú3 žYþQíôÔTOëHªGú‘'4òHAé´6íÐ;²ãˆ[vPŸ®õ/!W\¶íõkÔÊI}ÅÐêšA×tZYÒòÌÔ‘Ô†“´.c;ëØÇûÌ æ+_“9úûB;…ÍÖ:>Ô� , ŒÚ;f©ßûPK¢|$[ð› ÂY twine/commands/register.py}VßoÛF ~×_A¨µGé ì%Cx‰ƒË#vWA`_$J>LÖiw§:FÑÿ½äI§v6?$’H‘?~ä) ÿTRå±*¬�…,2°;„\e2†TiØníA3i,êí6 Ã0ÞÁ�*�Zf; ?üò+ÌEA�t"L¬‚wd¿—1¨Šµ ;-ELÿËþFm¤*àcôFì6¦püE8ª öâ…²P¤Ò@* .¾ÆXZ�ß—¹EŒp�vçÒ4A" ñµ ¡^¸<ä_Ò]Ú÷aà�µ¥¹º¼<‘pH#¥³Ë¼ö2—÷ó›Ùb5» ´Îÿs‘£1ÄÌ¿•ÔTçËDI`bñBsqâOdÉfƒ=hi‰â •ÚƒÐHQ¢U˗ʘòШà¾qE4‡ÓÌW!ü1]ÍWŠñe¾þôðy _¦��ÓÅz>[ÁÃ#Ü<,nçëùÂîî`ºø η@â‰Òàk©?�”Ì!&LØ q€5À÷¦ÄX¦$Š\Y%2„L}CíS¢ÞKÃ�4/¡(¹ÜK+¬{rVTP>¥-�•Bô÷ÊD¥°» ÕjöXrìÆtO$L ƵYËxç�¥–…^«µ1Ô2ao©ÃÿpÂøË kë Oƒ–Ûf‚ H0mGaä/6Þáªu�VÍÅÄ'›Õc¸øªÀ«èG“´Ôxáã�<=¨BìA ÊW*#­ÒGxAêBUæJ$L�èäAeF� ÛÅÔ®¬"ž!S•\O­£åq9g!ºÎl·Ý|óHí©�ü–*ò£ Y`LZA¦n.Ã0Bš¢ÆÂö¡Ú�°~6 HÛ`»¢Ž‹=œSç¬ü[ïÜ:JeVñT©º�ôF.¬ÛOªŸ¦_gÔÏàiÄeyùéïÇA«Fí¾{´"VxàZHÚµ2¢NYÑšÌüý0ß  © y%�TÈéD[dmE­•Ž©˜=4ÖDŸÖëåŒí§Ñ{4˜RÑ %�hñxaóÿÎSé®Ý<�ßä¼QϹúv�Â籋è&o”†�Íën4üQ�߇„ã„áÛ":,‡!vd-xãºïÕ}2äÎ�« 1êEoòÑwªÈs‡¬e?­ŠØ‘ß~1:8õÁß X (ç'þ¤_PäWQðPK¢|$[)ɾU• &twine/commands/upload.pyÅZQoã6~ϯ ´•{Ž’¦ûpu‘Ò$Û·M‚Äé¢H›–(‡�,éH*Ž»È¿™!)Q²Óm�.Xlb‰Î g¾ùfä(Š~®²¦,­JÃe)Ë%3�‚ÕR¦,¯›ÏÍZ–‚5uQñl>O¢(ÚÛ{ÇN«z£äòѰ£Ão¾egUÉ‹ŒÝš&ÏÍÞ;XðQ¦¢Ô"cM™ EROjžÂ/wgÌ~J˪dGÉ!‹qAänE£ïA¦jØŠoXYÖh"¤f¹mÅK*jÃd z¯êBò2l-Í#mã„$ âW'¢Z uŒÃú>åá:Æ­Â�ÆÔzrp°^¯Nš&•Zv•>øxqz~y{¾ÚÒú»²Z3%þÓHv.6Œ× LÊ bÁ× ÜÇ—JÀ=S¡²k% xxÌt•›5W¤dR%�éyÊ«‡ ÀW¼dÑÉ-»¸�Ø'··c�ñébúÓÕÝ”}:¹¹9¹œ^œß²«vzuyv1½¸º„OØÉå¯ìß—gc&ÀO°�x©êJJô¡ÈÐa·BôÀÀϺ©Ì!& ^.¾lY= ES µ’ORƒzH)äJnèÊ–QÉìW)øZÖ\iá?CÐ-AÞ^®ª3›e»[g25c áÿ”k³çŸAï m´}HÉôÑ?R+YÂ:+ŒBØÝ€�Y�šzÇ-W¨öŽ›Oh8×þÏ㎕Z<ç]B# ½·‡¦‚W޽ÍÉR˜�t-žÍJ¾³Ùhoo/9ÓO²žÙì‹÷üÀ©Õ ¡˜´Æ'7îÒØ®/à(�:a‹ª*Æ^ÝIOïäÚ~ø€6ŒØþ¿hñ„¶€?�5Ï!orË2‡–‡B)ˆø—‡ÄÏE±arYV £‰DMàˆùªSš.âÏ"Â_eä(nŒXÕ†0¨ò[ÍçNéù¯rx¦®´4•Ú$á}ÃÛ].r�0U <>v ŠBl6«„{¢4 ó¹WÇn•…€ÇZ™m JðlÃhW�ÉiȦV?Ü]Wc¼\îò!:¬•ŠAžë™å�®ç6¯€y䆭!�fwžWÂ4ªìõŽØu¢;�pÌ*„е¿Íçx�®I|„Ðo…ðü†÷­Œž´J9×·GŸØ+³´Ê„ o®Á•Ç R"BŪ îÈÞ‹à¯h”ÕÒeDñbv?‚wÐïØåÕô<Ör¹âÏR5úèýh®7× qbü`õ‡Haï½î¨i[lœ,› +ÀS<é#�,A�¿vEÈ‹p+V°ƒCHç%îd½?ü“êýáwáŽÉ^àиõð;VPj¡ŒYìK^£âU«êw‘šƒnѨ}ÔÅ1n$°) ‹ûóÇÓkv¢ŒÌyjØ�XbEÚ´O€®q(ë�‹—›8êeH„~y¡Šò‚Þ[ÿŒéðFV1�z³3èÖ\-„Æ h®Æ6š Pac8¯ •ˆ{¼ƒ·ì}DíÜ<¡òqßÝ´‘?ó`=ia;¹uŒ-.¾…š-Vž‚%F�Ù¨„‹{dÌ)P•çˆ0Þ‚q øØ>æ·¶Áy’e•æE±Á¦›šjtÏ$¨ È^ râ&‡5€ŽÍ D<'üžAeà*@§~:û‡�ß4:A¨žyKâΤ�;,³ã.ÝÐ`‘Qië„'î*:É ò¨>Áœô‡že³e½œµkânõ} çaJµñ& Øk¨:®Úޯƻ–Î ¶ 30_šM 8'ã«JÀ˜öü@àdÀ¥…ªíp� f¾¿ž¼Cáº÷B>Ìj ŠÄP‰¯CQùÝð¢c Q>†4T…%4o�ý!¯ekA•…)NX% ¯°x ñ\­‰Ö™¤èJÈnÅ ô8 Ž›%å3/dvæ™1\Œ{OPHßÙÊFîëYÔºÒ �¶Í£Ï>_Ù#Ô׈‡ÖU*¹¤`ÿáÑÎØ ×Çá¸ï˜Ä‘ ÎÚ«±—åu²»X ™È2¯bPx¸è•ÅŸ[¯£häÆ/ìeLçð¾Ø[ÊëÃσ¬mwŠœ®8¹eÈsý `K�NÓ¾—UÙ­;R¸„Ug¥k�èh Ñ‚+,© h UµÄ¶ÇíEÞ�õùÜvÔ¦» !Ö²(ÚnñÖr�–ß°Ÿ€v@á£óçó- ™r3Vî) ¦½ÂÛƒ‘h -l¶rÍ{ßþ‰3üú=•ÕºìÛòIPùg…\^°»›�k—nÒtÉ(I­<ï:½[¿~ 0 �¥E‘ËûèªöiZPºµ½€k ÞllüôĆî <ÓAká fHا8b@. Œ[ö´h²Ž¢ùå:…CB¿ã‡.—á"‰CÝÅ&Ôûò¿-ÍÒFð1ùÜ ¥�†ÕH·ÌOÛÎ1 €oŠÎý義Î(Gë³FX_yÇ’–66“Þ&mùÓtz}Ž÷‡=šw�‹éÌïÉób‹<ŠôiÖI™5ªp¬}¸²Dæ›Y.“f)¯ùBPBÝú¾@KÄÄù†¢‚¥Ö÷Qw)zøBMF¶ݺ�¸ Æo1t]8« Nâ$}‚|ဲ„Ò‚ •ÓYø ÅÊçFðs‰!0›¢J¡ä¢8û°²ÀŒ¦†„§lËJûf¬Üà¦ÀÝ„.¿2P�ž!È S‹bߦtV!ŒþA ³°øö=‹=™2¹««±-MgçNî>Ng7ç×W·Ó«›_Ç®hMÏoÃËŽÏoUt(Ò/ØÑº}ÛÓî z%÷ûAÅŽÀHy‚ø8 DÇ[r ÈX=[ö‰Ë ¾¤ÍҔ؇îÆݨ¨[áâ†�ãÏ�’(†e6£Ë1ý?r'y6"XÊôH3$í"Ö¾­©øÆùa¹éŸ‰£}D°l¶Ø�6=•¥™É²n:--ëÇQTû»f·ªêg{Æš—Иü!(‰ûA3jy€/ 3S¹Á¨qßúpGkbáŽi‡a÷~éÃVî‡ÕÊOúÐãÎtÿaÏg ¶¬uŸÑ“5Ò–QAhÃÃogÐßNœQŸþ´ ®´Áció"ƒÚô§eœ]ÿxÝ9ï` ä�M>Ãzõ*æTê®ttxôíÈÍ�zlÝJ :é2)KÚ(e)>�~mÉû½Ñ”α±e°˜Ÿ4VˆtKh\Xêøäûªü¨P:_\ÑŸ!åùŽÓ÷ö" •µhëzÛÒ¸QNÿñ„À ñòVS Špj;*­7:­{§M3>7BF@*5ô•Ê—ŠNžYâ<}½£ùùŒ³¼¡8|vozðÍ ÙB§Ü÷!#'»äà[ ?ÛZBN5 /àT‹�¼©5°˜ƒo¿£œ%ÝwH¾F­K†§4 ØŒ © Íy.Óã1еμëp¥‹Ý¶Þn£I04ýÛíntÊKzIfAÀÕKwm\®´(ž1¸V˜“MØë_Z)Çó†…€$&[Å� ÒµP;�²3�š?%=fð�ná6½õi[Íp2ô y—rœáKƒoÚW¶áë ‡ÚáÏ;è"�˜�íõ[ŒÈ)ûº"Ó_;b–K…¯™üfí<gÖ1xY9^ŠR(­‰£8C:Ý’kÆ—‰!¸û‘« ºÙ–ŸQL5óuò[i?¯^Ý}ë�×hº„îàôx§I´[»¤?4KöݽŸY |k@Me&ôfxE?VMÕt� êAÝVÎÖ*—7å5�#—U£­1'‘™q~èúu¿�kCÈÃÂa§D‰Ãˆxl.04°%r½ª[”rE|ÅqzX5¡nÝv€ß¨L²åA¿ê ƒ·· Ym %Àƒ¹Zêí*òeÂ8þkÏ€†��àPAî#l–»OÇ+ûïLQÀvØËžö éÿ.7¬%¶÷ Âù mà„@“#¶JJBä–+W$yJ÷S1¼oIk§�á²`äM Aô[y�__(~‘b 4|õ)56á‘»dtrð¬}¿�™wôO^l±*KRì` kRñÉ‘JÈ‚Ä*ÿ�pq‰““ªt’êª*üK±Öê´7Æþ%6Â1FçŸÏ_DÚÛ#úùÜ|îû¦þ ‹„ W´l¿@V·lÏõð­ŠÒô¶ßã!9q ¯éNŒSÉã(ü¢�óôÖ”Î_¿ 5ó²fí¦±ÝgìiçÏnA—£uQ—B%vý#¸`ÇŸ¹:¦µÁõGQÔÇÑß®<#Š;¾ty”°;Ýà».yð5K¨áÁ[0ÿ¥P g8޳4ÓNëh,åËb× µß £·ÄYض°à=òœ¦×UäËÖÝš‚jç VoŸ™GI²Û´8-çwS¼¦¤XïÔnOÙVMÓ…ÃÐ ð»GîãШĵÿÿPK¢|$[ä’$ŸÇ ß%&twine-6.2.0.dist-info/licenses/LICENSEÕZ[sÛ6~÷¯ÀxfgìFI»íî¶}rc§U7µ3¶³™>B$(aC*@ZÖþúýÎ9 JŠ7¯ëÉ´EœûùÎV꥟«­.7F½·¥é‚9ûÒkÿ2>Xשoo õ›îí÷êÛ7o¾;}bÓ÷Ûðãë×»Ýn¡™ÃÂùõëF¸„×gg�7÷¿?¨«Ûkõöîözù¸¼»}PïîîÕLJ›BÝß|¸¿»þø–üÖõòáñ~ùóGzrvöÍB]›Úv¶‡PaqFRœGÎUØè¦Q­Ñ�ê¡[o|”î*Uº®’#ªv^ ÁÊ›­wÕPÒã‚èЋ• ½·«�*TEÌL¥V{õ`J¡ð ˆ{7¬7êåj|°xÏ•Ckº~&‘óG"•n»÷v½é•ÛuÆ+ƒS¶ß+=ôçí˜9õz¿Ñ½»µ×8Õ­ù¥¨~bmÖºQ7LôˆýБ^,´Qºd‰?´Ç»DÃáÛ(—5A˜Â‚½wM¡´7éCò¤=º ÇJ×¶®#2ñ-µ³ýFˆ«…zçz4O¢DEÉ„ÛZƒ�k8àqÆ®mÇî=TFœz–Î…:4Y´ÅkôÓŽ€ïM«­d�ÙjÏA¶`é[ãM³G˜wŸÙX+DÅC§[s™ük�*¾Ö%ã{‘JÚhÅ#qÈ"ÆÕÑÁo …c%>éÜÃ1q-3)Õ½Q¢4óGi;&ãÄ|_~Iæ"‹ùžÛ�oà†° ‚Aê 8„X`,F:sa>ªüìP®N/¢|ÞE¬2oŠè•�kXà �Å×Õdu>jsN„¤*�¸Š¦Afy4-Èì+Ýp¼ì<ê¸9ºhnEq>ZÙLÆ!ÛôaÊ6x(^,!D9uü›¤¶Ù†N6hð@*«3cŸö¡7m1õq0„þ%—´øµxšj•tc ”[¹HÈ0sxf^²zÍr\Ž™]ËÈ›ºO _±¤˜ç¤ø\ÅtÐ lm9¸! +[í?Žù©uáNÈ»î¹oä¶äÉp#ð9¿…�µÊópq~�›î¨mÊ®—û‘Üb„tí;µ�+ƒ¸Ag�!nÎ$&X0ˆ“†––ÒJ�g–Z–oêêvˆáÛQåÔð¨‡AÊa È“óCJ¡Y êšÊŒ¢ *wV\ÀÑ­A9t][ÓÃc±¦ÚYj:×½b'hI_¡%ñkšRÜ^7ýþUí >Y4[O®$0žWÞ8`«4×àuäÏ–"õ¶"o‡ÂlÅm£Çãˆ*Å1ð“Xþó il±G<åvõˆ×‰ÒËpwü5sÇMÀùÿë‹ œ1Ûž2�~Ÿúˆd¹T[Q1óºePÚè'ÃÍ‹Âé«kê½á¦„Ê�Î÷â†1µcÃ;5† VˆÔ�$~z»mhžsüËf% ŠB•�¶0°¼›t‚Ù˜BnÎþ:ädÚ[N»ÚMÒa,¬<—/Â%ÆK×™XÆdhƦšÏ`=dxŒÅRKã5+Òß‘íS�Z¨eM®–Ù#s(dGôv-ÌõZÓ× Wq¾˜ �4¸Þ…ðŠ-DÒ—n ÎF>ÃÉZ5zÛ“†�Y „ÃJIìX¼Àí%¨bD‘Ca#‘rrÅ>i“¬ßrßÒ$ÍÃ�û™4êÅ,Hþ”?±N¥–G°�r�|Å1¡Cê£*‹J­ý&ÓôÁFˆñÓ#Þ¬]ƒD qB¡Ïdˆ¹íÄÒšÄ$©¤Ë‡û,Ô"øÉ[P™ÀèçH9ÍÐ~ØÂÿÄ%�¹­2n²ï˜šYZh–]ˆ§8Aßn; ™ÐBbLH5†+¤IxÍÚ!’ñ,3žÞôH›"u®ÙDÌm9d9Ô)±YM^/(o¦bVÄÈ-Ú*CíL‘Š=ý`Ž“(ê#ƒü If˜¨f­”À_"ÀbUŽ»J”ÒŽì'yäûXjTìž•ˬT]ðŒNŽ“¹ôüöîqùöæùôܳu)™"uêx‡‘G¦dï$:iªÓp—®x‚›ÂÊœ´#Á‹¦�g¢�‰Ó\ägÉ‹¯1d¢qÚž' É�Ñ�—q1ߟý Øý˜ÔIºÉ¸“U¦Ð /rÿ)ÇáY$�©:_×([OˆAåm=¬câÎfÕ©óÊB±#?a™:Ï.ð±Ä5 æ«W¤Û~ôDGK,L¡Tø�ƈ÷¸‘a‡�èÀ®™k¹¸Ë|:®ÁжOs!µ™ 1i{ö³…ô÷˜©éwOÃEv‰D’8Zåk½sX~T…ÇÚT•骡M½ã,2PÈ€•œ7C'¶hÚ@õ“‰ÂëL'Rªý0 21ÆÉýI›L�<÷޼¡–}°#J†' Qö\RÚ[Yjg}æ‰Ö9.¿NÜ„�ìÄÕ'ä(bJÔ<‡í¿Ðúç+¬1M˜ñMû®‰õÑ Ì¬\Ž/­T¹“¥x™ãXpÐOæÿžÇЏø–pjÈÂB}ìPñ»È<ƒEii¦drÙ5€ìö‡Ý\¶÷É–>_\ôÄþšxî?¤ëZåدbßÃf�!祬ÒåcƧãÕ×…•“Á‡’qÍóU* ó`*#WåÉ‘…TÙÂlãð±ÆÐÄq½� ÀS�y6ehFÎÑÞ¬µ—ë’Ã^Ÿ¶ßœ¥î ´e=låýziw³»²t¼’Þ‚÷õº¥åÒØkÐvÈø'ZdÇ��&F©¼œÂ2ÉZÄMÿ¼ùs°ñR„*o€¨ö²÷Pž]Kw¨$ÌŠÖ „^ÑöÒäÓÚòhY™2%y)ù ‡uþ¾P×6ð„B—Œµú„>¶Ø�1> ¹ÚË`ȃ,M21³Ùa<)LÛ¢brOLç0 yARÒô=ýòWi©7óã%­€ØçWjùp®~¾zX>°5?-½ûø¨>]Ýß_Ý>.oÔÝ}~c|÷N]Ýþ¡þ¹¼½F/båÆò™†!*`$ªlm8eï u�=&G6�þ&a½Çåãû›6¾}µ¼}w¿¼ýåæ÷›ÛÇBý~sÿöWÈwõóòýòñ•wËÇÛ›¹Ó¾"®îá›�ï¯îÕ‡�÷în¤2Ê}WC‹tˆ½;ËKv¾|�™+ 8É»­·Ô³’5Bˆ¾ç ›P3ÛÊ.4,¤%ã­ ŒËÁ•v<’ãí ¯&óëÁã!ö�>$Ò‰÷V¯l÷»Kª’ ½I׳B�ÞÿA:Ì®iI‘ng(}>xwfÝX4E¥¹,ÆÙb¶Ô”mÉÿ ç )ç´Èn슛,kMS½ì賞îÄ_ßžÁøÓ:ƒÔXfGkö¢nõz¾¸¦£éªzº´[C—¿éžÉ‚S6çÔ]Èv“®—"Å„²´˜‚Ä´¯õr¯KWê*]q�l¾aÄŒAž`´×eð8ŽÞ/ÞÛ&yHÛÆIT®�«v¶÷jŸQ@Ýv«iƒF•{ ykm›ÁKÑM=tSóÁ•ëð�hïMá™Û@Xš€¡`£þøp[EÆ%²®ž,_ðÕñ�àQñt×i#ÀX¨«’�4OèI<¯¦¢šÅü§ uÎó<œÝx½xy”zÂrãœ,yÿ7Ýó½TmZ,›îJ#²oe3qlÏÁeÚŽþ´!.ŽÄŽMY¹Uw6ÜW¼&¡Tn ¥Cfl˜î2ÐÖÿêv4yȬ6‰ ˜Q�Ô⿥蚴ó»Þ¸üçuf|Lx8¡!KÊ�ÈtW!yÚ®dQPl-Ki,YÌö¨Å•©1!ÈëhO«[cí[F–Ô᎖‹I:x?ÝýÄí)p£.�‚²T,Ž×§«}l ¢{Òz2âØKï²�Ëú¸QŠÅÙPK¢|$[C'bîtwine-6.2.0.dist-info/METADATA¥WKsÚH¾ëWL¼‡½X’YW¬ ÞÍ©¸Ê1THv÷#©�Y�f”™‘AUùñÛ= ¦ÂÁFš~|ý˜¯›�àxÎÿc…V »ˆ^÷¼€„¹…P<\EÑY0ªŠ‚›:a]-%d�˜ž²Ê )œË¦Ú°²J¥°s¡f¬äÙŸá{”ÖÃÛà]åæÚ„Pp!ÖÓŠËœ�\5�:ÆU΄ÊÅ£È+.Y¦•3"­œ6–½Í½èŸÖ‹FBßw"e!ì/K¶AùÎ!$¨C£ÿC„á—Ow û  (È)›;WÚ$Ž}x‘ž»9ä:³h3ÞVéÊd*3áæUeºˆËºä�‰g:=�U(Ç)5¼�Š%w`Ý3CŸ4JŸ£à—kCåê,*kÌ¤Š´™Å+1»>ËÆ¢�ƒ®ä˜�©“°[å@å�³wU.@eÀÄ � u‰•Þ’½ç®2X‡;®fæŽDûjF¥Ý’ &†‹pGµuP�ÜGž Fë/ÿTF·/”¢/�z/¾ªZ†,2£­Æ>ć°õb;X£™áEAj›9úB!Jß.�•§?%ëcõ¢ë£5ÎÏŽW9?^åâx•c³v[”ž®#½é¶Õú_+�´6Ï »éPªz`3#J’»š.‹ ?×%Ñ!,]¼ �uOÄó^H<¸»íöïGýµÅž°.at×  ]7Íÿ†¤´#ôµÂûoo:ÑÅwOC§µLAºW�³è::;½Áÿov­UFJ‘^ÞtΣ‹«ÝcQ”Ú8”‹–ö)æ«ßYÃ$ãdžêÙ[vB=pò\ÿjƒ©F¬ç4 PÙ ù¾HºÈoìU‡�”evõZ‰gó½öòúl¹cÝL³Ëë7WþõžD‰lŽGèv÷ì‰óÙU‘»âLÁ7ý¥3ü)’ƒ‰³bn�N‚^­x!²„ɶ ¦ØAEì›çø°Íâ7L7¶#vÜŠ¹E1‹p(‚Ì=ùãñc;ìã,`Œ%Ž›¸µ y†oÙ¼oÜ5U[ù³/sXÖ+ùŸðLó+Ly>ƒƒN7&^kaŸ³}Ó±q•VBæ/ôÕŒç˜û­ÄÆ m¦R/b‹·¿²›#—Õ…ü#5\eó=ï¶â·‚àyÉwk²•«­h‚ ÉG‡>A𙘰Œ·ËTíW©Éz—šŒW͹TMh«šŒ£ ¸u¬lûœyGÌ6³ G”DCʱª”šç–v6ë·MS¡p© &9^¿q pãÄC¥Ýkã½ß –j7g Þ,Q†îÔdµxxP#†~Ø$ßÜ‹Zœåkº%HS •ì)bƪI饲_Y"wzmª&ý§Þ¯êÁ2§ÏsAïqaA >6‹º«%’°…ŸMl«ý‡íC9× ²�}Á“q�“ç€d4‡�úIcÉ,PduZQ(¬µMF‰ Uƒ) ±åÌ,�¬�ÖjMý€ÈP»á„Šáí`M£´IþÕâ–œCÊ­OšµHYÙ6à)ËæÜ1£uÑæŠvn²„ …UÅ^ƒe‰FpÄø¦¸ÐS¤s&ÃÑ{ö ¤¯&ÞÈñfG&?³–zc¾{wY§9Üj»IB7ô‡þfR[‹�ÿ‚Y+<©¯ÔÃf§†6„U‹à™ÁvIoÁmµÊ�¹¬ ö{}¶Ù�_ ÙF?GsWÈ6�{*ö}‹ZªL¥N=ÆÝA¯?¼w÷½/ÝÏQ‘ÿPK¢|$[tæÊì\[twine-6.2.0.dist-info/WHEELÁ1 Ã0 Ð]§ÐØ2]_ t+!´³ Ÿ4`¤ ËCnß÷>? ÉÞwÓÂSÊô€Âk˜îˆq„Yë|¹ç4§|¥Å,äÙå5mÿ µn…�ó&j ©zýPK¢|$[†¢éœa¹&twine-6.2.0.dist-info/entry_points.txte‹[ € Eÿ]…+pA+‘dJò…m¿r°¿.œ{ŽÆkò‹Ë­n¢Ý.’\e_Œ‹Ë7Bh¦…vW²€)íâAxþáàªcÎG6KãaïÊ>;[ÌÙyPK¢|$[ôú»–#twine-6.2.0.dist-info/top_level.txt+)ÏÌKåPK¢|$[;§;Ùtwine-6.2.0.dist-info/RECORDuÔɲ¢Xà}= Üb�iÑ &‘QeC€…OßV„VÓÕ}_à‹Ì?øÌkð3ò:‡AðÕÎHŸ…Eÿ5JЗÔ>˜eN'Ä;Wfì¥õ1–¼¹±¦)Ð,ПêÌÙ<œÆ°ð-Ua^ÿKêhñ¼‡£¤Çâ(¨,uÎ,›¾´y»8ùþ>ÅNX÷‘ÌRjƒàý¥p€ÙJi̽iÈ”¥ÍcIV3,xÁ'ýÒès¶Ï"@™7PS†à8FRoæVæëZØlQ±bââhe¦Ç(¼¨N󸞋sJ‡s–î}ÃB!‹l0#qÞÃ.�˜7õJKQœÚ™Ó1NytÓ÷”íÖ8|d¸†6¯®wA…a °ªá_�‘o L7Ðþ’úµ­Åùè>Ή yÓkë8}2'éaX¦go·žnhõÃSÆ®{„¢ðOamx+¬ ùªÜ/~ϸ܄ËÏgnj‰Áw‚’39XR‰(HÃ�m–WN$A±iþ‚s â�³a$Ùjïì^8…hà(•‡R °‹ŠÒµöàù;2; [ù ¿mÓç°éæUAw1ƨÝY|Üq°+Eõˆì–ºµÉìDç”G•BœHO/#6o«ÿ•ùŠiáælÞÓp‹–Jp5ÃKn¨Þq{ž<;û"y8å~‘ Bpø§­@˜×é:ê:£w‰M§(vQ…(ëØNâ7%Ζۇ°0ׄPw+KN´©ñÐæ[r‰2èïȾŽvÖeò12ÂÑõ ¯É/ñÿO² ÁùUtTG_ð®Z°N¨–rÓÈ ©}¾ôsÌ�§:EH'¾a½½,?f°ˆ1©ã%ÍñÝ4;Û-rcrN]/ÅÃÞžtC¢Œaª�áðoDPÃnÚ&¯aÿ'øûçùÒ¢Ê5F9>“—Ž”q¬üÛ°-Äå"ï‚;åE^”\cûµõìw1À¦ J0¾î`E;Ö�'†æÌâΪiD¥õLè§ãMIj²)øv 7O„þ¶eѰ%ùñ7PK¢|$[š%wR'@¤�twine/__init__.pyPK¢|$[o2äì+¤�Vtwine/__main__.pyPK¢|$[:S~`Ê + ¤�™twine/auth.pyPK¢|$[˜…pÇiç ¤�Žtwine/cli.pyPK¢|$[à/e§r™¤�!twine/distribution.pyPK¢|$[ÏäúÑš‡¤�Ætwine/exceptions.pyPK¢|$[(!—ÚÊ3¤�‘"twine/package.pyPK¢|$[¤�™2twine/py.typedPK¢|$[³|Oé( @#¤�Ç2twine/repository.pyPK¢|$[¼ö¾³+f ¤� >twine/sdist.pyPK¢|$[·q•»d Q3¤�wAtwine/settings.pyPK¢|$[a¥¢•´µ2¤� Otwine/utils.pyPK¢|$[ßnî�ê ¤�ê`twine/wheel.pyPK¢|$[–!ÄÁ É ¤�¥etwine/commands/__init__.pyPK¢|$[ðb²f m¤�ýjtwine/commands/check.pyPK¢|$[ð› ÂY ¤�˜ttwine/commands/register.pyPK¢|$[)ɾU• &¤�’ytwine/commands/upload.pyPK¢|$[ä’$ŸÇ ß%&¤�]‡twine-6.2.0.dist-info/licenses/LICENSEPK¢|$[C'bî¤�h•twine-6.2.0.dist-info/METADATAPK¢|$[tæÊì\[¤�Áštwine-6.2.0.dist-info/WHEELPK¢|$[†¢éœa¹&¤�V›twine-6.2.0.dist-info/entry_points.txtPK¢|$[ôú»–#¤�û›twine-6.2.0.dist-info/top_level.txtPK¢|$[;§;Ù´�Dœtwine-6.2.0.dist-info/RECORDPK¹ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/helpers.py0000644000175100017510000000373215231700043015470 0ustar00runnerrunner# 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 io import os import pathlib import tarfile import textwrap import zipfile TESTS_DIR = pathlib.Path(__file__).parent FIXTURES_DIR = os.path.join(TESTS_DIR, "fixtures") SDIST_FIXTURE = os.path.join(FIXTURES_DIR, "twine-1.5.0.tar.gz") WHEEL_FIXTURE = os.path.join(FIXTURES_DIR, "twine-4.0.2-py3-none-any.whl") NEW_SDIST_FIXTURE = os.path.join(FIXTURES_DIR, "twine-1.6.5.tar.gz") NEW_WHEEL_FIXTURE = os.path.join(FIXTURES_DIR, "twine-6.2.0-py3-none-any.whl") def build_archive(path, name, archive_format, files): filepath = path / f"{name}.{archive_format}" if archive_format == "tar.gz": with tarfile.open(filepath, "x:gz") as archive: for mname, content in files.items(): if isinstance(content, tarfile.TarInfo): content.name = mname archive.addfile(content) else: data = textwrap.dedent(content).encode("utf8") member = tarfile.TarInfo(mname) member.size = len(data) archive.addfile(member, io.BytesIO(data)) return filepath if archive_format == "zip": with zipfile.ZipFile(filepath, mode="w") as archive: for mname, content in files.items(): archive.writestr(mname, textwrap.dedent(content)) return filepath raise ValueError(format) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/test_auth.py0000644000175100017510000003305715231700043016031 0ustar00runnerrunnerimport base64 import getpass import logging import platform import re import time import typing as t import pytest import requests.auth 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_username_keyring_not_installed_defers_to_prompt( monkeypatch, entered_username, config ): monkeypatch.setattr(auth, "keyring", None) 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_get_password_keyring_not_installed_defers_to_prompt( monkeypatch, entered_password, config ): monkeypatch.setattr(auth, "keyring", None) 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) @pytest.mark.skipif( platform.machine() in {"ppc64le", "s390x"}, reason="keyring module is optional on ppc64le and s390x", ) def test_keyring_module(): assert auth.keyring is not None def test_resolver_authenticator_config_authentication(config): config.update(username="username", password="password") res = auth.Resolver(config, auth.CredentialInput()) assert isinstance(res.authenticator, requests.auth.HTTPBasicAuth) def test_resolver_authenticator_credential_input_authentication(config): res = auth.Resolver(config, auth.CredentialInput("username", "password")) assert isinstance(res.authenticator, requests.auth.HTTPBasicAuth) def test_resolver_authenticator_trusted_publishing_authentication(config): res = auth.Resolver( config, auth.CredentialInput(username="__token__", password="skip-stdin") ) res._tp_token = auth.TrustedPublishingToken( success=True, token="fake-tp-token", ) assert isinstance(res.authenticator, auth.TrustedPublishingAuthenticator) class MockResponse: def __init__(self, status_code: int, json: t.Any) -> None: self.status_code = status_code self._json = json def json(self, *args, **kwargs) -> t.Any: return self._json def raise_for_status(self) -> None: if 400 <= self.status_code: raise requests.exceptions.HTTPError() def ok(self) -> bool: return self.status_code == 200 class MockSession: def __init__( self, get_response_list: t.List[MockResponse], post_response_list: t.List[MockResponse], ) -> None: self.post_counter = self.get_counter = 0 self.get_response_list = get_response_list self.post_response_list = post_response_list def get(self, url: str, **kwargs) -> MockResponse: response = self.get_response_list[self.get_counter] self.get_counter += 1 return response def post(self, url: str, **kwargs) -> MockResponse: response = self.post_response_list[self.post_counter] self.post_counter += 1 return response def test_trusted_publish_authenticator_refreshes_token(monkeypatch, config): def make_session(): return MockSession( get_response_list=[ MockResponse(status_code=200, json={"audience": "fake-aud"}) ], post_response_list=[ MockResponse( status_code=200, json={ "success": True, "token": "new-token", "expires": int(time.time()) + 900, }, ), ], ) def detect_credential(*args, **kwargs) -> str: return "fake-oidc-token" config.update({"repository": utils.TEST_REPOSITORY}) res = auth.Resolver(config, auth.CredentialInput(username="__token__")) res._tp_token = auth.TrustedPublishingToken( success=True, token="expiring-tp-token", ) res._expires = int(time.time()) + 4 * 60 monkeypatch.setattr(auth, "detect_credential", detect_credential) monkeypatch.setattr(auth.utils, "make_requests_session", make_session) authenticator = auth.TrustedPublishingAuthenticator(resolver=res) prepped_req = requests.models.PreparedRequest() prepped_req.prepare_headers({}) request = authenticator(prepped_req) assert ( request.headers["Authorization"] == f"Basic {base64.b64encode(b'__token__:new-token').decode()}" ) def test_trusted_publish_authenticator_reuses_token(monkeypatch, config): def make_session(): return MockSession( get_response_list=[ MockResponse(status_code=200, json={"audience": "fake-aud"}) ], post_response_list=[ MockResponse( status_code=200, json={ "success": True, "token": "new-token", "expires": int(time.time()) + 900, }, ), ], ) def detect_credential(*args, **kwargs) -> str: return "fake-oidc-token" config.update({"repository": utils.TEST_REPOSITORY}) res = auth.Resolver(config, auth.CredentialInput(username="__token__")) res._tp_token = auth.TrustedPublishingToken( success=True, token="valid-tp-token", ) res._expires = int(time.time()) + 900 monkeypatch.setattr(auth, "detect_credential", detect_credential) monkeypatch.setattr(auth.utils, "make_requests_session", make_session) authenticator = auth.TrustedPublishingAuthenticator(resolver=res) prepped_req = requests.models.PreparedRequest() prepped_req.prepare_headers({}) request = authenticator(prepped_req) assert ( request.headers["Authorization"] == f"Basic {base64.b64encode(b'__token__:valid-tp-token').decode()}" ) def test_inability_to_make_token_raises_error(): class MockResolver: def make_trusted_publishing_token(self) -> None: return None authenticator = auth.TrustedPublishingAuthenticator( resolver=MockResolver(), ) with pytest.raises(exceptions.TrustedPublishingFailure): authenticator(None) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/test_check.py0000644000175100017510000001405315231700043016140 0ustar00runnerrunner# 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 pretend import pytest from tests import helpers 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_with_metadata(path, metadata): name = "test" version = "1.2.3" sdist = helpers.build_archive( path, f"{name}-{version}", "tar.gz", { f"{name}-{version}/README": "README", f"{name}-{version}/PKG-INFO": metadata, }, ) return str(sdist) @pytest.mark.parametrize("strict", [False, True]) def test_warns_missing_description(strict, tmp_path, capsys, caplog): sdist = build_sdist_with_metadata( tmp_path, """\ Metadata-Version: 2.1 Name: test Version: 1.2.3 """, ) 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_fails_rst_syntax_error(tmp_path, capsys, caplog): sdist = build_sdist_with_metadata( tmp_path, """\ Metadata-Version: 2.1 Name: test-package Version: 1.2.3 Description-Content-Type: text/x-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: Warning: Transition at the end of the document.", ), ] def test_fails_rst_no_content(tmp_path, capsys, caplog): sdist = build_sdist_with_metadata( tmp_path, """\ Metadata-Version: 2.1 Name: test-package Version: 1.2.3 Description-Content-Type: text/x-rst test-package ============ """, ) assert check.check([sdist]) assert capsys.readouterr().out == f"Checking {sdist}: FAILED\n" assert len(caplog.record_tuples) == 1 # Note: we intentionally don't check the full error message below, # since readme_renderer/docutils doesn't guarantee it. error = caplog.record_tuples[0] assert error[0] == "twine.commands.check" assert error[1] == logging.ERROR assert "syntax errors" in error[2] def test_passes_rst_description(tmp_path, capsys, caplog): sdist = build_sdist_with_metadata( tmp_path, """\ Metadata-Version: 2.1 Name: test-package Version: 1.2.3 Description-Content-Type: text/x-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_with_metadata( tmp_path, f"""\ Metadata-Version: 2.1 Name: test-package Version: 1.2.3 Description-Content-Type: {content_type} # 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)] def test_check_expands_glob(monkeypatch): """Regression test for #1187.""" warning_stream = pretend.stub() warning_stream_cls = pretend.call_recorder(lambda: warning_stream) monkeypatch.setattr(check, "_WarningStream", warning_stream_cls) check_file = pretend.call_recorder(lambda fn, stream: ([], True)) monkeypatch.setattr(check, "_check_file", check_file) assert not check.main([f"{helpers.FIXTURES_DIR}/*"]) # check_file is called more than once, indicating the glob has been expanded assert len(check_file.calls) > 1 # TODO: Test print() color output # TODO: Test log formatting ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/test_cli.py0000644000175100017510000000203215231700043015624 0ustar00runnerrunner# 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=1785167907.0 twine-7.0.0/tests/test_commands.py0000644000175100017510000000504015231700043016660 0ustar00runnerrunnerimport os import pytest from tests import helpers 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 def test_split_inputs(): """Split inputs into dists, signatures, and attestations.""" inputs = [ helpers.WHEEL_FIXTURE, helpers.WHEEL_FIXTURE + ".asc", helpers.WHEEL_FIXTURE + ".build.attestation", helpers.WHEEL_FIXTURE + ".publish.attestation", helpers.SDIST_FIXTURE, helpers.SDIST_FIXTURE + ".asc", helpers.NEW_WHEEL_FIXTURE, helpers.NEW_WHEEL_FIXTURE + ".frob.attestation", helpers.NEW_SDIST_FIXTURE, ] inputs = commands._split_inputs(inputs) assert inputs.dists == [ helpers.WHEEL_FIXTURE, helpers.SDIST_FIXTURE, helpers.NEW_WHEEL_FIXTURE, helpers.NEW_SDIST_FIXTURE, ] expected_signatures = { os.path.basename(dist) + ".asc": dist + ".asc" for dist in [helpers.WHEEL_FIXTURE, helpers.SDIST_FIXTURE] } assert inputs.signatures == expected_signatures assert inputs.attestations_by_dist == { helpers.WHEEL_FIXTURE: [ helpers.WHEEL_FIXTURE + ".build.attestation", helpers.WHEEL_FIXTURE + ".publish.attestation", ], helpers.SDIST_FIXTURE: [], helpers.NEW_WHEEL_FIXTURE: [helpers.NEW_WHEEL_FIXTURE + ".frob.attestation"], helpers.NEW_SDIST_FIXTURE: [], } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/test_integration.py0000644000175100017510000001330015231700043017400 0ustar00runnerrunnerimport 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("*") # NOTE: newer versions of setuptools (invoked via build) adhere to PEP 625, # causing the dist name to be `twine_sampleproject` instead of # `twine-sampleproject`. Both are allowed here for now, but the hyphenated # version can be removed eventually. # See: https://github.com/pypa/setuptools/issues/3593 assert dist.name in ( f"twine-sampleproject-3.0.0.post{tag}.tar.gz", f"twine_sampleproject-3.0.0.post{tag}.tar.gz", ) return dist sampleproject_token = ( "pypi-AgENdGVzdC5weXBpLm9yZwIkNDgzYTFhMjEtMzEwYi00NT" "kzLTkwMzYtYzc1Zjg4NmFiMjllAAJEeyJwZXJtaXNzaW9ucyI6IH" "sicHJvamVjdHMiOiBbInR3aW5lLXNhbXBsZXByb2plY3QiXX0sIC" "J2ZXJzaW9uIjogMX0AAAYg2kBZ1tN8lj8dlmL3ScoVvr_pvQE0t" "6PKqigoYJKvw3M" ) @pytest.mark.xfail(reason="service is unreliable (#684)") 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) @pytest.mark.xfail(reason="service is unreliable (#684)") 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-4.0.2-py3-none-any.whl", "twine-1.6.5.tar.gz", "twine-6.2.0-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=1785167907.0 twine-7.0.0/tests/test_main.py0000644000175100017510000000513115231700043016004 0ustar00runnerrunner# 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 pytest 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'" ) @pytest.mark.parametrize( ("status_code", "status_phrase", "reason"), [(400, "Bad Request", "Error reason"), (599, "Unknown Status", "Another reason")], ) def test_http_exception_handling( monkeypatch, capsys, status_code, status_phrase, reason ): 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=status_code, reason=reason ) ) ), ) error = dunder_main.main() assert error captured = capsys.readouterr() assert _unwrap_lines(captured.out) == ( f"{RED_ERROR} HTTPError: {status_code} {status_phrase} " f"from https://example.org {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=1785167907.0 twine-7.0.0/tests/test_package.py0000644000175100017510000004053715231700043016464 0ustar00runnerrunner# 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 json import string import packaging import pretend import pytest from packaging import metadata from packaging import version 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=metadata.RawMetadata(name="deprecated-pypirc", version="1.2.3"), 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=metadata.RawMetadata(name="deprecated-pypirc", version="1.2.3"), 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=metadata.RawMetadata(name="deprecated-pypirc", version="1.2.3"), python_version=None, filetype=None, ) assert package.signed_basefilename == "deprecated-pypirc.asc" assert package.signed_filename == (filename + ".asc") def test_package_add_attestations(tmp_path): package = package_file.PackageFile.from_filename(helpers.WHEEL_FIXTURE, None) assert package.attestations is None attestations = [] for i in range(3): path = tmp_path / f"fake.{i}.attestation" path.write_text(json.dumps({"fake": f"attestation {i}"})) attestations.append(str(path)) package.add_attestations(attestations) assert package.attestations == [ {"fake": "attestation 0"}, {"fake": "attestation 1"}, {"fake": "attestation 2"}, ] def test_package_add_attestations_invalid_json(tmp_path): package = package_file.PackageFile.from_filename(helpers.WHEEL_FIXTURE, None) assert package.attestations is None attestation = tmp_path / "fake.publish.attestation" attestation.write_text("this is not valid JSON") with pytest.raises( exceptions.InvalidDistribution, match="invalid JSON in attestation" ): package.add_attestations([attestation]) @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=metadata.RawMetadata(name=pkg_name, version="1.2.3"), python_version=None, filetype=None, ) assert package.safe_name == expected_name def test_metadata_keys_consistency(): """Check that the translation keys exist in the respective ``TypedDict``.""" raw_keys = metadata.RawMetadata.__annotations__.keys() if version.Version(packaging.__version__) < version.Version("24.1"): # ``packaging`` version 24.0 and earlier do not know about the # License-Expression and License-File metadata fields yet. raw_keys = raw_keys | set(("license_files", "license_expression")) assert set(package_file._RAW_TO_PACKAGE_METADATA.keys()).issubset(raw_keys) package_keys = package_file.PackageMetadata.__annotations__.keys() assert set(package_file._RAW_TO_PACKAGE_METADATA.values()).issubset(package_keys) @pytest.mark.parametrize("gpg_signature", [(None), (pretend.stub())]) @pytest.mark.parametrize("attestation", [(None), ({"fake": "attestation"})]) def test_metadata_dictionary_values(gpg_signature, attestation): """Pass values from ``metadata.RawMetadata`` through to ``PackageMetadata``.""" meta = metadata.RawMetadata( metadata_version=pretend.stub(), name="whatever", version="1.2.3", platforms=pretend.stub(), summary=pretend.stub(), description=pretend.stub(), keywords=pretend.stub(), home_page=pretend.stub(), author=pretend.stub(), author_email=pretend.stub(), license=pretend.stub(), supported_platforms=pretend.stub(), download_url=pretend.stub(), classifiers=pretend.stub(), provides=pretend.stub(), requires=pretend.stub(), obsoletes=pretend.stub(), maintainer=pretend.stub(), maintainer_email=pretend.stub(), requires_dist=pretend.stub(), provides_dist=pretend.stub(), obsoletes_dist=pretend.stub(), requires_python=pretend.stub(), requires_external=pretend.stub(), project_urls=pretend.stub(), description_content_type=pretend.stub(), provides_extra=pretend.stub(), dynamic=pretend.stub(), license_expression=pretend.stub(), license_files=pretend.stub(), ) package = package_file.PackageFile( filename="tests/fixtures/twine-4.0.2-py3-none-any.whl", comment=pretend.stub(), metadata=meta, python_version=pretend.stub(), filetype=pretend.stub(), ) package.gpg_signature = gpg_signature if attestation: package.attestations = [attestation] result = package.metadata_dictionary() # Medata 1.0 - PEP 241 assert result["metadata_version"] == meta["metadata_version"] assert result["name"] == package.safe_name assert result["version"] == package.version == meta["version"] assert result["platform"] == meta["platforms"] assert result["summary"] == meta["summary"] assert result["description"] == meta["description"] assert result["keywords"] == meta["keywords"] assert result["home_page"] == meta["home_page"] assert result["author"] == meta["author"] assert result["author_email"] == meta["author_email"] assert result["license"] == meta["license"] # Metadata 1.1 - PEP 314 assert result["supported_platform"] == meta["supported_platforms"] assert result["download_url"] == meta["download_url"] assert result["classifiers"] == meta["classifiers"] assert result["provides"] == meta["provides"] assert result["requires"] == meta["requires"] assert result["obsoletes"] == meta["obsoletes"] # Metadata 1.2 - PEP 345 assert result["maintainer"] == meta["maintainer"] assert result["maintainer_email"] == meta["maintainer_email"] assert result["requires_dist"] == meta["requires_dist"] assert result["provides_dist"] == meta["provides_dist"] assert result["obsoletes_dist"] == meta["obsoletes_dist"] assert result["requires_python"] == meta["requires_python"] assert result["requires_external"] == meta["requires_external"] assert result["project_urls"] == meta["project_urls"] # Metadata 2.1 - PEP 566 assert result["description_content_type"] == meta["description_content_type"] assert result["provides_extra"] == meta["provides_extra"] # Metadata 2.2 - PEP 643 assert result["dynamic"] == meta["dynamic"] # Metadata 2.4 - PEP 639 assert result["license_expression"] == meta["license_expression"] assert result["license_file"] == meta["license_files"] # Additional metadata assert result["comment"] == package.comment assert result["filetype"] == package.filetype assert result["pyversion"] == package.python_version # GPG signature assert result.get("gpg_signature") == gpg_signature # Attestations if attestation: assert result["attestations"] == json.dumps(package.attestations) else: assert "attestations" not in result TWINE_4_0_2_WHEEL_HEXDIGEST = package_file.Hexdigest( "929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8", "3a38a3f27a9e8ce45523d7d1e28c09e9085b61a98dab15d35ec086f36a44b37c", ) def test_hash_manager(): """Generate hexdigest via HashManager.""" filename = "tests/fixtures/twine-4.0.2-py3-none-any.whl" hasher = package_file.HashManager(filename) hasher.hash() assert hasher.hexdigest() == TWINE_4_0_2_WHEEL_HEXDIGEST @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-4.0.2-py3-none-any.whl" hasher = package_file.HashManager(filename) hasher.hash() hashes = TWINE_4_0_2_WHEEL_HEXDIGEST._replace(blake2=None) assert hasher.hexdigest() == hashes def test_fips_metadata_excludes_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")) monkeypatch.setattr(package_file.hashlib, "blake2b", replaced_blake2b) filename = "tests/fixtures/twine-4.0.2-py3-none-any.whl" pf = package_file.PackageFile.from_filename(filename, None) mddict = pf.metadata_dictionary() assert "blake2_256_digest" not in mddict @pytest.mark.parametrize( "read_data, exception_message", [ pytest.param( b"Metadata-Version: 102.3\nName: test-package\nVersion: 1.0.0\n", "'102.3' is not a valid metadata version", id="unsupported Metadata-Version", ), pytest.param( b"Metadata-Version: 2.3\nName: test-package\nVersion: UNKNOWN\n", "'UNKNOWN' is invalid for 'version'", id="invalid Version", ), pytest.param( b"Metadata-Version: 2.2\nName: test-package\nVersion: UNKNOWN\n", "'UNKNOWN' is invalid for 'version'", id="invalid Version", ), pytest.param( b"Metadata-Version: 2.3\n", "'name' is a required field; 'version' is a required field", id="missing Name and Version", ), pytest.param( b"Metadata-Version: 2.2\n", "'name' is a required field; 'version' is a required field", id="missing Name and Version", ), pytest.param( b"Metadata-Version: 2.3\nVersion: 1.0.0\n", "'name' is a required field", id="missing Name", ), pytest.param( b"Metadata-Version: 2.2\nVersion: 1.0.0\n", "'name' is a required field", id="missing Name", ), pytest.param( b"Metadata-Version: 2.3\nName: test-package\n", "'version' is a required field", id="missing Version", ), pytest.param( b"Metadata-Version: 2.2\nName: test-package\n", "'version' is a required field", id="missing Version", ), pytest.param( b"Metadata-Version: 2.2\nName: test-package\nVersion: 1.0.0\nFoo: bar\n", "unrecognized or malformed field 'foo'", id="unrecognized field", ), ], ) def test_pkginfo_returns_no_metadata(read_data, exception_message, monkeypatch): """Raise an exception when pkginfo can't interpret the metadata.""" monkeypatch.setattr(package_file.wheel.Wheel, "read", lambda _: read_data) filename = "tests/fixtures/twine-4.0.2-py3-none-any.whl" with pytest.raises(exceptions.InvalidDistribution) as err: package_file.PackageFile.from_filename(filename, comment=None) assert exception_message 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_sdist(): filename = "tests/fixtures/twine-1.5.0.tar.gz" p = package_file.PackageFile.from_filename(filename, comment=None) assert p.python_version == "source" def test_package_from_unrecognized_file_error(): filename = "twine/package.py" with pytest.raises(exceptions.InvalidDistribution) as err: package_file.PackageFile.from_filename(filename, comment=None) assert "Unknown distribution format" in err.value.args[0] def test_setuptools_license_file_invalid(monkeypatch): """Drop License-File metadata entries if Metadata-Version is less than 2.4.""" read_data = ( "Metadata-Version: 2.1\n" "Name: test-package\n" "Version: 1.0.0\n" "License-File: LICENSE\n" ) monkeypatch.setattr(package_file.wheel.Wheel, "read", lambda _: read_data) filename = "tests/fixtures/twine-4.0.2-py3-none-any.whl" package = package_file.PackageFile.from_filename(filename, comment=None) meta = package.metadata_dictionary() assert "license_file" not in meta @pytest.mark.skipif( version.Version(packaging.__version__) < version.Version("24.1"), reason="packaging is too old", ) def test_setuptools_license_file_valid(monkeypatch): """License-File metadata entries are kept when Metadata-Version is 2.4.""" read_data = ( "Metadata-Version: 2.4\n" "Name: test-package\n" "Version: 1.0.0\n" "License-File: LICENSE\n" ) monkeypatch.setattr(package_file.wheel.Wheel, "read", lambda _: read_data) filename = "tests/fixtures/twine-4.0.2-py3-none-any.whl" package = package_file.PackageFile.from_filename(filename, comment=None) meta = package.metadata_dictionary() assert "license_file" in meta ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/test_parse_config_encoding.py0000644000175100017510000001175715231700043021400 0ustar00runnerrunnerimport builtins import logging import pathlib from twine import utils def _write_utf8_ini(path, username: str = "テストユーザーðŸ��") -> None: """Write an ini file encoded in UTF-8. Including an emoji makes decoding more likely to fail under locales like cp932. """ content = f"""[server-login] username = {username} password = secret """ # Write explicitly as UTF-8 bytes # (so reading will fail if the reader assumes a different encoding) path.write_bytes(content.encode("utf-8")) def test_parse_config_triggers_utf8_fallback(monkeypatch, caplog, tmp_path): """Test UTF-8 fallback when UnicodeDecodeError is raised. If the first read of the file raises a UnicodeDecodeError, _parse_config should take the UTF-8 fallback path. This test simulates a decode failure on the first I/O and then allows normal I/O so the fallback is exercised. """ ini_path = tmp_path / "pypirc" expected_username = "テストユーザーðŸ��" _write_utf8_ini(ini_path, expected_username) # Coordinate a single "raise once" behavior across multiple common I/O # entrypoints. call = {"n": 0} original_open = builtins.open original_read_text = pathlib.Path.read_text def open_raise_once(*args, **kwargs): # Only raise on the very first attempted open; afterwards delegate # to real open. if call["n"] == 0: call["n"] += 1 # UnicodeDecodeError(encoding, object, start, end, reason) raise UnicodeDecodeError("utf-8", b"", 0, 1, "simulated") return original_open(*args, **kwargs) def read_text_raise_once(self, encoding=None, errors=None): # Only raise on the very first attempted read_text; afterwards # delegate. if call["n"] == 0: call["n"] += 1 raise UnicodeDecodeError("utf-8", b"", 0, 1, "simulated") return original_read_text(self, encoding=encoding, errors=errors) # Patch both builtins.open and pathlib.Path.read_text to be robust # against whichever API _parse_config uses internally. monkeypatch.setattr(builtins, "open", open_raise_once) monkeypatch.setattr(pathlib.Path, "read_text", read_text_raise_once, raising=True) caplog.set_level(logging.INFO, logger="twine") parser = utils._parse_config(str(ini_path)) # Ensure the parsed result is correct (file was read as UTF-8 after # fallback) assert parser.get("server-login", "username") == expected_username # Ensure a log message indicating the fallback is present assert ( "Configuration file not readable with default locale encoding, " "trying UTF-8" in caplog.text ) def test_parse_config_no_fallback_when_default_utf8(monkeypatch, caplog, tmp_path): """Test normal parsing when default encoding is UTF-8. When the default encoding is UTF-8, no fallback is necessary and the file should be parsed via the normal path. To make this deterministic across Python versions/environments, force I/O calls without an explicit encoding to use UTF-8 by wrapping open / Path.read_text. """ ini_path = tmp_path / "pypirc" expected_username = "テストユーザーðŸ��" _write_utf8_ini(ini_path, expected_username) # Wrap builtins.open so that if encoding is not provided, we force # utf-8. original_open = builtins.open def open_force_utf8( file, mode="r", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, ): if encoding is None and "b" not in mode: # delegate to real open but force utf-8 as default encoding return original_open( file, mode, buffering=buffering, encoding="utf-8", errors=errors, newline=newline, closefd=closefd, opener=opener, ) return original_open( file, mode, buffering=buffering, encoding=encoding, errors=errors, newline=newline, closefd=closefd, opener=opener, ) # Wrap pathlib.Path.read_text similarly original_read_text = pathlib.Path.read_text def read_text_force_utf8(self, encoding=None, errors=None): if encoding is None: return original_read_text(self, encoding="utf-8", errors=errors) return original_read_text(self, encoding=encoding, errors=errors) monkeypatch.setattr(builtins, "open", open_force_utf8) monkeypatch.setattr(pathlib.Path, "read_text", read_text_force_utf8, raising=True) caplog.set_level(logging.INFO, logger="twine") parser = utils._parse_config(str(ini_path)) # Ensure the parsed result is correct assert parser.get("server-login", "username") == expected_username # Verify that the used configuration file path is present in the logs. assert f"Using configuration from {ini_path}" in caplog.text ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/test_register.py0000644000175100017510000001020215231700043016677 0ustar00runnerrunnerimport 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, "TWINE_USERNAME": "pypiuser", "TWINE_PASSWORD": "pypipassword", "TWINE_CERT": "/foo/bar.crt", } for key, value in testenv.items(): monkeypatch.setenv(key, value) cli.dispatch(["register", helpers.WHEEL_FIXTURE]) register_settings = replaced_register.calls[0].args[0] assert "pypipassword" == register_settings.password assert "pypiuser" == 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", } for key, value in testenv.items(): monkeypatch.setenv(key, value) 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=1785167907.0 twine-7.0.0/tests/test_repository.py0000644000175100017510000002670615231700043017312 0ustar00runnerrunner# 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 packaging import pretend import pytest import requests from packaging import version from twine import package 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_metadata_to_list_of_tuples(data) assert tuples == [("gpg_signature", ("filename.asc", "filecontent"))] def test_iterables_are_flattened(): """Flatten iterable metadata to list of tuples.""" data = { "platform": ["UNKNOWN"], } tuples = repository.Repository._convert_metadata_to_list_of_tuples(data) assert tuples == [("platform", "UNKNOWN")] data = { "platform": ["UNKNOWN", "ANOTHERPLATFORM"], } tuples = repository.Repository._convert_metadata_to_list_of_tuples(data) assert tuples == [("platform", "UNKNOWN"), ("platform", "ANOTHERPLATFORM")] def test_all_metadata_fields_are_flattened(monkeypatch): """Verify that package metadata fields are correctly flattened.""" if version.Version(packaging.__version__) < version.Version("24.1"): # All metadata fields up to metadata version 2.3 metadata = open("tests/fixtures/everything.metadata23") else: # All metadata fields up to metadata version 2.4 metadata = open("tests/fixtures/everything.metadata24") monkeypatch.setattr(package.wheel.Wheel, "read", metadata.read) filename = "tests/fixtures/twine-4.0.2-py3-none-any.whl" data = package.PackageFile.from_filename( filename, comment="comment" ).metadata_dictionary() tuples = repository.Repository._convert_metadata_to_list_of_tuples(data) # Verifies that all metadata fields parsed by ``packaging.metadata`` are # correctly flattened into a list of (str, str) tuples. This does not # apply to the ``gpg_signature`` field, but this field is not added here # as there are specific tests for it. for key, value in tuples: assert isinstance(key, str) assert isinstance(value, str) 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", 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", 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", version="0.1", basefilename="fake.whl", ) 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", version="0.1", basefilename="fake.whl", ) 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", version="0.1", basefilename="foo.whl", ) 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, 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=1785167907.0 twine-7.0.0/tests/test_sdist.py0000644000175100017510000001123515231700043016210 0ustar00runnerrunnerimport os import pathlib import tarfile import pytest from twine import exceptions from twine import sdist from .helpers import TESTS_DIR from .helpers import build_archive @pytest.fixture( params=[ "fixtures/twine-1.5.0.tar.gz", "fixtures/twine-1.6.5.tar.gz", ] ) def example_sdist(request): file_name = os.path.join(TESTS_DIR, request.param) return sdist.SDist(file_name) @pytest.fixture(params=["tar.gz", "zip"]) def archive_format(request): return request.param def test_read_example(example_sdist): """Parse metadata from a valid sdist file.""" metadata = example_sdist.read() assert b"Metadata-Version: 1.1" in metadata assert b"Name: twine" in metadata assert b"Version: 1." in metadata def test_read_non_existent(): """Raise an exception when sdist file doesn't exist.""" file_name = str(pathlib.Path("/foo/bar/baz.tar.gz").resolve()) with pytest.raises(exceptions.InvalidDistribution, match="No such file"): sdist.SDist(file_name).read() def test_formar_not_supported(): """Raise an exception when sdist is not a .tar.gz or a .zip.""" file_name = str(pathlib.Path("/foo/bar/baz.foo").resolve()) with pytest.raises(exceptions.InvalidDistribution, match="Unsupported sdist"): sdist.SDist(file_name).read() def test_read(archive_format, tmp_path): """Read PKG-INFO from a valid sdist.""" filepath = build_archive( tmp_path, "test-1.2.3", archive_format, { "test-1.2.3/README": "README", "test-1.2.3/PKG-INFO": """ Metadata-Version: 1.1 Name: test Version: 1.2.3 """, }, ) metadata = sdist.SDist(str(filepath)).read() assert b"Metadata-Version: 1.1" in metadata assert b"Name: test" in metadata assert b"Version: 1.2.3" in metadata def test_missing_pkg_info(archive_format, tmp_path): """Raise an exception when sdist does not contain PKG-INFO.""" filepath = build_archive( tmp_path, "test-1.2.3", archive_format, { "test-1.2.3/README": "README", }, ) with pytest.raises(exceptions.InvalidDistribution, match="No PKG-INFO in archive"): sdist.SDist(str(filepath)).read() def test_invalid_pkg_info(archive_format, tmp_path): """Raise an exception when PKG-INFO does not contain ``Metadata-Version``.""" filepath = build_archive( tmp_path, "test-1.2.3", archive_format, { "test-1.2.3/README": "README", "test-1.2.3/PKG-INFO": """ Name: test Version: 1.2.3. """, }, ) with pytest.raises(exceptions.InvalidDistribution, match="No PKG-INFO in archive"): sdist.SDist(str(filepath)).read() def test_pkg_info_directory(archive_format, tmp_path): """Raise an exception when PKG-INFO is a directory.""" filepath = build_archive( tmp_path, "test-1.2.3", archive_format, { "test-1.2.3/README": "README", "test-1.2.3/PKG-INFO/content": """ Metadata-Version: 1.1 Name: test Version: 1.2.3. """, }, ) with pytest.raises(exceptions.InvalidDistribution, match="No PKG-INFO in archive"): sdist.SDist(str(filepath)).read() def test_pkg_info_not_regular_file(tmp_path): """Raise an exception when PKG-INFO is a directory.""" link = tarfile.TarInfo() link.type = tarfile.LNKTYPE link.linkname = "README" filepath = build_archive( tmp_path, "test-1.2.3", "tar.gz", { "test-1.2.3/README": "README", "test-1.2.3/PKG-INFO": link, }, ) with pytest.raises( exceptions.InvalidDistribution, match=r"^PKG-INFO is not a reg.*test-1.2.3.tar.gz$", ): sdist.SDist(str(filepath)).read() def test_multiple_top_level(archive_format, tmp_path): """Raise an exception when there are too many top-level members.""" filepath = build_archive( tmp_path, "test-1.2.3", archive_format, { "test-1.2.3/README": "README", "test-1.2.3/PKG-INFO": """ Metadata-Version: 1.1 Name: test Version: 1.2.3. """, "test-2.0.0/README": "README", }, ) with pytest.raises( exceptions.InvalidDistribution, match=r"^Too many top-level.*test-1.2.3.(tar.gz|zip)$", ): sdist.SDist(str(filepath)).read() def test_py_version(example_sdist): assert example_sdist.py_version == "source" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/test_settings.py0000644000175100017510000001536115231700043016726 0ustar00runnerrunner"""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 repository 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.client_cert is None assert s.disable_progress_bar is False def test_settings_verify_feature_compatibility() -> None: s = settings.Settings(skip_existing=True) s.repository_config = {"repository": repository.WAREHOUSE} try: s.verify_feature_capability() except exceptions.UnsupportedConfiguration as unexpected_exc: pytest.fail( "Expected feature capability to work with production PyPI" f" but got {unexpected_exc!r}" ) s.repository_config["repository"] = repository.TEST_WAREHOUSE try: s.verify_feature_capability() except exceptions.UnsupportedConfiguration as unexpected_exc: pytest.fail( "Expected feature capability to work with TestPyPI but got" f" {unexpected_exc!r}" ) s.repository_config["repository"] = "https://not-really-pypi.example.com/legacy" with pytest.raises(exceptions.UnsupportedConfiguration): s.verify_feature_capability() s.skip_existing = False try: s.verify_feature_capability() except exceptions.UnsupportedConfiguration as unexpected_exc: pytest.fail( "Expected an exception only when --skip-existing is provided" f" but got {unexpected_exc!r}" ) @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 def test_attestations_flag(self): args = self.parse_args(["--attestations"]) assert args.attestations ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/test_upload.py0000644000175100017510000004403515231700043016352 0ustar00runnerrunner# 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/4.0.2/" NEW_RELEASE_URL = "https://pypi.org/project/twine/6.2.0/" @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 = "35.5 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 package.attestations is 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 = "41.7 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_make_package_attestations_flagged_but_missing(upload_settings): """Fail when the user requests attestations but does not supply any attestations.""" upload_settings.attestations = True with pytest.raises( exceptions.InvalidDistribution, match="Upload with attestations requested" ): upload._make_package(helpers.NEW_WHEEL_FIXTURE, {}, [], upload_settings) def test_success_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: "35.5 KB", helpers.NEW_WHEEL_FIXTURE: "41.7 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-4.0.2-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-4.0.2-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-4.0.2-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} (35.5 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-4.0.2-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-4.0.2-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-4.0.2-py3-none-any.whl" already ' "exists for twine-4.0.2." ), ), id="pypi", ), pytest.param( dict( status_code=409, reason=( 'A file named "twine-4.0.2-py3-none-any.whl" already ' "exists for twine-4.0.2." ), ), id="pypiserver", ), ], ) 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, "TWINE_USERNAME": "pypiuser", "TWINE_PASSWORD": "pypipassword", "TWINE_CERT": "/foo/bar.crt", } for key, value in testenv.items(): monkeypatch.setenv(key, value) cli.dispatch(["upload", "path/to/file"]) upload_settings = replaced_upload.calls[0].args[0] assert "pypipassword" == upload_settings.password assert "pypiuser" == 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", } for key, value in testenv.items(): monkeypatch.setenv(key, value) 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, ], ) def test_upload_warns_attestations_non_pypi(upload_settings, caplog, stub_response): upload_settings.repository_config["repository"] = "https://notpypi.example.com" upload_settings.attestations = True # This fails because the attestation isn't a real file, which is fine # since our functionality under test happens before the failure. with pytest.raises(exceptions.InvalidDistribution): upload.upload( upload_settings, [helpers.WHEEL_FIXTURE, helpers.WHEEL_FIXTURE + ".foo.attestation"], ) assert ( "Only PyPI and TestPyPI support attestations; if you experience " "failures, remove the --attestations flag and re-try this command" in caplog.messages ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tests/test_utils.py0000644000175100017510000002507215231700043016226 0ustar00runnerrunner# 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 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( "repository_url, expected_config", [ ( "https://user:pass@notexisting.python.org/pypi", { "repository": "https://notexisting.python.org/pypi", "username": "user", "password": "pass", }, ), ( "https://auser:pass@pypi.proxy.local.repo.net:8443", { "repository": "https://pypi.proxy.local.repo.net:8443", "username": "auser", "password": "pass", }, ), ], ) def test_get_repository_config_url_with_auth( config_file, repository_url, expected_config ): assert ( utils.get_repository_from_config(config_file, "foo", repository_url) == expected_config ) assert ( utils.get_repository_from_config(config_file, "pypi", repository_url) == expected_config ) @pytest.mark.parametrize( "input_url, expected_url", [ ("https://upload.pypi.org/legacy/", "https://upload.pypi.org/legacy/"), ( "https://user:pass@upload.pypi.org/legacy/", "https://********@upload.pypi.org/legacy/", ), ], ) def test_sanitize_url(input_url: str, expected_url: str) -> None: assert utils.sanitize_url(input_url) == expected_url @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( "invalid_config", [ # No surrounding [server] section """ username = testuser password = testpassword """, # Valid section but bare API token """ [pypi] pypi-lolololol """, # No section, bare API token """ pypi-lolololol """, ], ) def test_get_repository_config_invalid_syntax(write_config_file, invalid_config): """Raise an exception when the .pypirc has invalid syntax.""" config_file = write_config_file(invalid_config) with pytest.raises( exceptions.InvalidConfiguration, match="Malformed configuration", ): utils.get_repository_from_config(config_file, "pypi") @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( monkeypatch, env_name, default, environ, expected ): option_strings = ("-x", "--example") dest = "example" for key, value in environ.items(): monkeypatch.setenv(key, value) 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=1785167907.0 twine-7.0.0/tests/test_wheel.py0000644000175100017510000000574415231700043016176 0ustar00runnerrunner# 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 pathlib import re import zipfile import pretend import pytest from twine import exceptions from twine import wheel @pytest.fixture() def example_wheel(request): parent = pathlib.Path(__file__).parent file_name = str(parent / "fixtures" / "twine-4.0.2-py3-none-any.whl") return wheel.Wheel(file_name) def test_version_parsing(example_wheel): assert example_wheel.py_version == "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: 4.0.2" 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).read() 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).read() 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 or METADATA missing 'Metadata-Version': {whl_file}" ), ): wheel.Wheel(whl_file).read() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/tox.ini0000644000175100017510000000644315231700043013627 0ustar00runnerrunner[tox] minversion = 3.10 envlist = lint,types,py{310,311,312,313,314},integration,docs isolated_build = True [testenv] deps = pretend pytest pytest-socket coverage passenv = PYTEST_ADDOPTS setenv = HOME=/tmp/nonexistent COVERAGE_FILE={env_tmp_dir}/.coverage 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 # required for report generation. 5.2.1 is forbidden due to an observed # broken wheel on CPython 3.8: # https://bugs.launchpad.net/lxml/+bug/2064158 lxml >= 5.2.0, != 5.2.1 # 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 = pip list 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} # Usage: # tox -e create-changelog-item -- [additional arguments] {filename}.{bugfix,feature,doc,removal,misc} [testenv:create-changelog-item] basepython = python3 skip_install = True deps = towncrier commands = towncrier create --config pyproject.toml {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=1785167912.5014184 twine-7.0.0/twine/0000755000175100017510000000000015231700051013432 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/twine/__init__.py0000644000175100017510000000275415231700043015554 0ustar00runnerrunner"""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 email.utils import importlib.metadata as importlib_metadata metadata = importlib_metadata.metadata("twine") assert metadata is not None # nosec: B101 __title__ = metadata["name"] __summary__ = metadata["summary"] __uri__ = next( entry.split(", ")[1] for entry in metadata.get_all("Project-URL", ()) if entry.startswith("Homepage") ) __version__ = metadata["version"] __author__, __email__ = email.utils.parseaddr(metadata["author-email"]) __license__ = None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/twine/__main__.py0000644000175100017510000000423415231700043015530 0ustar00runnerrunner#!/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 try: status_phrase = http.HTTPStatus(status_code).phrase except ValueError: # HTTPStatus will raise ValueError if the server responds with # a non-standard status code. This is almost certainly an upstream # index error since non-standard status codes should only be used # for internal signaling, but there's nothing we can do about that # here other than avoid propagating it as an unhandled exception. # # See: # See: status_phrase = "Unknown Status" 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=1785167907.0 twine-7.0.0/twine/auth.py0000644000175100017510000002541315231700043014753 0ustar00runnerrunnerimport datetime import functools import getpass import json import logging import time import typing as t from typing import cast from urllib.parse import urlparse import requests.auth from id import AmbientCredentialError from id import detect_credential # keyring has an indirect dependency on PyCA cryptography, which has no # pre-built wheels for ppc64le and s390x, see #1158. if t.TYPE_CHECKING: import keyring from keyring.errors import NoKeyringError else: try: import keyring from keyring.errors import NoKeyringError except ModuleNotFoundError: # pragma: no cover keyring = None NoKeyringError = None from twine import exceptions from twine import utils logger = logging.getLogger(__name__) TOKEN_USERNAME: t.Final[str] = "__token__" #: Tokens expire after 15 minutes, let's start allowing renewal/replacement #: after 10 minutes that way if we fail, we may still have time to replace it #: before it expires. Thus, if our current time + this threshold is past the #: greater or equal to the expiration time, we should start trying to replace #: the token. TOKEN_RENEWAL_THRESHOLD: t.Final[datetime.timedelta] = datetime.timedelta( minutes=5, ) class CredentialInput: def __init__( self, username: t.Optional[str] = None, password: t.Optional[str] = None ) -> None: self.username = username self.password = password class TrustedPublishingTokenRetrievalError(t.TypedDict): code: str description: str class TrustedPublishingToken(t.TypedDict, total=False): message: t.Optional[str] errors: t.Optional[list[TrustedPublishingTokenRetrievalError]] token: t.Optional[str] success: t.Optional[bool] # Depends on https://github.com/pypi/warehouse/issues/18235 expires: t.Optional[int] class TrustedPublishingAuthenticator(requests.auth.AuthBase): def __init__( self, resolver: "Resolver", ) -> None: self.resolver = resolver def __call__( self, request: "requests.models.PreparedRequest" ) -> "requests.models.PreparedRequest": token = self.resolver.make_trusted_publishing_token() if token is None: raise exceptions.TrustedPublishingFailure( "Expected a trusted publishing token but got None" ) # Instead of reconstructing Basic Auth headers ourself, let's just # rely on the underlying class to do the right thing. basic_auth = requests.auth.HTTPBasicAuth( username=TOKEN_USERNAME, password=token, ) return cast(requests.models.PreparedRequest, basic_auth(request)) class Resolver: _tp_token: t.Optional[TrustedPublishingToken] = None _expires: t.Optional[int] = None def __init__( self, config: utils.RepositoryConfig, input: CredentialInput, ) -> None: self.config = config self.input = input @property @functools.lru_cache() def authenticator(self) -> "requests.auth.AuthBase": username = self.username password = self.password if self._tp_token: # If `self.password` ended up getting a Trusted Publishing token, # we've cached it here so we should use that as the authenticator. # We have a custom authenticator so we can repeatedly invoke # `make_trusted_publishing_token` which if the token is 10 minutes # old or more, we should get a new one automatically. return TrustedPublishingAuthenticator(resolver=self) if username and password: return requests.auth.HTTPBasicAuth( username=username, password=password, ) raise exceptions.InvalidConfiguration( "could not determine credentials for configured repository" ) @classmethod def choose(cls, interactive: bool) -> t.Type["Resolver"]: return cls if interactive else Private @property @functools.lru_cache() def username(self) -> t.Optional[str]: if self.is_pypi() and not self.input.username: # Default username. self.input.username = TOKEN_USERNAME 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) -> t.Optional[str]: return utils.get_userpass_value( self.input.password, self.config, key="password", prompt_strategy=self.password_from_keyring_or_trusted_publishing_or_prompt, ) def _has_valid_cached_tp_token(self) -> bool: return self._tp_token is not None and ( int(time.time()) + TOKEN_RENEWAL_THRESHOLD.seconds < cast(int, self._tp_token.get("expires", self._expires)) ) def _make_trusted_publishing_token(self) -> t.Optional[TrustedPublishingToken]: if self._has_valid_cached_tp_token(): return self._tp_token # Trusted publishing (OpenID Connect): get one token from the CI # system, and exchange that for a PyPI token. repository_domain = cast(str, urlparse(self.system).netloc) session = utils.make_requests_session() # Indices are expected to support `https://{domain}/_/oidc/audience`, # which tells OIDC exchange clients which audience to use. audience_url = f"https://{repository_domain}/_/oidc/audience" resp = session.get(audience_url, timeout=5) resp.raise_for_status() audience = cast(str, resp.json()["audience"]) try: oidc_token = detect_credential(audience) except AmbientCredentialError as e: # If we get here, we're on a supported CI platform for trusted # publishing, and we have not been given any token, so we can error. raise exceptions.TrustedPublishingFailure( "Unable to retrieve an OIDC token from the CI platform for " f"trusted publishing {e}" ) if oidc_token is None: logger.warning("This environment is not supported for trusted publishing") if self._tp_token and int(time.time()) > cast( int, self._tp_token.get("expires", self._expires) ): return None # Fall back to prompting for a token (if possible) # The cached trusted publishing token may still be valid for a # while longer, let's continue using it instead of prompting return self._tp_token logger.warning("Got OIDC token for audience %s", audience) token_exchange_url = f"https://{repository_domain}/_/oidc/mint-token" mint_token_resp = session.post( token_exchange_url, json={"token": oidc_token}, timeout=5, # S113 wants a timeout ) try: mint_token_payload = mint_token_resp.json() except json.JSONDecodeError: raise exceptions.TrustedPublishingFailure( "The token-minting request returned invalid JSON" ) if not mint_token_resp.ok: reasons = "\n".join( f"* `{error['code']}`: {error['description']}" for error in mint_token_payload["errors"] ) raise exceptions.TrustedPublishingFailure( "The token request failed; the index server gave the following" f" reasons:\n\n{reasons}" ) logger.warning("Minted upload token for trusted publishing") self._tp_token = cast(TrustedPublishingToken, mint_token_payload) self._expires = int(time.time()) + 900 return self._tp_token def make_trusted_publishing_token(self) -> t.Optional[str]: mint_token_payload = self._make_trusted_publishing_token() if not mint_token_payload: return None return cast(str, mint_token_payload["token"]) @property def system(self) -> t.Optional[str]: return self.config["repository"] def get_username_from_keyring(self) -> t.Optional[str]: if keyring is None: logger.info("keyring module is not available") return None try: system = cast(str, self.system) logger.info("Querying keyring for username") creds = keyring.get_credential(system, None) if creds: return 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) -> t.Optional[str]: if keyring is None: logger.info("keyring module is not available") return None 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 NoKeyringError: logger.info("No keyring backend found") 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_trusted_publishing_or_prompt(self) -> str: password = self.get_password_from_keyring() if password: logger.info("password set from keyring") return password if self.is_pypi() and self.username == TOKEN_USERNAME: logger.info( "Trying to use trusted publishing (no token was explicitly provided)" ) if (token := self.make_trusted_publishing_token()) is not None: return token # Prompt for API token when required. what = "API token" if self.is_pypi() else "password" return self.prompt(what, getpass.getpass) def prompt(self, what: str, how: t.Callable[..., str]) -> str: return how(f"Enter your {what}: ") def is_pypi(self) -> bool: """As of 2024-01-01, PyPI requires API tokens for uploads.""" return cast(str, self.config["repository"]).startswith( ( utils.DEFAULT_REPOSITORY, utils.TEST_REPOSITORY, ) ) class Private(Resolver): def prompt(self, what: str, how: t.Optional[t.Callable[..., str]] = None) -> str: raise exceptions.NonInteractive(f"Credential not found for {what}.") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/twine/cli.py0000644000175100017510000000760415231700043014563 0ustar00runnerrunner# 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 importlib.metadata as importlib_metadata import logging.config from typing import Any, List, Tuple 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 = [ "readme-renderer", "requests", "requests-toolbelt", "urllib3", "keyring", # optional for non-desktop use "rfc3986", "rich", "packaging", "id", ] result: List[Tuple[str, str]] = [] for dep in deps: try: version = importlib_metadata.version(dep) except importlib_metadata.PackageNotFoundError: version = "NOT INSTALLED" result.append((dep, version)) return result 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() return main(args.args) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1785167912.5028415 twine-7.0.0/twine/commands/0000755000175100017510000000000015231700051015233 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/twine/commands/__init__.py0000644000175100017510000000571115231700043017351 0ustar00runnerrunner"""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 fnmatch import glob import os.path from typing import Dict, List, NamedTuple 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) class Inputs(NamedTuple): """Represents structured user inputs.""" dists: List[str] signatures: Dict[str, str] attestations_by_dist: Dict[str, List[str]] def _split_inputs( inputs: List[str], ) -> Inputs: """ Split the unstructured list of input files provided by the user into groups. Three groups are returned: upload files (i.e. dists), signatures, and attestations. Upload files are returned as a linear list, signatures are returned as a dict of ``basename -> path``, and attestations are returned as a dict of ``dist-path -> [attestation-path]``. """ signatures = {os.path.basename(i): i for i in fnmatch.filter(inputs, "*.asc")} attestations = fnmatch.filter(inputs, "*.*.attestation") dists = [ dist for dist in inputs if dist not in (set(signatures.values()) | set(attestations)) ] attestations_by_dist = {} for dist in dists: dist_basename = os.path.basename(dist) attestations_by_dist[dist] = [ a for a in attestations if os.path.basename(a).startswith(dist_basename) ] return Inputs(dists, signatures, attestations_by_dist) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/twine/commands/check.py0000644000175100017510000001355515231700043016674 0ustar00runnerrunner"""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, Tuple 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 = metadata.get("description") description_content_type = metadata.get("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 not description 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. :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``. """ dists = commands._find_dists(dists) uploads, _, _ = commands._split_inputs(dists) 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=1785167907.0 twine-7.0.0/twine/commands/register.py0000644000175100017510000000553115231700043017436 0ustar00runnerrunner"""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=1785167907.0 twine-7.0.0/twine/commands/upload.py0000644000175100017510000002300115231700043017066 0ustar00runnerrunner"""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 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])) ) def _make_package( filename: str, signatures: Dict[str, str], attestations: List[str], upload_settings: settings.Settings, ) -> package_file.PackageFile: """Create and sign a package, based off of filename, signatures, and settings. Additionally, any supplied attestations are attached to the package when the settings indicate to do so. """ 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) # Attestations are only attached if explicitly requested with `--attestations`. if upload_settings.attestations: # Passing `--attestations` without any actual attestations present # indicates user confusion, so we fail rather than silently allowing it. if not attestations: raise exceptions.InvalidDistribution( "Upload with attestations requested, but " f"{filename} has no associated attestations" ) package.add_attestations(attestations) 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`` and ``.attestation`` files, which will be added to their respective file uploads. :raises twine.exceptions.TwineException: The upload failed due to a configuration error. :raises requests.HTTPError: The repository responded with an error. """ upload_settings.check_repository_url() upload_settings.verify_feature_capability() repository_url = cast(str, upload_settings.repository_config["repository"]) # Attestations are only supported on PyPI and TestPyPI at the moment. # We warn instead of failing to allow twine to be used in local testing # setups (where the PyPI deployment doesn't have a well-known domain). if upload_settings.attestations and not repository_url.startswith( (utils.DEFAULT_REPOSITORY, utils.TEST_REPOSITORY) ): logger.warning( "Only PyPI and TestPyPI support attestations; " "if you experience failures, remove the --attestations flag and " "re-try this command" ) dists = commands._find_dists(dists) # Determine if the user has passed in pre-signed distributions or any attestations. uploads, signatures, attestations_by_dist = commands._split_inputs(dists) print(f"Uploading distributions to {utils.sanitize_url(repository_url)}") packages_to_upload = [ _make_package( filename, signatures, attestations_by_dist[filename], 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( utils.sanitize_url(repository_url), utils.sanitize_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=1785167907.0 twine-7.0.0/twine/distribution.py0000644000175100017510000000023115231700043016520 0ustar00runnerrunnerclass Distribution: def read(self) -> bytes: raise NotImplementedError @property def py_version(self) -> str: return "any" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/twine/exceptions.py0000644000175100017510000001302615231700043016170 0ustar00runnerrunner"""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. import typing as t 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 UnsupportedConfiguration(TwineException): """An upload attempt was detected using features not supported by a repository. The features specified either in configuration or on the command-line. """ class Builder: """Build the parameters for an UnsupportedConfiguration exception. In the event we add additional features we are not allowing with something other than PyPI or TestPyPI, we can use a builder to accumulate them all instead of requiring someone to run multiple times to discover all unsupported configuration options. """ repository_url: str features: t.List[str] def __init__(self) -> None: self.repository_url = "" self.features = [] def with_repository_url( self, repository_url: str ) -> "UnsupportedConfiguration.Builder": self.repository_url = repository_url return self def with_feature(self, feature: str) -> "UnsupportedConfiguration.Builder": self.features.append(feature) return self def finalize(self) -> "UnsupportedConfiguration": return UnsupportedConfiguration( f"The configured repository {self.repository_url!r} does not " "have support for the following features: " f"{', '.join(self.features)} and is an unsupported " "configuration", self.repository_url, *self.features, ) 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 TrustedPublishingFailure(TwineException): """Raised if we expected to use trusted publishing but couldn't.""" 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 class UnableToReadConfigurationFile(TwineException): """Configuration file exists but cannot be read (e.g. encoding issue).""" pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/twine/package.py0000644000175100017510000003150015231700043015377 0ustar00runnerrunner# 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 json import logging import os import re import subprocess from typing import Any, Dict, List, NamedTuple, Optional, Tuple, TypedDict from packaging import errors from packaging import metadata from packaging import version from rich import print from twine import exceptions from twine import sdist from twine import wheel DIST_TYPES = { "bdist_wheel": wheel.Wheel, "sdist": sdist.SDist, } DIST_EXTENSIONS = { ".whl": "bdist_wheel", ".tar.gz": "sdist", ".zip": "sdist", } 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) # Map ``metadata.RawMetadata`` fields to ``PackageMetadata`` fields. Some # fields are renamed to match the names expected in the upload form. _RAW_TO_PACKAGE_METADATA = { # Metadata 1.0 - PEP 241 "metadata_version": "metadata_version", "name": "name", "version": "version", "platforms": "platform", # Renamed "summary": "summary", "description": "description", "keywords": "keywords", "home_page": "home_page", "author": "author", "author_email": "author_email", "license": "license", # Metadata 1.1 - PEP 314 "supported_platforms": "supported_platform", # Renamed "download_url": "download_url", "classifiers": "classifiers", "requires": "requires", "provides": "provides", "obsoletes": "obsoletes", # Metadata 1.2 - PEP 345 "maintainer": "maintainer", "maintainer_email": "maintainer_email", "requires_dist": "requires_dist", "provides_dist": "provides_dist", "obsoletes_dist": "obsoletes_dist", "requires_python": "requires_python", "requires_external": "requires_external", "project_urls": "project_urls", # Metadata 2.1 - PEP 566 "description_content_type": "description_content_type", "provides_extra": "provides_extra", # Metadata 2.2 - PEP 643 "dynamic": "dynamic", # Metadata 2.4 - PEP 639 "license_expression": "license_expression", "license_files": "license_file", # Renamed } class PackageMetadata(TypedDict, total=False): # Metadata 1.0 - PEP 241 metadata_version: str name: str version: str platform: List[str] summary: str description: str keywords: List[str] home_page: str author: str author_email: str license: str # Metadata 1.1 - PEP 314 supported_platform: List[str] download_url: str classifiers: List[str] requires: List[str] provides: List[str] obsoletes: List[str] # Metadata 1.2 - PEP 345 maintainer: str maintainer_email: str requires_dist: List[str] provides_dist: List[str] obsoletes_dist: List[str] requires_python: str requires_external: List[str] project_urls: Dict[str, str] # Metadata 2.1 - PEP 566 description_content_type: str provides_extra: List[str] # Metadata 2.2 - PEP 643 dynamic: List[str] # Metadata 2.4 - PEP 639 license_expression: str license_file: List[str] # Additional metadata comment: str pyversion: str filetype: str gpg_signature: Tuple[str, bytes] attestations: str sha256_digest: str blake2_256_digest: str class PackageFile: def __init__( self, filename: str, comment: Optional[str], metadata: metadata.RawMetadata, python_version: str, filetype: 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.version: str = metadata["version"] self.signed_filename = self.filename + ".asc" self.signed_basefilename = self.basefilename + ".asc" self.gpg_signature: Optional[Tuple[str, bytes]] = None self.attestations: Optional[List[Dict[Any, str]]] = None hasher = HashManager(filename) hasher.hash() hexdigest = hasher.hexdigest() 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: dist = DIST_TYPES[dtype](filename) data = dist.read() py_version = dist.py_version 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) ) # Parse and validate metadata. meta, unparsed = metadata.parse_email(data) # setuptools emits License-File metadata fields while declaring # Metadata-Version 2.1. This is invalid because the metadata # specification does not allow to add arbitrary fields, and because # the semantic implemented by setuptools is different than the one # described in PEP 639. However, rejecting these packages would be # too disruptive. Drop License-File metadata entries from the data # sent to the package index if the declared metadata version is less # than 2.4. if version.Version(meta.get("metadata_version", "0")) < version.Version("2.4"): meta.pop("license_files", None) # Support for metadata version 2.4 requires packaging version 24.1 # or later. When parsing metadata with an older packaging, the # invalid License-File fields are not understood and added to the # unparsed dictionary. Remove them to avoid triggering the # following check. unparsed.pop("license-file", None) if unparsed: raise exceptions.InvalidDistribution( "Invalid distribution metadata: {}".format( "; ".join( f"unrecognized or malformed field {key!r}" for key in unparsed ) ) ) try: metadata.Metadata.from_raw(meta) except errors.ExceptionGroup as group: raise exceptions.InvalidDistribution( "Invalid distribution metadata: {}".format( "; ".join(sorted(str(e) for e in group.exceptions)) ) ) return cls(filename, comment, meta, py_version, dtype) def metadata_dictionary(self) -> PackageMetadata: """Merge multiple sources of metadata into a single dictionary. Includes values from filename, PKG-INFO, hashers, and signature. """ data = PackageMetadata() for key, value in self.metadata.items(): field = _RAW_TO_PACKAGE_METADATA.get(key) if field: # A ``TypedDict`` only support literal key names. Here key # names are computed but they can only be valid key names. data[field] = value # type: ignore[literal-required] # Override name with safe name. data["name"] = self.safe_name # File content. data["pyversion"] = self.python_version data["filetype"] = self.filetype # Additional meta-data: some of these fields may not be set. Some # package repositories do not allow null values, so this only sends # non-null values. In particular, FIPS disables Blake2, making # the digest values null. See https://github.com/pypa/twine/issues/775 if self.comment is not None: data["comment"] = self.comment if self.sha2_digest is not None: data["sha256_digest"] = self.sha2_digest if self.gpg_signature is not None: data["gpg_signature"] = self.gpg_signature if self.attestations is not None: data["attestations"] = json.dumps(self.attestations) if self.blake2_256_digest: data["blake2_256_digest"] = self.blake2_256_digest return data def add_attestations(self, attestations: List[str]) -> None: loaded_attestations = [] for attestation in attestations: with open(attestation, "rb") as att: try: loaded_attestations.append(json.load(att)) except json.JSONDecodeError: raise exceptions.InvalidDistribution( f"invalid JSON in attestation: {attestation}" ) self.attestations = loaded_attestations 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): 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._sha2_hasher = hashlib.sha256() self._blake_hasher = None try: self._blake_hasher = hashlib.blake2b(digest_size=256 // 8) except (ValueError, TypeError, AttributeError): # FIPS mode disables blake2 pass 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._sha2_update(content) self._blake_update(content) def hexdigest(self) -> Hexdigest: """Return the hexdigest for the file.""" return Hexdigest( self._sha2_hexdigest(), self._blake_hexdigest(), ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/twine/py.typed0000644000175100017510000000000015231700043015120 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/twine/repository.py0000644000175100017510000002150015231700043016222 0ustar00runnerrunner# 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 import requests import requests_toolbelt import rich.progress from rich import print from twine import package as package_file from twine.utils import make_requests_session 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 = make_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'}>") # 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 def close(self) -> None: self.session.close() @staticmethod def _convert_metadata_to_list_of_tuples( data: package_file.PackageMetadata, ) -> List[Tuple[str, Any]]: # This does what ``warehouse.forklift.parse_form_metadata()`` does, in reverse. data_to_send: List[Tuple[str, Any]] = [] for key, value in data.items(): if key == "gpg_signature": assert isinstance(value, tuple) data_to_send.append((key, value)) elif key == "project_urls": assert isinstance(value, dict) for name, url in value.items(): data_to_send.append((key, f"{name}, {url}")) elif key == "keywords": assert isinstance(value, list) data_to_send.append((key, ", ".join(value))) elif isinstance(value, (list, tuple)): data_to_send.extend((key, item) for item in value) else: assert isinstance(value, str) data_to_send.append((key, value)) 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: print(f"Registering {package.basefilename}") metadata = package.metadata_dictionary() data_to_send = self._convert_metadata_to_list_of_tuples(metadata) data_to_send.append((":action", "submit")) data_to_send.append(("protocol_version", "1")) 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: print(f"Uploading {package.basefilename}") metadata = package.metadata_dictionary() data_to_send = self._convert_metadata_to_list_of_tuples(metadata) data_to_send.append((":action", "file_upload")) data_to_send.append(("protocol_version", "1")) 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: """Determine if a package has been uploaded to PyPI already. .. warning:: This does not support indexes other than PyPI or TestPyPI :param package: The package file that will otherwise be uploaded. :type package: :class:`~twine.package.PackageFile` :param bypass_cache: Force a request to PyPI. :type bypass_cache: bool :returns: True if package has already been uploaded, False otherwise :rtype: 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.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.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 its hash against what it has locally. pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/twine/sdist.py0000644000175100017510000000554615231700043015145 0ustar00runnerrunnerimport os import tarfile import zipfile from contextlib import suppress from twine import distribution from twine import exceptions class SDist(distribution.Distribution): def __new__(cls, filename: str) -> "SDist": if cls is not SDist: return object.__new__(cls) FORMATS = { ".tar.gz": TarGzSDist, ".zip": ZipSDist, } for extension, impl in FORMATS.items(): if filename.endswith(extension): return impl(filename) raise exceptions.InvalidDistribution(f"Unsupported sdist format: {filename}") def __init__(self, filename: str) -> None: if not os.path.exists(filename): raise exceptions.InvalidDistribution(f"No such file: {filename}") self.filename = filename @property def py_version(self) -> str: return "source" class TarGzSDist(SDist): def read(self) -> bytes: with tarfile.open(self.filename, "r:gz") as sdist: # The sdist must contain a single top-level directory... root = os.path.commonpath(sdist.getnames()) if root in {".", "/", ""}: raise exceptions.InvalidDistribution( f"Too many top-level members in sdist archive: {self.filename}" ) # ...containing the package metadata in a ``PKG-INFO`` file. with suppress(KeyError): member = sdist.getmember(root.rstrip("/") + "/PKG-INFO") if not member.isfile(): raise exceptions.InvalidDistribution( f"PKG-INFO is not a regular file: {self.filename}" ) fd = sdist.extractfile(member) assert fd is not None, "for mypy" data = fd.read() if b"Metadata-Version" in data: return data raise exceptions.InvalidDistribution( "No PKG-INFO in archive or " f"PKG-INFO missing 'Metadata-Version': {self.filename}" ) class ZipSDist(SDist): def read(self) -> bytes: with zipfile.ZipFile(self.filename) as sdist: # The sdist must contain a single top-level directory... root = os.path.commonpath(sdist.namelist()) if root in {".", "/", ""}: raise exceptions.InvalidDistribution( f"Too many top-level members in sdist archive: {self.filename}" ) # ...containing the package metadata in a ``PKG-INFO`` file. with suppress(KeyError): data = sdist.read(root.rstrip("/") + "/PKG-INFO") if b"Metadata-Version" in data: return data raise exceptions.InvalidDistribution( "No PKG-INFO in archive or " f"PKG-INFO missing 'Metadata-Version': {self.filename}" ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167907.0 twine-7.0.0/twine/settings.py0000644000175100017510000003152115231700043015647 0ustar00runnerrunner"""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, *, attestations: bool = False, 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 attestations: Whether the package file should be uploaded with attestations. :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.attestations = attestations 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( "--attestations", action="store_true", default=False, help="Upload each file's associated attestations.", ) 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. Has no effect on PyPI or TestPyPI. " "(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 verify_feature_capability(self) -> None: """Verify configured settings are supported for the configured repository. This presently checks: - ``--skip-existing`` was only provided for PyPI and TestPyPI :raises twine.exceptions.UnsupportedConfiguration: The configured features are not available with the configured repository. """ repository_url = cast(str, self.repository_config["repository"]) if self.skip_existing and not repository_url.startswith( (repository.WAREHOUSE, repository.TEST_WAREHOUSE) ): raise exceptions.UnsupportedConfiguration.Builder().with_feature( "--skip-existing" ).with_repository_url(repository_url).finalize() 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=1785167907.0 twine-7.0.0/twine/utils.py0000644000175100017510000003361515231700043015155 0ustar00runnerrunner# 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 import urllib3 from requests.adapters import HTTPAdapter from requests_toolbelt.utils import user_agent import twine 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 _parse_file(path: str, **open_kwargs: Any) -> configparser.RawConfigParser: """Open and parse a configuration file. This helper performs a single open/read operation so that if a UnicodeDecodeError is raised it happens before the parser has been partially populated. """ parser = configparser.RawConfigParser() with open(path, **open_kwargs) as f: parser.read_file(f) return parser def _parse_config(path: str) -> configparser.RawConfigParser: """Parse a config file with a UTF-8 fallback on decode errors. Try to parse using the default system encoding first; if a UnicodeDecodeError occurs, retry using UTF-8 and log that a fallback was used. """ logger.info("Using configuration from %s", path) try: parser = _parse_file(path) except UnicodeDecodeError: logger.info( "Configuration file not readable with default locale encoding, trying UTF-8" ) else: return parser try: parser = _parse_file(path, encoding="utf-8") except UnicodeDecodeError as ude: raise exceptions.UnableToReadConfigurationFile( f"Unable to read configuration file: {path}" ) from ude else: return parser 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 pypi and testpypi. """ realpath = os.path.realpath(os.path.expanduser(path)) try: parser = _parse_config(realpath) except FileNotFoundError: # User probably set --config-file, but the file can't be read if path != DEFAULT_CONFIG_FILE: raise # Create empty parser for missing default config file parser = configparser.RawConfigParser() # 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 sanitize_url(url: str) -> str: """Sanitize a URL. Sanitize URLs, removing any user:password combinations and replacing them with asterisks. Returns the original URL if the string is a non-matching pattern. :param url: str containing a URL to sanitize. return: str either sanitized or as entered depending on pattern match. """ uri = rfc3986.urlparse(url) if uri.userinfo: return cast(str, uri.copy_with(userinfo="*" * 8).unsplit()) return url 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 _config_from_repository_url(repository_url) 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/ " ) except configparser.Error: # NOTE: We intentionally fully mask the configparser exception here, # since it could leak tokens and other sensitive values. raise exceptions.InvalidConfiguration( f"Malformed configuration in {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 _config_from_repository_url(url: str) -> RepositoryConfig: parsed = urlparse(url) config = {"repository": url, "username": None, "password": None} if parsed.username: config["username"] = parsed.username config["password"] = parsed.password config["repository"] = cast( str, rfc3986.urlparse(url).copy_with(userinfo=None).unsplit() ) config["repository"] = normalize_repository_url(cast(str, config["repository"])) return config 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") def make_requests_session() -> requests.Session: """Prepare a requests Session with retries & twine's user-agent string.""" s = requests.Session() retry = urllib3.Retry( allowed_methods=["GET"], connect=5, total=10, status_forcelist=[500, 501, 502, 503], ) for scheme in ("http://", "https://"): s.mount(scheme, HTTPAdapter(max_retries=retry)) s.headers["User-Agent"] = ( user_agent.UserAgentBuilder("twine", twine.__version__) .include_implementation() .build() ) return s 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=1785167907.0 twine-7.0.0/twine/wheel.py0000644000175100017510000000535215231700043015116 0ustar00runnerrunner# 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 import re import zipfile from typing import List from twine import distribution from twine import exceptions 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) -> None: if not os.path.exists(filename): raise exceptions.InvalidDistribution(f"No such file: {filename}") self.filename = filename @property def py_version(self) -> str: wheel_info = wheel_file_re.match(os.path.basename(self.filename)) 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 ) searched_files: List[str] = [] 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 searched_files.append(candidate) finally: archive.close() raise exceptions.InvalidDistribution( "No METADATA in archive or METADATA missing 'Metadata-Version': " "%s (searched %s)" % (fqn, ",".join(searched_files)) ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1785167912.502956 twine-7.0.0/twine.egg-info/0000755000175100017510000000000015231700051015124 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167912.0 twine-7.0.0/twine.egg-info/PKG-INFO0000644000175100017510000000673615231700050016234 0ustar00runnerrunnerMetadata-Version: 2.4 Name: twine Version: 7.0.0 Summary: Collection of utilities for publishing packages on PyPI Author-email: Donald Stufft and individual contributors License-Expression: Apache-2.0 Project-URL: Homepage, https://twine.readthedocs.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: 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.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE 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: keyring>=21.2.0; platform_machine != "ppc64le" and platform_machine != "s390x" Requires-Dist: rfc3986>=1.4.0 Requires-Dist: rich>=14.3.3 Requires-Dist: packaging>=26.1 Requires-Dist: id Provides-Extra: keyring Requires-Dist: keyring>=21.2.0; extra == "keyring" Dynamic: license-file .. |twine-version| image:: https://img.shields.io/pypi/v/twine.svg :target: https://pypi.org/project/twine .. |python-versions| image:: https://img.shields.io/pypi/pyversions/twine.svg :target: https://pypi.org/project/twine .. |docs-badge| image:: https://img.shields.io/readthedocs/twine :target: https://twine.readthedocs.io .. |build-badge| image:: https://img.shields.io/github/actions/workflow/status/pypa/twine/main.yml?branch=main :target: https://github.com/pypa/twine/actions |twine-version| |python-versions| |docs-badge| |build-badge| 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=1785167912.0 twine-7.0.0/twine.egg-info/SOURCES.txt0000644000175100017510000000436615231700050017020 0ustar00runnerrunner.coveragerc .flake8 .git-blame-ignore-revs .gitignore .isort.cfg .readthedocs.yaml AUTHORS LICENSE README.rst mypy.ini pyproject.toml pytest.ini 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 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_parse_config_encoding.py tests/test_register.py tests/test_repository.py tests/test_sdist.py tests/test_settings.py tests/test_upload.py tests/test_utils.py tests/test_wheel.py tests/fixtures/deprecated-pypirc tests/fixtures/everything.metadata23 tests/fixtures/everything.metadata24 tests/fixtures/malformed.tar.gz tests/fixtures/twine-1.5.0.tar.gz tests/fixtures/twine-1.6.5.tar.gz tests/fixtures/twine-4.0.2-py3-none-any.whl tests/fixtures/twine-4.0.2-py3-none-any.whl.asc tests/fixtures/twine-6.2.0-py3-none-any.whl twine/__init__.py twine/__main__.py twine/auth.py twine/cli.py twine/distribution.py twine/exceptions.py twine/package.py twine/py.typed twine/repository.py twine/sdist.py twine/settings.py twine/utils.py twine/wheel.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/scm_file_list.json twine.egg-info/scm_version.json 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=1785167912.0 twine-7.0.0/twine.egg-info/dependency_links.txt0000644000175100017510000000000115231700050021171 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167912.0 twine-7.0.0/twine.egg-info/entry_points.txt0000644000175100017510000000027115231700050020421 0ustar00runnerrunner[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=1785167912.0 twine-7.0.0/twine.egg-info/requires.txt0000644000175100017510000000036215231700050017524 0ustar00runnerrunnerreadme-renderer>=35.0 requests>=2.20 requests-toolbelt!=0.9.0,>=0.8.0 urllib3>=1.26.0 rfc3986>=1.4.0 rich>=14.3.3 packaging>=26.1 id [:platform_machine != "ppc64le" and platform_machine != "s390x"] keyring>=21.2.0 [keyring] keyring>=21.2.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167912.0 twine-7.0.0/twine.egg-info/scm_file_list.json0000644000175100017510000000515415231700050020637 0ustar00runnerrunner{ "files": [ "tox.ini", ".git-blame-ignore-revs", "README.rst", "pyproject.toml", ".readthedocs.yaml", "LICENSE", "AUTHORS", "mypy.ini", "pytest.ini", ".flake8", ".gitignore", ".coveragerc", ".isort.cfg", ".github/dependabot.yml", ".github/ISSUE_TEMPLATE/04_other.yml", ".github/ISSUE_TEMPLATE/config.yml", ".github/ISSUE_TEMPLATE/02_bug.yml", ".github/ISSUE_TEMPLATE/03_feature.yml", ".github/ISSUE_TEMPLATE/01_upload_failed.yml", ".github/workflows/main.yml", ".github/workflows/codeql-analysis.yml", ".github/workflows/release.yml", "tests/helpers.py", "tests/test_auth.py", "tests/test_upload.py", "tests/test_settings.py", "tests/test_package.py", "tests/__init__.py", "tests/test_integration.py", "tests/test_wheel.py", "tests/test_register.py", "tests/test_commands.py", "tests/test_main.py", "tests/test_sdist.py", "tests/test_parse_config_encoding.py", "tests/test_cli.py", "tests/test_check.py", "tests/conftest.py", "tests/test_utils.py", "tests/test_repository.py", "tests/fixtures/everything.metadata23", "tests/fixtures/twine-1.5.0.tar.gz", "tests/fixtures/malformed.tar.gz", "tests/fixtures/twine-4.0.2-py3-none-any.whl", "tests/fixtures/twine-1.6.5.tar.gz", "tests/fixtures/deprecated-pypirc", "tests/fixtures/twine-4.0.2-py3-none-any.whl.asc", "tests/fixtures/twine-6.2.0-py3-none-any.whl", "tests/fixtures/everything.metadata24", "twine/wheel.py", "twine/settings.py", "twine/sdist.py", "twine/py.typed", "twine/auth.py", "twine/__init__.py", "twine/package.py", "twine/cli.py", "twine/exceptions.py", "twine/distribution.py", "twine/__main__.py", "twine/repository.py", "twine/utils.py", "twine/commands/__init__.py", "twine/commands/check.py", "twine/commands/register.py", "twine/commands/upload.py", "docs/requirements.txt", "docs/index.rst", "docs/contributing.rst", "docs/conf.py", "docs/changelog.rst", "docs/internal/twine.wheel.rst", "docs/internal/twine.rst", "docs/internal/twine.commands.check.rst", "docs/internal/twine.cli.rst", "docs/internal/twine.commands.register.rst", "docs/internal/twine.exceptions.rst", "docs/internal/twine.settings.rst", "docs/internal/twine.commands.upload.rst", "docs/internal/twine.commands.rst", "docs/internal/twine.repository.rst", "docs/internal/twine.utils.rst", "docs/internal/twine.package.rst", "docs/internal/twine.auth.rst", "changelog/.gitignore" ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167912.0 twine-7.0.0/twine.egg-info/scm_version.json0000644000175100017510000000024015231700050020341 0ustar00runnerrunner{ "tag": "7.0.0", "distance": 0, "node": "gfdb86cb45de11faeece9b274be6da7d275cdd4f9", "dirty": false, "branch": "HEAD", "node_date": "2026-07-27" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1785167912.0 twine-7.0.0/twine.egg-info/top_level.txt0000644000175100017510000000000615231700050017651 0ustar00runnerrunnertwine