Source code for semantic_release.repository
"""Helper for using Twine to upload to an artifact repository.
"""
import logging
import os
from dataclasses import InitVar
from dataclasses import asdict as dataclass_asdict
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional
import requests
from twine.commands.upload import upload as twine_upload
from twine.exceptions import TwineException
from twine.settings import Settings as TwineSettings
from semantic_release import ImproperConfigurationError
from semantic_release.helpers import LoggedFunction
from semantic_release.settings import config
logger = logging.getLogger(__name__)
[docs]def get_env_var(name: str) -> Optional[str]:
"""
Resolve variable name from config and return matching environment variable
:param name: Variable name to retrieve from environment
:returns Value of environment variable or None if not set.
"""
return os.environ.get(config.get(name))
[docs]@dataclass(eq=False)
class ArtifactRepo:
"""
Object that manages the configuration and execution of upload using Twine.
This object needs only one shared argument to be instantiated.
"""
dist_path: InitVar[Path]
repository_name: str = "pypi"
repository_url: Optional[str] = None
username: Optional[str] = field(repr=False, default=None)
password: Optional[str] = field(repr=False, default=None)
dists: List[str] = field(init=False, default_factory=list)
def __post_init__(self, dist_path: Path) -> None:
"""
:param dist_path: Path to dist folder containing the files to upload.
"""
self._handle_credentials_init()
self._handle_repository_config()
self._handle_glob_patterns(dist_path)
@LoggedFunction(logger)
def _handle_credentials_init(self) -> None:
"""
Initialize credentials from environment variables.
For the transitional period until the *pypi* variables can be safely removed,
additional complexity is needed.
:raises ImproperConfigurationError:
Error while setting up credentials configuration.
"""
username = get_env_var("repository_user_var") or get_env_var("pypi_user_var")
password = (
get_env_var("repository_pass_var")
or get_env_var("pypi_pass_var")
or get_env_var("pypi_token_var")
)
if username and password:
self.username = username
self.password = password
elif password and not username:
self.username = username or "__token__"
self.password = password
logger.warning(
"Providing only password or token without username is deprecated"
)
# neither username nor password provided, check for ~/.pypirc file
elif not Path("~/.pypirc").expanduser().exists():
raise ImproperConfigurationError(
"Missing credentials for uploading to artifact repository"
)
@LoggedFunction(logger)
def _handle_glob_patterns(self, dist_path: Path) -> None:
"""
Load glob patterns that select the distribution files to publish.
:param dist_path: Path to folder with package files
"""
glob_patterns = config.get("dist_glob_patterns") or config.get(
"upload_to_pypi_glob_patterns"
)
glob_patterns = (glob_patterns or "*").split(",")
self.dists = [str(dist_path.joinpath(pattern)) for pattern in glob_patterns]
@LoggedFunction(logger)
def _handle_repository_config(self) -> None:
"""
Initialize repository settings from config.
*repository_url* overrides *repository*, Twine handles this the same way.
Defaults to repository_name `pypi` when both are not set.
"""
repository_url = get_env_var("repository_url_var") or config.get(
"repository_url"
)
repository_name = config.get("repository")
if repository_url:
self.repository_url = repository_url
elif repository_name:
self.repository_name = repository_name
@LoggedFunction(logger)
def _create_twine_settings(self, addon_kwargs: Dict[str, Any]) -> TwineSettings:
"""
Gather all parameters that had a value set during instantiation and
pass them to Twine which then validates and loads the config.
"""
params = {name: val for name, val in dataclass_asdict(self).items() if val}
settings = TwineSettings(**params, **addon_kwargs)
return settings
[docs] @LoggedFunction(logger)
def upload(
self, noop: bool, verbose: bool, skip_existing: bool, **additional_kwargs
) -> bool:
"""
Upload artifact to repository using Twine.
For known repositories (like PyPI), the web URLs of successfully uploaded packages
will be displayed.
:param noop: Do not apply any changes..
:param verbose: Show verbose output for Twine.
:param skip_existing: Continue uploading files if one already exists.
(May not work, check your repository for support.)
:raises ImproperConfigurationError:
The upload failed due to a configuration error.
:returns True if successful, False otherwise.
"""
addon_kwargs = {
"non_interactive": True,
"verbose": verbose,
"skip_existing": skip_existing,
**additional_kwargs,
}
try:
twine_settings = self._create_twine_settings(addon_kwargs)
if not noop:
twine_upload(upload_settings=twine_settings, dists=self.dists)
except TwineException as e:
raise ImproperConfigurationError(
"Upload to artifact repository has failed"
) from e
except requests.HTTPError as e:
logger.warning(f"Upload to artifact repository has failed: {e}")
return False
else:
return True
[docs] @staticmethod
def upload_enabled() -> bool:
"""
Check if artifact repository upload is enabled
:returns True if upload is enabled, False otherwise.
"""
return config.get("upload_to_repository") and config.get("upload_to_pypi")