initial commit

This commit is contained in:
Davidson Gomes
2024-10-30 11:19:09 -03:00
commit 8654a31a4d
3744 changed files with 585542 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
"""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)

View File

@@ -0,0 +1,194 @@
"""Module containing the logic for ``twine check``."""
# Copyright 2018 Dustin Ingram
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import email.message
import io
import logging
import re
from typing import Dict, List, Optional, Tuple, cast
import readme_renderer.rst
from rich import print
from twine import commands
from twine import package as package_file
logger = logging.getLogger(__name__)
_RENDERERS = {
None: readme_renderer.rst, # Default if description_content_type is None
"text/plain": None, # Rendering cannot fail
"text/x-rst": readme_renderer.rst,
"text/markdown": None, # Rendering cannot fail
}
# Regular expression used to capture and reformat docutils warnings into
# something that a human can understand. This is loosely borrowed from
# Sphinx: https://github.com/sphinx-doc/sphinx/blob
# /c35eb6fade7a3b4a6de4183d1dd4196f04a5edaf/sphinx/util/docutils.py#L199
_REPORT_RE = re.compile(
r"^<string>:(?P<line>(?:\d+)?): "
r"\((?P<level>DEBUG|INFO|WARNING|ERROR|SEVERE)/(\d+)?\) "
r"(?P<message>.*)",
re.DOTALL | re.MULTILINE,
)
class _WarningStream(io.StringIO):
def write(self, text: str) -> int:
matched = _REPORT_RE.search(text)
if matched:
line = matched.group("line")
level_text = matched.group("level").capitalize()
message = matched.group("message").rstrip("\r\n")
text = f"line {line}: {level_text}: {message}\n"
return super().write(text)
def __str__(self) -> str:
return self.getvalue().strip()
def _parse_content_type(value: str) -> Tuple[str, Dict[str, str]]:
"""Implement logic of deprecated cgi.parse_header().
From https://docs.python.org/3.11/library/cgi.html#cgi.parse_header.
"""
msg = email.message.EmailMessage()
msg["content-type"] = value
return msg.get_content_type(), msg["content-type"].params
def _check_file(
filename: str, render_warning_stream: _WarningStream
) -> Tuple[List[str], bool]:
"""Check given distribution."""
warnings = []
is_ok = True
package = package_file.PackageFile.from_filename(filename, comment=None)
metadata = package.metadata_dictionary()
description = cast(Optional[str], metadata["description"])
description_content_type = cast(Optional[str], metadata["description_content_type"])
if description_content_type is None:
warnings.append(
"`long_description_content_type` missing. defaulting to `text/x-rst`."
)
description_content_type = "text/x-rst"
content_type, params = _parse_content_type(description_content_type)
renderer = _RENDERERS.get(content_type, _RENDERERS[None])
if description is None or description.rstrip() == "UNKNOWN":
warnings.append("`long_description` missing.")
elif renderer:
rendering_result = renderer.render(
description, stream=render_warning_stream, **params
)
if rendering_result is None:
is_ok = False
return warnings, is_ok
def check(
dists: List[str],
strict: bool = False,
) -> bool:
"""Check that a distribution will render correctly on PyPI and display the results.
This is currently only validates ``long_description``, but more checks could be
added; see https://github.com/pypa/twine/projects/2.
:param dists:
The distribution files to check.
:param output_stream:
The destination of the resulting output.
:param strict:
If ``True``, treat warnings as errors.
:return:
``True`` if there are rendering errors, otherwise ``False``.
"""
uploads = [i for i in commands._find_dists(dists) if not i.endswith(".asc")]
if not uploads: # Return early, if there are no files to check.
logger.error("No files to check.")
return False
failure = False
for filename in uploads:
print(f"Checking {filename}: ", end="")
render_warning_stream = _WarningStream()
warnings, is_ok = _check_file(filename, render_warning_stream)
# Print the status and/or error
if not is_ok:
failure = True
print("[red]FAILED[/red]")
logger.error(
"`long_description` has syntax errors in markup"
" and would not be rendered on PyPI."
f"\n{render_warning_stream}"
)
elif warnings:
if strict:
failure = True
print("[red]FAILED due to warnings[/red]")
else:
print("[yellow]PASSED with warnings[/yellow]")
else:
print("[green]PASSED[/green]")
# Print warnings after the status and/or error
for message in warnings:
logger.warning(message)
return failure
def main(args: List[str]) -> bool:
"""Execute the ``check`` command.
:param args:
The command-line arguments.
:return:
The exit status of the ``check`` command.
"""
parser = argparse.ArgumentParser(prog="twine check")
parser.add_argument(
"dists",
nargs="+",
metavar="dist",
help="The distribution files to check, usually dist/*",
)
parser.add_argument(
"--strict",
action="store_true",
default=False,
required=False,
help="Fail on warnings",
)
parsed_args = parser.parse_args(args)
# Call the check function with the arguments from the command line
return check(parsed_args.dists, strict=parsed_args.strict)

View File

@@ -0,0 +1,87 @@
"""Module containing the logic for ``twine register``."""
# Copyright 2015 Ian Cordasco
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import os.path
from typing import List, cast
from rich import print
from twine import exceptions
from twine import package as package_file
from twine import settings
def register(register_settings: settings.Settings, package: str) -> None:
"""Pre-register a package name with a repository before uploading a distribution.
Pre-registration is not supported on PyPI, so the ``register`` command is only
necessary if you are using a different repository that requires it.
:param register_settings:
The configured options relating to repository registration.
:param package:
The path of the distribution to use for package metadata.
:raises twine.exceptions.TwineException:
The registration failed due to a configuration error.
:raises requests.HTTPError:
The repository responded with an error.
"""
repository_url = cast(str, register_settings.repository_config["repository"])
print(f"Registering package to {repository_url}")
repository = register_settings.create_repository()
if not os.path.exists(package):
raise exceptions.PackageNotFound(
f'"{package}" does not exist on the file system.'
)
resp = repository.register(
package_file.PackageFile.from_filename(package, register_settings.comment)
)
repository.close()
if resp.is_redirect:
raise exceptions.RedirectDetected.from_args(
repository_url,
resp.headers["location"],
)
resp.raise_for_status()
def main(args: List[str]) -> None:
"""Execute the ``register`` command.
:param args:
The command-line arguments.
"""
parser = argparse.ArgumentParser(
prog="twine register",
description="register operation is not required with PyPI.org",
)
settings.Settings.register_argparse_arguments(parser)
parser.add_argument(
"package",
metavar="package",
help="File from which we read the package metadata.",
)
parsed_args = parser.parse_args(args)
register_settings = settings.Settings.from_argparse(parsed_args)
# Call the register function with the args from the command line
register(register_settings, parsed_args.package)

View File

@@ -0,0 +1,298 @@
"""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 fnmatch
import logging
import os.path
from typing import Dict, List, NamedTuple, cast
import requests
from rich import print
from twine import commands
from twine import exceptions
from twine import package as package_file
from twine import settings
from twine import utils
logger = logging.getLogger(__name__)
def skip_upload(
response: requests.Response, skip_existing: bool, package: package_file.PackageFile
) -> bool:
"""Determine if a failed upload is an error or can be safely ignored.
:param response:
The response from attempting to upload ``package`` to a repository.
:param skip_existing:
If ``True``, use the status and content of ``response`` to determine if the
package already exists on the repository. If so, then a failed upload is safe
to ignore.
:param package:
The package that was being uploaded.
:return:
``True`` if a failed upload can be safely ignored, otherwise ``False``.
"""
if not skip_existing:
return False
status = response.status_code
reason = getattr(response, "reason", "").lower()
text = getattr(response, "text", "").lower()
# NOTE(sigmavirus24): PyPI presently returns a 400 status code with the
# error message in the reason attribute. Other implementations return a
# 403 or 409 status code.
return (
# pypiserver (https://pypi.org/project/pypiserver)
status == 409
# PyPI / TestPyPI / GCP Artifact Registry
or (status == 400 and any("already exist" in x for x in [reason, text]))
# Nexus Repository OSS (https://www.sonatype.com/nexus-repository-oss)
or (status == 400 and any("updating asset" in x for x in [reason, text]))
# Artifactory (https://jfrog.com/artifactory/)
or (status == 403 and "overwrite artifact" in text)
# Gitlab Enterprise Edition (https://about.gitlab.com)
or (status == 400 and "already been taken" in text)
)
def _make_package(
filename: str,
signatures: Dict[str, str],
attestations: List[str],
upload_settings: settings.Settings,
) -> package_file.PackageFile:
"""Create and sign a package, based off of filename, signatures, and settings.
Additionally, any supplied attestations are attached to the package when
the settings indicate to do so.
"""
package = package_file.PackageFile.from_filename(filename, upload_settings.comment)
signed_name = package.signed_basefilename
if signed_name in signatures:
package.add_gpg_signature(signatures[signed_name], signed_name)
elif upload_settings.sign:
package.sign(upload_settings.sign_with, upload_settings.identity)
# Attestations are only attached if explicitly requested with `--attestations`.
if upload_settings.attestations:
# Passing `--attestations` without any actual attestations present
# indicates user confusion, so we fail rather than silently allowing it.
if not attestations:
raise exceptions.InvalidDistribution(
"Upload with attestations requested, but "
f"{filename} has no associated attestations"
)
package.add_attestations(attestations)
file_size = utils.get_file_size(package.filename)
logger.info(f"{package.filename} ({file_size})")
if package.gpg_signature:
logger.info(f"Signed with {package.signed_filename}")
return package
class Inputs(NamedTuple):
"""Represents structured user inputs."""
dists: List[str]
signatures: Dict[str, str]
attestations_by_dist: Dict[str, List[str]]
def _split_inputs(
inputs: List[str],
) -> Inputs:
"""
Split the unstructured list of input files provided by the user into groups.
Three groups are returned: upload files (i.e. dists), signatures, and attestations.
Upload files are returned as a linear list, signatures are returned as a
dict of ``basename -> path``, and attestations are returned as a dict of
``dist-path -> [attestation-path]``.
"""
signatures = {os.path.basename(i): i for i in fnmatch.filter(inputs, "*.asc")}
attestations = fnmatch.filter(inputs, "*.*.attestation")
dists = [
dist
for dist in inputs
if dist not in (set(signatures.values()) | set(attestations))
]
attestations_by_dist = {}
for dist in dists:
dist_basename = os.path.basename(dist)
attestations_by_dist[dist] = [
a for a in attestations if os.path.basename(a).startswith(dist_basename)
]
return Inputs(dists, signatures, attestations_by_dist)
def upload(upload_settings: settings.Settings, dists: List[str]) -> None:
"""Upload one or more distributions to a repository, and display the progress.
If a package already exists on the repository, most repositories will return an
error response. However, if ``upload_settings.skip_existing`` is ``True``, a message
will be displayed and any remaining distributions will be uploaded.
For known repositories (like PyPI), the web URLs of successfully uploaded packages
will be displayed.
:param upload_settings:
The configured options related to uploading to a repository.
:param dists:
The distribution files to upload to the repository. This can also include
``.asc`` and ``.attestation`` files, which will be added to their respective
file uploads.
:raises twine.exceptions.TwineException:
The upload failed due to a configuration error.
:raises requests.HTTPError:
The repository responded with an error.
"""
upload_settings.check_repository_url()
repository_url = cast(str, upload_settings.repository_config["repository"])
# Attestations are only supported on PyPI and TestPyPI at the moment.
# We warn instead of failing to allow twine to be used in local testing
# setups (where the PyPI deployment doesn't have a well-known domain).
if upload_settings.attestations and not repository_url.startswith(
(utils.DEFAULT_REPOSITORY, utils.TEST_REPOSITORY)
):
logger.warning(
"Only PyPI and TestPyPI support attestations; "
"if you experience failures, remove the --attestations flag and "
"re-try this command"
)
dists = commands._find_dists(dists)
# Determine if the user has passed in pre-signed distributions or any attestations.
uploads, signatures, attestations_by_dist = _split_inputs(dists)
print(f"Uploading distributions to {utils.sanitize_url(repository_url)}")
packages_to_upload = [
_make_package(
filename, signatures, attestations_by_dist[filename], upload_settings
)
for filename in uploads
]
if any(p.gpg_signature for p in packages_to_upload):
if repository_url.startswith((utils.DEFAULT_REPOSITORY, utils.TEST_REPOSITORY)):
# Warn the user if they're trying to upload a PGP signature to PyPI
# or TestPyPI, which will (as of May 2023) ignore it.
# This warning is currently limited to just those indices, since other
# indices may still support PGP signatures.
logger.warning(
"One or more packages has an associated PGP signature; "
"these will be silently ignored by the index"
)
else:
# On other indices, warn the user that twine is considering
# removing PGP support outright.
logger.warning(
"One or more packages has an associated PGP signature; "
"a future version of twine may silently ignore these. "
"See https://github.com/pypa/twine/issues/1009 for more "
"information"
)
repository = upload_settings.create_repository()
uploaded_packages = []
if signatures and not packages_to_upload:
raise exceptions.InvalidDistribution(
"Cannot upload signed files by themselves, must upload with a "
"corresponding distribution file."
)
for package in packages_to_upload:
skip_message = (
f"Skipping {package.basefilename} because it appears to already exist"
)
# Note: The skip_existing check *needs* to be first, because otherwise
# we're going to generate extra HTTP requests against a hardcoded
# URL for no reason.
if upload_settings.skip_existing and repository.package_is_uploaded(package):
logger.warning(skip_message)
continue
resp = repository.upload(package)
logger.info(f"Response from {resp.url}:\n{resp.status_code} {resp.reason}")
if resp.text:
logger.info(resp.text)
# Bug 92. If we get a redirect we should abort because something seems
# funky. The behaviour is not well defined and redirects being issued
# by PyPI should never happen in reality. This should catch malicious
# redirects as well.
if resp.is_redirect:
raise exceptions.RedirectDetected.from_args(
utils.sanitize_url(repository_url),
utils.sanitize_url(resp.headers["location"]),
)
if skip_upload(resp, upload_settings.skip_existing, package):
logger.warning(skip_message)
continue
utils.check_status_code(resp, upload_settings.verbose)
uploaded_packages.append(package)
release_urls = repository.release_urls(uploaded_packages)
if release_urls:
print("\n[green]View at:")
for url in release_urls:
print(url)
# Bug 28. Try to silence a ResourceWarning by clearing the connection
# pool.
repository.close()
def main(args: List[str]) -> None:
"""Execute the ``upload`` command.
:param args:
The command-line arguments.
"""
parser = argparse.ArgumentParser(prog="twine upload")
settings.Settings.register_argparse_arguments(parser)
parser.add_argument(
"dists",
nargs="+",
metavar="dist",
help="The distribution files to upload to the repository "
"(package index). Usually dist/* . May additionally contain "
"a .asc file to include an existing signature with the "
"file upload.",
)
parsed_args = parser.parse_args(args)
upload_settings = settings.Settings.from_argparse(parsed_args)
# Call the upload function with the arguments from the command line
return upload(upload_settings, parsed_args.dists)