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