/
repository.py
181 lines (149 loc) · 6.2 KB
/
repository.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
"""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__)
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))
@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
@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
@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")