wowool-build 0.0.1.dev9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wowool/build/codeartifact.py +195 -0
- wowool/build/exceptions.py +2 -0
- wowool/build/git.py +88 -0
- wowool/build/logging.py +156 -0
- wowool/build/platform.py +33 -0
- wowool/build/pydist.py +119 -0
- wowool/build/pypi.py +28 -0
- wowool/build/pytask.py +110 -0
- wowool/build/pytest.py +149 -0
- wowool/build/utility.py +37 -0
- wowool_build-0.0.1.dev9.dist-info/METADATA +24 -0
- wowool_build-0.0.1.dev9.dist-info/RECORD +14 -0
- wowool_build-0.0.1.dev9.dist-info/WHEEL +5 -0
- wowool_build-0.0.1.dev9.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from os import environ
|
|
2
|
+
from sys import stderr
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional, Union
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from subprocess import run
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from exceptions import UploadError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class AwsCredentials:
|
|
15
|
+
"""
|
|
16
|
+
Credentials to access AWS
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
domain: str
|
|
20
|
+
domain_owner: int
|
|
21
|
+
region: str
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def from_environment():
|
|
25
|
+
return AwsCredentials(
|
|
26
|
+
domain=environ["AWS_DOMAIN"],
|
|
27
|
+
domain_owner=int(environ["AWS_DOMAIN_OWNER"]),
|
|
28
|
+
region=environ["AWS_REGION"],
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class CodeArtifactCredentials:
|
|
34
|
+
"""
|
|
35
|
+
Credentials to access a CodeArtifact repository
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
repository: str
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def from_environment():
|
|
42
|
+
return CodeArtifactCredentials(repository=environ["AWS_CODEARTIFACT_REPOSITORY"])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _check_not_dirty(fn: Path):
|
|
46
|
+
if "dirty" in fn.name:
|
|
47
|
+
raise UploadError(f"Attempted to upload a dirty version: {fn}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def upload_pypi(fp: Path, expression: str = "*"):
|
|
51
|
+
"""
|
|
52
|
+
Upload a Python package to AWS CodeArtifact
|
|
53
|
+
"""
|
|
54
|
+
fp_dist = fp / "dist"
|
|
55
|
+
for fn in fp_dist.glob(expression):
|
|
56
|
+
_check_not_dirty(fn)
|
|
57
|
+
cmd = f"twine upload {fn} --repository codeartifact"
|
|
58
|
+
try:
|
|
59
|
+
run(cmd, shell=True, check=True, cwd=fp)
|
|
60
|
+
except Exception as error:
|
|
61
|
+
raise UploadError(f"AWS CodeArtifact Twine upload failed: {error}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _sha256sum(fn: Path):
|
|
65
|
+
with open(fn, "rb", buffering=0) as ifs:
|
|
66
|
+
return hashlib.file_digest(ifs, "sha256").hexdigest() # pyright: ignore
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def delete_raw(
|
|
70
|
+
package: str,
|
|
71
|
+
version: str,
|
|
72
|
+
namespace: str | None = None,
|
|
73
|
+
format: str = "generic",
|
|
74
|
+
aws: Optional[AwsCredentials] = None,
|
|
75
|
+
codeartifact: Optional[CodeArtifactCredentials] = None,
|
|
76
|
+
):
|
|
77
|
+
aws = aws or AwsCredentials.from_environment()
|
|
78
|
+
codeartifact = codeartifact or CodeArtifactCredentials.from_environment()
|
|
79
|
+
options = f"--namespace {namespace}" if namespace != None else ""
|
|
80
|
+
options += f" --format {format}"
|
|
81
|
+
cmd = f"""aws codeartifact delete-package-versions --domain {aws.domain} --domain-owner {aws.domain_owner} --region {aws.region} --repository {codeartifact.repository} {options} --package {package} --versions {version} """
|
|
82
|
+
result = run(cmd, shell=True, check=False, capture_output=True)
|
|
83
|
+
print(result.stdout.decode())
|
|
84
|
+
if result.returncode != 0:
|
|
85
|
+
print(result.stderr.decode(), file=stderr)
|
|
86
|
+
raise UploadError(f"AWS CodeArtifact generic upload failed {cmd=}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def upload_raw(
|
|
90
|
+
fn: Path,
|
|
91
|
+
namespace: str,
|
|
92
|
+
package: str,
|
|
93
|
+
version: str,
|
|
94
|
+
aws: Optional[AwsCredentials] = None,
|
|
95
|
+
codeartifact: Optional[CodeArtifactCredentials] = None,
|
|
96
|
+
overwrite=False,
|
|
97
|
+
):
|
|
98
|
+
fn = Path(fn)
|
|
99
|
+
aws = aws or AwsCredentials.from_environment()
|
|
100
|
+
codeartifact = codeartifact or CodeArtifactCredentials.from_environment()
|
|
101
|
+
|
|
102
|
+
if overwrite:
|
|
103
|
+
try:
|
|
104
|
+
delete_raw(namespace=namespace, package=package, version=version)
|
|
105
|
+
except Exception as error:
|
|
106
|
+
print(f"AWS CodeArtifact generic delete failed: {error}", file=stderr)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
asset_hash = _sha256sum(fn)
|
|
110
|
+
except Exception as error:
|
|
111
|
+
raise UploadError(f"Could not produce sha256 hash for {fn}: {error}")
|
|
112
|
+
try:
|
|
113
|
+
cmd = f"""aws codeartifact publish-package-version --domain {aws.domain} --domain-owner {aws.domain_owner} --region {aws.region} --repository {codeartifact.repository} --format generic --namespace {namespace} --package {package} --package-version {version} --asset-content {fn} --asset-name {fn.name} --asset-sha256 '{asset_hash}' --unfinished"""
|
|
114
|
+
result = run(cmd, shell=True, check=False, capture_output=True)
|
|
115
|
+
print(result.stdout.decode())
|
|
116
|
+
if result.returncode != 0:
|
|
117
|
+
print(result.stderr.decode(), file=stderr)
|
|
118
|
+
raise UploadError(f"AWS CodeArtifact generic upload failed {cmd=}")
|
|
119
|
+
except Exception as error:
|
|
120
|
+
raise UploadError(f"AWS CodeArtifact generic upload failed: {error}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def run_codeartifact(
|
|
124
|
+
cmd: str,
|
|
125
|
+
aws: Optional[AwsCredentials] = None,
|
|
126
|
+
codeartifact: Optional[CodeArtifactCredentials] = None,
|
|
127
|
+
):
|
|
128
|
+
aws = aws or AwsCredentials.from_environment()
|
|
129
|
+
codeartifact = codeartifact or CodeArtifactCredentials.from_environment()
|
|
130
|
+
try:
|
|
131
|
+
codeartifact_cmd = f"aws codeartifact {cmd}"
|
|
132
|
+
print(f"Running: {codeartifact_cmd}")
|
|
133
|
+
result = run(codeartifact_cmd, shell=True, check=False, capture_output=True)
|
|
134
|
+
|
|
135
|
+
if result.returncode != 0:
|
|
136
|
+
print(result.stderr.decode(), file=stderr)
|
|
137
|
+
raise UploadError(f"AWS CodeArtifact generic cmd failed {cmd=}")
|
|
138
|
+
else:
|
|
139
|
+
return result.stdout.decode()
|
|
140
|
+
except Exception as error:
|
|
141
|
+
raise UploadError(f"AWS CodeArtifact generic cmd failed: {error}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def download_raw(
|
|
145
|
+
output: Union[str, Path],
|
|
146
|
+
namespace: str,
|
|
147
|
+
package: str,
|
|
148
|
+
version: str,
|
|
149
|
+
aws: Optional[AwsCredentials] = None,
|
|
150
|
+
codeartifact: Optional[CodeArtifactCredentials] = None,
|
|
151
|
+
):
|
|
152
|
+
output = Path(output)
|
|
153
|
+
aws = aws or AwsCredentials.from_environment()
|
|
154
|
+
codeartifact = codeartifact or CodeArtifactCredentials.from_environment()
|
|
155
|
+
try:
|
|
156
|
+
cmd = f"""aws codeartifact get-package-version-asset --package-version {version} --domain {aws.domain} --domain-owner {aws.domain_owner} --region {aws.region} --repository {codeartifact.repository} --format generic --namespace {namespace} --package {package} --asset {output.name} {output}"""
|
|
157
|
+
result = run(cmd, shell=True, check=False, capture_output=True)
|
|
158
|
+
print(result.stdout.decode())
|
|
159
|
+
if result.returncode != 0:
|
|
160
|
+
print(result.stderr.decode(), file=stderr)
|
|
161
|
+
raise UploadError(f"AWS CodeArtifact generic upload failed {cmd=}")
|
|
162
|
+
except Exception as error:
|
|
163
|
+
raise UploadError(f"AWS CodeArtifact generic upload failed: {error}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def iter_packages(expression=".*", aws: Optional[AwsCredentials] = None, codeartifact: Optional[CodeArtifactCredentials] = None):
|
|
167
|
+
|
|
168
|
+
exp_ = re.compile(expression)
|
|
169
|
+
cmd = "list-packages --domain wowool --repository wowool --max-items 1000 "
|
|
170
|
+
data = run_codeartifact(cmd, aws, codeartifact)
|
|
171
|
+
jo = json.loads(data)
|
|
172
|
+
for package in jo["packages"]:
|
|
173
|
+
if exp_.search(package["package"]):
|
|
174
|
+
yield package["package"]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def list_package_info(
|
|
178
|
+
expression: str = ".*",
|
|
179
|
+
version_expression: str = ".*",
|
|
180
|
+
aws: Optional[AwsCredentials] = None,
|
|
181
|
+
codeartifact: Optional[CodeArtifactCredentials] = None,
|
|
182
|
+
):
|
|
183
|
+
|
|
184
|
+
version_exp_ = re.compile(version_expression)
|
|
185
|
+
for name in iter_packages(expression, aws, codeartifact):
|
|
186
|
+
try:
|
|
187
|
+
cmd = f"list-package-versions --domain wowool --repository wowool --format pypi --package {name}"
|
|
188
|
+
jo_version = json.loads(run_codeartifact(cmd))
|
|
189
|
+
# print(jo_version)
|
|
190
|
+
# yield jo_version['defaultDisplayVersion']
|
|
191
|
+
for version in jo_version["versions"]:
|
|
192
|
+
if version_exp_.match(version["version"]):
|
|
193
|
+
yield jo_version["namespace"], jo_version["package"], version["version"], jo_version["format"]
|
|
194
|
+
except UploadError as error:
|
|
195
|
+
print(f"Error: {error}", file=stderr)
|
wowool/build/git.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from os import getcwd
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from subprocess import run, CalledProcessError
|
|
5
|
+
from typing import Optional, Union
|
|
6
|
+
from os import environ
|
|
7
|
+
|
|
8
|
+
logger = getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_repository_path(fp_repo: Optional[Union[str, Path]] = None) -> Path:
|
|
12
|
+
if fp_repo is None:
|
|
13
|
+
return Path(getcwd())
|
|
14
|
+
else:
|
|
15
|
+
return Path(fp_repo).parent if Path(fp_repo).is_file() else Path(fp_repo)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def increment_version(version: str) -> str:
|
|
19
|
+
try:
|
|
20
|
+
parts = version.split(".")
|
|
21
|
+
parts[-1] = str(int(parts[-1]) + 1)
|
|
22
|
+
return ".".join(parts)
|
|
23
|
+
except ValueError:
|
|
24
|
+
return version
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def make_version(tag: str, nr_commits: int, has_changes: bool):
|
|
28
|
+
postfix = environ.get("WOWOOL_VERSION_POSTFIX", f".dev{nr_commits}+dirty")
|
|
29
|
+
if "dev0" in tag:
|
|
30
|
+
tag = tag.replace(".dev0", "")
|
|
31
|
+
version = f"{tag}.dev{nr_commits}" if has_changes == 0 else f"{tag}{postfix}"
|
|
32
|
+
return version
|
|
33
|
+
else:
|
|
34
|
+
if has_changes == 0:
|
|
35
|
+
if nr_commits == 0:
|
|
36
|
+
return tag
|
|
37
|
+
else:
|
|
38
|
+
tag = increment_version(tag)
|
|
39
|
+
if "WOWOOL_VERSION_POSTFIX" not in environ:
|
|
40
|
+
version = f"{tag}.dev{nr_commits}"
|
|
41
|
+
else:
|
|
42
|
+
version = f"{tag}{postfix}"
|
|
43
|
+
return version
|
|
44
|
+
else:
|
|
45
|
+
tag = increment_version(tag)
|
|
46
|
+
version = f"{tag}{postfix}"
|
|
47
|
+
return version
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_safe(cmd: str, capture_output: bool = True, cwd: Optional[Union[str, Path]] = None) -> str:
|
|
51
|
+
try:
|
|
52
|
+
res = run(cmd, shell=True, check=True, cwd=cwd, capture_output=capture_output)
|
|
53
|
+
return res.stdout.decode("utf-8").strip()
|
|
54
|
+
except CalledProcessError as ex:
|
|
55
|
+
logger.error(f"Error running command: {cmd}")
|
|
56
|
+
logger.error(ex)
|
|
57
|
+
print(ex.stderr.decode("utf-8"))
|
|
58
|
+
print(ex.stderr.decode("utf-8"))
|
|
59
|
+
raise ex
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_version_info(fp_repo: Optional[Union[str, Path]] = None) -> dict:
|
|
63
|
+
tag = run_safe("git describe --tags --abbrev=0", cwd=fp_repo)
|
|
64
|
+
nr_commits_result = run_safe(f"git log {tag}..HEAD --oneline", cwd=fp_repo)
|
|
65
|
+
nr_commits = len(nr_commits_result.splitlines())
|
|
66
|
+
has_changes = run("git diff --quiet --exit-code HEAD", shell=True).returncode != 0
|
|
67
|
+
return {"tag": tag, "nr_commits": nr_commits, "has_changes": has_changes}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_version(fp_repo: Optional[Union[str, Path]] = None) -> str:
|
|
71
|
+
"""
|
|
72
|
+
Get the version from the git history of the given repository folder
|
|
73
|
+
|
|
74
|
+
:param fp_repo: Optional repository folder. If not provided, the current
|
|
75
|
+
working directory is used
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
fp_repo = _get_repository_path(fp_repo)
|
|
79
|
+
|
|
80
|
+
_git_info = get_version_info(fp_repo)
|
|
81
|
+
return make_version(_git_info["tag"], _git_info["nr_commits"], _git_info["has_changes"])
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def git_hash(fp_repo: Optional[Union[str, Path]] = None) -> str:
|
|
85
|
+
fp_repo = _get_repository_path(fp_repo)
|
|
86
|
+
result = run("git rev-parse HEAD", capture_output=True, shell=True, check=True, cwd=fp_repo)
|
|
87
|
+
git_hash_rev = result.stdout.decode().strip()
|
|
88
|
+
return git_hash_rev
|
wowool/build/logging.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from logging import (
|
|
4
|
+
CRITICAL,
|
|
5
|
+
DEBUG,
|
|
6
|
+
ERROR,
|
|
7
|
+
Formatter as StandardFormatter,
|
|
8
|
+
INFO,
|
|
9
|
+
Logger,
|
|
10
|
+
StreamHandler,
|
|
11
|
+
WARNING,
|
|
12
|
+
basicConfig,
|
|
13
|
+
getLogger as _getLogger,
|
|
14
|
+
)
|
|
15
|
+
from os import getcwd
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from tomllib import TOMLDecodeError, loads
|
|
18
|
+
from typing import Any, Union
|
|
19
|
+
|
|
20
|
+
_pyproject_tool_id = "wowool_build_logging"
|
|
21
|
+
_default_loggers_names = ["root", "wowool"]
|
|
22
|
+
_pyproject_toml_filename = "pyproject.toml"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_fn_pyproject_toml_from_identifier(identifier: Union[str, Path, None] = None) -> Path:
|
|
26
|
+
if identifier is None:
|
|
27
|
+
identifier = getcwd()
|
|
28
|
+
if isinstance(identifier, str):
|
|
29
|
+
identifier = Path(identifier).parent.resolve()
|
|
30
|
+
fn = identifier if identifier.is_file() else identifier / _pyproject_toml_filename
|
|
31
|
+
if not fn.exists() or not fn.is_file():
|
|
32
|
+
raise LoggingConfigError(f"Expected {fn} to be an existing file")
|
|
33
|
+
return fn
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# TODO: Once supoport for Python<3.11 is dropped, the imports can be moved to global scope
|
|
37
|
+
def _parse_pyproject_toml(fn_pyproject_toml: Path) -> dict:
|
|
38
|
+
try:
|
|
39
|
+
return loads(fn_pyproject_toml.read_text())
|
|
40
|
+
except TOMLDecodeError as error:
|
|
41
|
+
raise LoggingConfigError(f"Configuration error in {fn_pyproject_toml}: {error}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LoggingConfigError(RuntimeError):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class LoggerConfig:
|
|
50
|
+
level: Union[str, int]
|
|
51
|
+
format: str
|
|
52
|
+
colored: bool
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def configure_logger(logger_name: str, config: LoggerConfig):
|
|
56
|
+
logger = _getLogger(logger_name)
|
|
57
|
+
logger.propagate = False
|
|
58
|
+
logger.handlers.clear()
|
|
59
|
+
logger.setLevel(config.level)
|
|
60
|
+
|
|
61
|
+
handler = StreamHandler()
|
|
62
|
+
Formatter = ColoredFormatter if config.colored else StandardFormatter
|
|
63
|
+
formatter = Formatter(config.format)
|
|
64
|
+
handler.setFormatter(formatter)
|
|
65
|
+
logger.addHandler(handler)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def default_config():
|
|
69
|
+
return LoggerConfig(
|
|
70
|
+
level="INFO",
|
|
71
|
+
format="%(levelname)-8s %(name)s: %(message)s",
|
|
72
|
+
colored=False,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def read_config(config_raw: dict) -> LoggerConfig:
|
|
77
|
+
if not isinstance(config_raw, dict):
|
|
78
|
+
raise LoggingConfigError(f"Incorrect logging configuration, most likely no logger was specified")
|
|
79
|
+
|
|
80
|
+
def get_parameter(key: str, default_value: Any) -> Any:
|
|
81
|
+
return config_raw[key] if key in config_raw else default_value
|
|
82
|
+
|
|
83
|
+
defaults = default_config()
|
|
84
|
+
return LoggerConfig(
|
|
85
|
+
level=get_parameter("level", defaults.level),
|
|
86
|
+
format=get_parameter("format", defaults.format),
|
|
87
|
+
colored=get_parameter("colored", defaults.colored),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ColoredFormatter(StandardFormatter):
|
|
92
|
+
def __init__(self, format: str):
|
|
93
|
+
grey = "\x1b[0;30m"
|
|
94
|
+
blue = "\x1b[0;34m"
|
|
95
|
+
red = "\x1b[31;20m"
|
|
96
|
+
red_bold = "\x1b[31;1m"
|
|
97
|
+
yellow = "\x1b[33;20m"
|
|
98
|
+
|
|
99
|
+
reset = "\x1b[0m"
|
|
100
|
+
formats = {
|
|
101
|
+
DEBUG: grey + format + reset,
|
|
102
|
+
INFO: blue + format + reset,
|
|
103
|
+
WARNING: yellow + format + reset,
|
|
104
|
+
ERROR: red + format + reset,
|
|
105
|
+
CRITICAL: red_bold + format + reset,
|
|
106
|
+
}
|
|
107
|
+
self.formatter = {level: StandardFormatter(config) for level, config in formats.items()}
|
|
108
|
+
|
|
109
|
+
def format(self, record):
|
|
110
|
+
return self.formatter[record.levelno].format(record)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def init_logging(configuration_path_identifier: Union[str, Path, None] = None, name: str = ""):
|
|
114
|
+
fn_pyproject_toml = _get_fn_pyproject_toml_from_identifier(configuration_path_identifier)
|
|
115
|
+
pyproject = _parse_pyproject_toml(fn_pyproject_toml)
|
|
116
|
+
if not "tool" in pyproject or not _pyproject_tool_id in pyproject["tool"]:
|
|
117
|
+
basicConfig(level="DEBUG")
|
|
118
|
+
else:
|
|
119
|
+
wowool_build_logging = pyproject["tool"][_pyproject_tool_id]
|
|
120
|
+
logger_names = list(wowool_build_logging.keys())
|
|
121
|
+
logger_names.extend(_default_loggers_names)
|
|
122
|
+
logger_names = set(logger_names)
|
|
123
|
+
for logger_name in logger_names:
|
|
124
|
+
config = read_config(wowool_build_logging[logger_name]) if logger_name in wowool_build_logging else default_config()
|
|
125
|
+
configure_logger(logger_name, config)
|
|
126
|
+
|
|
127
|
+
if name:
|
|
128
|
+
return _getLogger(name)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_logger(name: str, configuration_path_identifier: Union[str, Path, None] = None) -> Logger:
|
|
132
|
+
if configuration_path_identifier is not None:
|
|
133
|
+
init_logging(configuration_path_identifier)
|
|
134
|
+
return _getLogger(name)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def getLogger(*args, **kwargs):
|
|
138
|
+
return get_logger(*args, **kwargs)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@contextmanager
|
|
142
|
+
def set_logger_level(logger: Union[Logger, str], level): # pyright: ignore
|
|
143
|
+
if isinstance(logger, str):
|
|
144
|
+
logger = _getLogger(logger)
|
|
145
|
+
level_previous = logger.level
|
|
146
|
+
try:
|
|
147
|
+
logger.debug(f"Switching logger level to {level}")
|
|
148
|
+
logger.setLevel(level)
|
|
149
|
+
yield
|
|
150
|
+
finally:
|
|
151
|
+
logger.debug(f"Switching logger level to {level_previous}")
|
|
152
|
+
logger.setLevel(level_previous)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def silence_logger(logger: Union[Logger, str], level=100):
|
|
156
|
+
return set_logger_level(logger, level)
|
wowool/build/platform.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _get_system_name() -> Optional[str]:
|
|
7
|
+
platform_name = platform.system()
|
|
8
|
+
if "Linux" == platform_name:
|
|
9
|
+
return (
|
|
10
|
+
f"{platform.system()}-{platform.machine()}-{platform.libc_ver()[0]}"
|
|
11
|
+
)
|
|
12
|
+
elif "Darwin" == platform_name:
|
|
13
|
+
return f"{platform.system()}-{platform.machine()}"
|
|
14
|
+
elif "Windows" == platform_name:
|
|
15
|
+
return f"{platform.system()}-{platform.machine()}-{platform.win32_ver()[0]}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_os_name():
|
|
19
|
+
platforms = {
|
|
20
|
+
"linux1": "linux",
|
|
21
|
+
"linux2": "linux",
|
|
22
|
+
"darwin": "macos",
|
|
23
|
+
"win32": "windows",
|
|
24
|
+
}
|
|
25
|
+
return (
|
|
26
|
+
platforms[sys.platform] if sys.platform in platforms else sys.platform
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
SYSTEM_NAME = _get_system_name()
|
|
31
|
+
OS_NAME = _get_os_name()
|
|
32
|
+
IS_PTY = sys.stdout.isatty() if OS_NAME != "windows" else False
|
|
33
|
+
IS_POSIX = OS_NAME != "win32"
|
wowool/build/pydist.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from logging import getLogger, DEBUG
|
|
2
|
+
from os import chmod, getcwd, stat
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from shutil import rmtree
|
|
5
|
+
from stat import S_IEXEC
|
|
6
|
+
from subprocess import run
|
|
7
|
+
from sys import path as sys_path, prefix
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
logger = getLogger(__name__)
|
|
11
|
+
logger_cmd = getLogger("wowool.cmd")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def make_environment_entry_points(fp: Path):
|
|
15
|
+
"""
|
|
16
|
+
Create the console scripts from the entry points defined in setup.py
|
|
17
|
+
|
|
18
|
+
:param fp: Path to the folder holding setup.py
|
|
19
|
+
"""
|
|
20
|
+
egg_dirs = [fp for fp in fp.glob("*.egg-info")]
|
|
21
|
+
if not egg_dirs:
|
|
22
|
+
raise RuntimeError("Missing *.egg-info directories")
|
|
23
|
+
for egg_dir in egg_dirs:
|
|
24
|
+
entry_points = egg_dir / "entry_points.txt"
|
|
25
|
+
from configparser import ConfigParser
|
|
26
|
+
|
|
27
|
+
configur = ConfigParser()
|
|
28
|
+
configur.read(entry_points)
|
|
29
|
+
try:
|
|
30
|
+
console_scripts = configur["console_scripts"]
|
|
31
|
+
except KeyError:
|
|
32
|
+
raise RuntimeError("No console scripts defined in setup.py")
|
|
33
|
+
for cs in console_scripts:
|
|
34
|
+
fn_cs = Path(f"{prefix}/bin/{cs}")
|
|
35
|
+
print(f"Added entry point {cs} as alias to {fn_cs}")
|
|
36
|
+
(imp, func) = console_scripts[cs].split(":")
|
|
37
|
+
sys_path.append(f"{Path(__file__).parent.parent.resolve()}/core-build-py")
|
|
38
|
+
template = rf"""#!{prefix}/bin/python
|
|
39
|
+
import re
|
|
40
|
+
import sys
|
|
41
|
+
sys.path.append("{fp}")
|
|
42
|
+
for p in {sys_path}:
|
|
43
|
+
sys.path.append(p)
|
|
44
|
+
from {imp} import {func}
|
|
45
|
+
if __name__ == '__main__':
|
|
46
|
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
|
47
|
+
sys.exit({func}())
|
|
48
|
+
"""
|
|
49
|
+
with open(fn_cs, "w") as fh:
|
|
50
|
+
fh.write(template)
|
|
51
|
+
st = stat(fn_cs)
|
|
52
|
+
chmod(fn_cs, st.st_mode | S_IEXEC)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_install_requires(fp: Optional[Path] = None):
|
|
56
|
+
"""
|
|
57
|
+
Return the contents of the install_requires*.txt files in the given path
|
|
58
|
+
|
|
59
|
+
:param fp: Path containing the install_requires*.txt files. If not set, the
|
|
60
|
+
current working directory is used
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def is_valid(requirement: str) -> bool:
|
|
64
|
+
return len(requirement) != 0 and not requirement.startswith("#")
|
|
65
|
+
|
|
66
|
+
fp = fp if fp is not None else Path(getcwd())
|
|
67
|
+
install_requires = []
|
|
68
|
+
for fn in fp.glob("install_requires*.txt"):
|
|
69
|
+
requirements = filter(is_valid, [line.strip() for line in open(fn).readlines()])
|
|
70
|
+
install_requires.extend(requirements)
|
|
71
|
+
return install_requires
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def resolve_template(template, __globals, __locals):
|
|
75
|
+
return eval("f'''" + template + "'''", __globals, __locals)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def build_wheel_package(fp: Path, no_deps: bool = True, build_isolation: bool = False):
|
|
79
|
+
"""
|
|
80
|
+
Build a wheel package
|
|
81
|
+
|
|
82
|
+
:param fp: Folder holding the requirement files
|
|
83
|
+
"""
|
|
84
|
+
template_pyproject_fn = fp / "pyproject_template.toml"
|
|
85
|
+
if template_pyproject_fn.exists():
|
|
86
|
+
from wowool.build.git import get_version
|
|
87
|
+
|
|
88
|
+
pyproject_fn = fp / "pyproject.toml"
|
|
89
|
+
data = template_pyproject_fn.read_text()
|
|
90
|
+
version = get_version(fp)
|
|
91
|
+
dependencies = get_install_requires(fp)
|
|
92
|
+
package_name: str = fp.name
|
|
93
|
+
if package_name.startswith("comp-"):
|
|
94
|
+
package_name = package_name[5:]
|
|
95
|
+
if package_name.endswith("-py"):
|
|
96
|
+
package_name = package_name[:-3]
|
|
97
|
+
print(f"Package name is {package_name}")
|
|
98
|
+
resolve_data = resolve_template(data, globals(), locals())
|
|
99
|
+
pyproject_fn.write_text(resolve_data)
|
|
100
|
+
|
|
101
|
+
options = []
|
|
102
|
+
if not build_isolation:
|
|
103
|
+
options.append("--no-isolation")
|
|
104
|
+
# cmd = f"python -m pip wheel {' '.join(options)} -w dist ."
|
|
105
|
+
cmd = f"python -m build --wheel {' '.join(options)} ."
|
|
106
|
+
print(f"CMD: {cmd}")
|
|
107
|
+
run(cmd, shell=True, check=True, cwd=fp)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def clean_package_build(fp: Path):
|
|
111
|
+
"""
|
|
112
|
+
Clean the package build
|
|
113
|
+
"""
|
|
114
|
+
for dir in [
|
|
115
|
+
"build",
|
|
116
|
+
"dist",
|
|
117
|
+
"var",
|
|
118
|
+
] + list(fp.glob("*.egg-info")):
|
|
119
|
+
rmtree(fp / dir, ignore_errors=True)
|
wowool/build/pypi.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from os import environ
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from subprocess import run
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from wowool.build.exceptions import UploadError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _check_not_dirty(fn: Path):
|
|
10
|
+
if "dirty" in fn.name:
|
|
11
|
+
raise UploadError(f"Attempted to upload a dirty version: {fn}")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def upload_pypi(fp: Path, expression: str = "*", repository: str | None = None):
|
|
15
|
+
"""
|
|
16
|
+
Upload a Python package to pypi
|
|
17
|
+
"""
|
|
18
|
+
fp_dist = fp / "dist"
|
|
19
|
+
for fn in fp_dist.glob(expression):
|
|
20
|
+
_check_not_dirty(fn)
|
|
21
|
+
repository = environ.get("TWINE_REPOSITORY", None)
|
|
22
|
+
repository_option = f"--repository-url {repository}" if repository else ""
|
|
23
|
+
cmd = f"twine upload {repository_option} dist/*"
|
|
24
|
+
try:
|
|
25
|
+
print(f"cmd: {cmd}")
|
|
26
|
+
run(cmd, shell=True, check=True, cwd=fp)
|
|
27
|
+
except Exception as error:
|
|
28
|
+
raise UploadError(f"PyPi Twine upload failed: {error}")
|
wowool/build/pytask.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from os import getcwd
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
from invoke.collection import Collection
|
|
6
|
+
from invoke.tasks import task
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_py_tasks(fp_root: Union[str, Path, None] = None) -> Collection:
|
|
10
|
+
fp_root = Path(fp_root) if fp_root is not None else Path(getcwd())
|
|
11
|
+
if fp_root.is_file():
|
|
12
|
+
fp_root = fp_root.parent
|
|
13
|
+
|
|
14
|
+
@task
|
|
15
|
+
def version(_):
|
|
16
|
+
"""
|
|
17
|
+
Print the version from Git
|
|
18
|
+
"""
|
|
19
|
+
from wowool.build.git import get_version
|
|
20
|
+
|
|
21
|
+
print(get_version(fp_root))
|
|
22
|
+
|
|
23
|
+
@task
|
|
24
|
+
def test(
|
|
25
|
+
_,
|
|
26
|
+
verbosity: int = 2,
|
|
27
|
+
failfast: bool = True,
|
|
28
|
+
list: bool = False,
|
|
29
|
+
expression: str = "",
|
|
30
|
+
xclude: str = "",
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Run the Python unit-tests
|
|
34
|
+
"""
|
|
35
|
+
from wowool.build.pytest import list_unittest_tests, run_unittest_tests
|
|
36
|
+
|
|
37
|
+
(
|
|
38
|
+
list_unittest_tests(
|
|
39
|
+
fp_root,
|
|
40
|
+
pattern_include=expression,
|
|
41
|
+
pattern_exclude=xclude,
|
|
42
|
+
)
|
|
43
|
+
if list
|
|
44
|
+
else run_unittest_tests(
|
|
45
|
+
fp_root,
|
|
46
|
+
verbosity=verbosity,
|
|
47
|
+
failfast=failfast,
|
|
48
|
+
pattern_include=expression,
|
|
49
|
+
pattern_exclude=xclude,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@task
|
|
54
|
+
def clean(_):
|
|
55
|
+
"""
|
|
56
|
+
Clean the Python package build
|
|
57
|
+
"""
|
|
58
|
+
from wowool.build.pydist import clean_package_build
|
|
59
|
+
|
|
60
|
+
clean_package_build(fp_root)
|
|
61
|
+
|
|
62
|
+
@task(pre=[clean])
|
|
63
|
+
def build(_, no_deps: bool = True):
|
|
64
|
+
"""
|
|
65
|
+
Build the Python package
|
|
66
|
+
"""
|
|
67
|
+
from wowool.build.pydist import build_wheel_package
|
|
68
|
+
|
|
69
|
+
build_wheel_package(fp_root, no_deps)
|
|
70
|
+
|
|
71
|
+
@task(aliases=("mep",))
|
|
72
|
+
def make_entry_points(_):
|
|
73
|
+
"""
|
|
74
|
+
Make entry points to the Python CLIs
|
|
75
|
+
"""
|
|
76
|
+
from wowool.build.pydist import make_environment_entry_points
|
|
77
|
+
|
|
78
|
+
make_environment_entry_points(fp_root)
|
|
79
|
+
|
|
80
|
+
@task
|
|
81
|
+
def upload(_, expression="wowool*"):
|
|
82
|
+
"""
|
|
83
|
+
Upload the Python package to AWS CodeArtifact
|
|
84
|
+
"""
|
|
85
|
+
from wowool.build.codeartifact import upload_pypi
|
|
86
|
+
|
|
87
|
+
upload_pypi(fp_root, expression=expression)
|
|
88
|
+
|
|
89
|
+
@task
|
|
90
|
+
def upload_raw(_, fn: Path, namespace: str, package: str, version: str):
|
|
91
|
+
"""
|
|
92
|
+
Upload the raw package to AWS CodeArtifact
|
|
93
|
+
"""
|
|
94
|
+
from wowool.build.codeartifact import upload_raw
|
|
95
|
+
|
|
96
|
+
upload_raw(fn, namespace, package, version)
|
|
97
|
+
|
|
98
|
+
@task
|
|
99
|
+
def upload_pypi(_, expression="wowool*"):
|
|
100
|
+
"""Uploading the Python package to pypi"""
|
|
101
|
+
from wowool.build.pypi import upload_pypi
|
|
102
|
+
|
|
103
|
+
upload_pypi(fp_root, expression=expression)
|
|
104
|
+
|
|
105
|
+
return Collection(version, test, clean, build, make_entry_points, upload, upload_raw, upload_pypi)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Allow both default Invoke namespaces to be available for import
|
|
109
|
+
ns = create_py_tasks()
|
|
110
|
+
namespace = ns
|
wowool/build/pytest.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from difflib import unified_diff
|
|
3
|
+
from logging import getLogger
|
|
4
|
+
from os import environ, pathsep, sep
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
from subprocess import check_output, run
|
|
8
|
+
from typing import Callable, List, Optional
|
|
9
|
+
from unittest import TestLoader, TestResult, TestSuite, TextTestRunner, skipIf
|
|
10
|
+
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from importlib import reload
|
|
13
|
+
from io import StringIO
|
|
14
|
+
import sys
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Test environment
|
|
22
|
+
def is_ci_environment():
|
|
23
|
+
return "CI" in environ and not environ["CI"].lower() in ("0", "false")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Unit-test skip decorators
|
|
27
|
+
skipIfCI = skipIf(is_ci_environment(), "CI environment detected")
|
|
28
|
+
skipIfNotCI = skipIf(not is_ci_environment(), "CI environment not detected")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UnittestError(RuntimeError):
|
|
32
|
+
def __init__(self, result: TestResult):
|
|
33
|
+
self.result = result
|
|
34
|
+
message = f"Unit-test(s) failed: {len(result.errors)} errors and {len(result.failures)} failures"
|
|
35
|
+
super(UnittestError, self).__init__(message)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_unittest_names(
|
|
39
|
+
fp: Path,
|
|
40
|
+
pattern_include: str = "",
|
|
41
|
+
pattern_exclude: Optional[str] = "",
|
|
42
|
+
):
|
|
43
|
+
names: List[str] = []
|
|
44
|
+
files = [fn for fn in fp.glob("tests/**/test_*.py")]
|
|
45
|
+
for filename in files:
|
|
46
|
+
node = ast.parse(filename.read_text())
|
|
47
|
+
offset = 0 if str(fp).endswith("/") else 1
|
|
48
|
+
module_name = str(filename.with_suffix(""))[len(str(fp)) + offset :].replace(sep, ".")
|
|
49
|
+
classes = [n for n in node.body if isinstance(n, ast.ClassDef)]
|
|
50
|
+
|
|
51
|
+
for class_ in classes:
|
|
52
|
+
methods = [n for n in class_.body if isinstance(n, ast.FunctionDef)]
|
|
53
|
+
for method in methods:
|
|
54
|
+
if method.name.startswith("test_"):
|
|
55
|
+
names.append(f"{module_name}.{class_.name}.{method.name}")
|
|
56
|
+
|
|
57
|
+
matcher_include = re.compile(f".*{pattern_include}")
|
|
58
|
+
names = [name for name in names if matcher_include.match(name)]
|
|
59
|
+
if pattern_exclude:
|
|
60
|
+
matcher_exclude = re.compile(f".*{pattern_exclude}")
|
|
61
|
+
names = [name for name in names if not matcher_exclude.match(name)]
|
|
62
|
+
return sorted(names)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_unittest_suite(
|
|
66
|
+
fp: Path,
|
|
67
|
+
pattern_include: str = "",
|
|
68
|
+
pattern_exclude: Optional[str] = "",
|
|
69
|
+
) -> TestSuite:
|
|
70
|
+
tests = get_unittest_names(fp, pattern_include, pattern_exclude)
|
|
71
|
+
return TestLoader().loadTestsFromNames(tests)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def list_unittest_tests(fp: Path, printer: Callable[[str], None] = print, pattern_include: str = "", pattern_exclude: Optional[str] = ""):
|
|
75
|
+
"""
|
|
76
|
+
List the unit tests found in the given directory
|
|
77
|
+
|
|
78
|
+
:param fp: Search directory
|
|
79
|
+
:param printer: Printer function that accepts a string
|
|
80
|
+
"""
|
|
81
|
+
printer("\n".join(get_unittest_names(fp, pattern_include, pattern_exclude)))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def run_unittest_tests(
|
|
85
|
+
fp: Path, verbosity: int, failfast: bool, pattern_include: str = "", pattern_exclude: Optional[str] = "", exit_on_error: bool = True
|
|
86
|
+
) -> TestResult:
|
|
87
|
+
"""
|
|
88
|
+
Run the unittest tests found in the given directory
|
|
89
|
+
|
|
90
|
+
:param fp: Search directory
|
|
91
|
+
:param verbosity: Verbosity level
|
|
92
|
+
:param failfast: Fail on first error
|
|
93
|
+
:param pattern: Only run tests matching the given pattern
|
|
94
|
+
:param exit_on_error: Exit directly in case of a failure, otherwise raise an error
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
suite = get_unittest_suite(fp, pattern_include, pattern_exclude)
|
|
98
|
+
result = TextTestRunner(verbosity=verbosity, failfast=failfast).run(suite)
|
|
99
|
+
if not result.wasSuccessful():
|
|
100
|
+
if exit_on_error:
|
|
101
|
+
exit(1)
|
|
102
|
+
else:
|
|
103
|
+
raise UnittestError(result)
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_unittest_samples(fp: Path, pattern: str = ""):
|
|
108
|
+
"""
|
|
109
|
+
Check the samples
|
|
110
|
+
"""
|
|
111
|
+
matcher = re.compile(f".*{pattern}")
|
|
112
|
+
pypath = f"{pathsep}{environ['PYTHONPATH']}" if "PYTHONPATH" in environ else ""
|
|
113
|
+
environ["PYTHONPATH"] = f"{str(fp)}{pypath}"
|
|
114
|
+
samples_dir = fp / "samples"
|
|
115
|
+
files = sorted([fn for fn in samples_dir.glob("**/*.py") if not (str(fn).endswith("_setup.py") or str(fn).endswith("_cleanup.py"))])
|
|
116
|
+
for fn in files:
|
|
117
|
+
if matcher.match(str(fn)):
|
|
118
|
+
logger.info(f"Running sample: {fn}")
|
|
119
|
+
|
|
120
|
+
fn_setup = Path(str(fn).replace(".py", "_setup.py"))
|
|
121
|
+
if fn_setup.exists():
|
|
122
|
+
run(f"python {fn_setup}", check=True, shell=True, cwd=fn.parent)
|
|
123
|
+
|
|
124
|
+
out_data = check_output(f"python {fn}", shell=True, cwd=fn.parent).decode().strip()
|
|
125
|
+
logger.debug(f"{fn} stdout:[{out_data}]")
|
|
126
|
+
|
|
127
|
+
fn_ref = Path(str(fn).replace(".py", "_output.txt"))
|
|
128
|
+
if fn_ref.exists():
|
|
129
|
+
expected_data = fn_ref.read_text().strip()
|
|
130
|
+
lines = [line for line in unified_diff(expected_data, out_data)]
|
|
131
|
+
if 0 != len(lines):
|
|
132
|
+
raise RuntimeError(f"Unexpected output:\n[{expected_data}]\n!=\n[{out_data}]")
|
|
133
|
+
|
|
134
|
+
fn_cleanup = Path(str(fn).replace(".py", "_cleanup.py"))
|
|
135
|
+
if fn_cleanup.exists():
|
|
136
|
+
run(f"python {fn_cleanup}", check=True, shell=True, cwd=fn.parent)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@contextmanager
|
|
140
|
+
def capture_stream(stream: str = "stdout"):
|
|
141
|
+
stream_original = getattr(sys, stream)
|
|
142
|
+
stream_captured = StringIO()
|
|
143
|
+
setattr(sys, stream, stream_captured)
|
|
144
|
+
reload(sys)
|
|
145
|
+
try:
|
|
146
|
+
yield stream_captured
|
|
147
|
+
finally:
|
|
148
|
+
sys.stdout = stream_original
|
|
149
|
+
reload(sys)
|
wowool/build/utility.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
from os import chdir, getcwd
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def ask(question, default: bool = True) -> bool:
|
|
8
|
+
"""
|
|
9
|
+
Ask a simple yes/no question
|
|
10
|
+
|
|
11
|
+
:param question: The question to present to the user
|
|
12
|
+
:param default: The default answer to assume if no valid answer is provided
|
|
13
|
+
"""
|
|
14
|
+
postfix = "Y/n" if default else "y/N"
|
|
15
|
+
while True:
|
|
16
|
+
answer = input(question + " [{}] ".format(postfix)).lower()
|
|
17
|
+
if "" == answer:
|
|
18
|
+
return default
|
|
19
|
+
elif "y" == answer:
|
|
20
|
+
return True
|
|
21
|
+
elif "n" == answer:
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@contextmanager
|
|
26
|
+
def in_cwd(fp: Union[Path, str]):
|
|
27
|
+
"""
|
|
28
|
+
Temporarily change the working directory
|
|
29
|
+
|
|
30
|
+
:param fp: The folder to change to
|
|
31
|
+
"""
|
|
32
|
+
oldpwd = getcwd()
|
|
33
|
+
chdir(str(fp))
|
|
34
|
+
try:
|
|
35
|
+
yield
|
|
36
|
+
finally:
|
|
37
|
+
chdir(oldpwd)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: wowool-build
|
|
3
|
+
Version: 0.0.1.dev9
|
|
4
|
+
Summary: Wowool Build System
|
|
5
|
+
Home-page: https://www.wowool.com/
|
|
6
|
+
Author: Wowool
|
|
7
|
+
Author-email: info@wowool.com
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: invoke
|
|
11
|
+
Requires-Dist: build
|
|
12
|
+
Requires-Dist: wheel
|
|
13
|
+
Requires-Dist: setuptools
|
|
14
|
+
Requires-Dist: setuptools_scm
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: requires-dist
|
|
19
|
+
Dynamic: requires-python
|
|
20
|
+
|
|
21
|
+
# core-build-py
|
|
22
|
+
|
|
23
|
+
This repository contains Python code for repository and building functionality.
|
|
24
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
wowool/build/codeartifact.py,sha256=m2YlJgzElY2yfMCBun_cdjabBYSKouHA4Zf80JUnFsc,7249
|
|
2
|
+
wowool/build/exceptions.py,sha256=GFWI0F_nTEpDid8dMjMQCDQ66muzTvfLzRm-KSJ1-Uw,42
|
|
3
|
+
wowool/build/git.py,sha256=oD9CB7MHOTYTO7HPZJ7NIuDOjO5ME_DIcUpcx02OM6w,3111
|
|
4
|
+
wowool/build/logging.py,sha256=4ycT471N7V6vrEBXURF0-lho1hbmWA7PJyCHhfHVjeA,4988
|
|
5
|
+
wowool/build/platform.py,sha256=dYEr4Pw0VyChQRnd0ZXwETXZsLVQ0tC9aC4Mu9KJZcc,901
|
|
6
|
+
wowool/build/pydist.py,sha256=j_OiN2G-RVsQ8NwEv2gn19IBpJEcALIFUeYiDn_4NPY,3924
|
|
7
|
+
wowool/build/pypi.py,sha256=lYwyQTj1qnRz2k1k02GVQow5IOkngUBhJRzmhffyWLU,889
|
|
8
|
+
wowool/build/pytask.py,sha256=wlwnQGfVkEUiA_IcRYnz9_BM7hNmzpT_1turmwYHFEM,2879
|
|
9
|
+
wowool/build/pytest.py,sha256=8F1ZklSvpTAnwODXZvzhF6qcND-MlDVbl-e4F1Pf3mI,5304
|
|
10
|
+
wowool/build/utility.py,sha256=q5RwGFD5O9pewuIUxbYt5sA7lj6u1CjWi6PVWQwCxnQ,890
|
|
11
|
+
wowool_build-0.0.1.dev9.dist-info/METADATA,sha256=XEkqufcVFcb7zGD2lrp2j2y4-tK6HGLvqheEVxp950k,558
|
|
12
|
+
wowool_build-0.0.1.dev9.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
13
|
+
wowool_build-0.0.1.dev9.dist-info/top_level.txt,sha256=zerqoDN-iHSHwsS1SN2h0q4H6RI1FOVeHW8fNQC9YdU,7
|
|
14
|
+
wowool_build-0.0.1.dev9.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wowool
|