ichec-platform-core 0.0.1__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.
- ichec_platform_core/__init__.py +0 -0
- ichec_platform_core/filesystem/__init__.py +1 -0
- ichec_platform_core/filesystem/cli.py +52 -0
- ichec_platform_core/filesystem/filesystem.py +268 -0
- ichec_platform_core/logging_utils.py +26 -0
- ichec_platform_core/main_cli.py +39 -0
- ichec_platform_core/network/__init__.py +1 -0
- ichec_platform_core/network/cli.py +44 -0
- ichec_platform_core/network/http_client.py +125 -0
- ichec_platform_core/project/__init__.py +2 -0
- ichec_platform_core/project/milestone.py +34 -0
- ichec_platform_core/project/person.py +17 -0
- ichec_platform_core/project/version.py +68 -0
- ichec_platform_core/py.typed +0 -0
- ichec_platform_core/runtime.py +61 -0
- ichec_platform_core/serialization/__init__.py +2 -0
- ichec_platform_core/serialization/csv_utils.py +15 -0
- ichec_platform_core/serialization/json_utils.py +73 -0
- ichec_platform_core/serialization/key_value.py +50 -0
- ichec_platform_core/serialization/yaml_utils.py +49 -0
- ichec_platform_core/system/__init__.py +4 -0
- ichec_platform_core/system/cluster/__init__.py +0 -0
- ichec_platform_core/system/cluster/cluster_allocation.py +8 -0
- ichec_platform_core/system/cluster/node.py +11 -0
- ichec_platform_core/system/cpu.py +50 -0
- ichec_platform_core/system/environment.py +44 -0
- ichec_platform_core/system/gpu.py +38 -0
- ichec_platform_core/system/network/__init__.py +0 -0
- ichec_platform_core/system/network/host.py +10 -0
- ichec_platform_core/system/network/info.py +7 -0
- ichec_platform_core/system/process.py +191 -0
- ichec_platform_core/system/system_event.py +12 -0
- ichec_platform_core/test_utils.py +24 -0
- ichec_platform_core/url_utils.py +27 -0
- ichec_platform_core/version_control/__init__.py +3 -0
- ichec_platform_core/version_control/cli.py +231 -0
- ichec_platform_core/version_control/git.py +246 -0
- ichec_platform_core/version_control/gitlab.py +106 -0
- ichec_platform_core/version_control/gitlab_client.py +339 -0
- ichec_platform_core-0.0.1.dist-info/METADATA +100 -0
- ichec_platform_core-0.0.1.dist-info/RECORD +45 -0
- ichec_platform_core-0.0.1.dist-info/WHEEL +5 -0
- ichec_platform_core-0.0.1.dist-info/entry_points.txt +2 -0
- ichec_platform_core-0.0.1.dist-info/licenses/LICENSE.txt +241 -0
- ichec_platform_core-0.0.1.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .filesystem import * # NOQA
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from ichec_platform_core.cli_utils import launch_common
|
|
5
|
+
|
|
6
|
+
from .filesystem import read_file, replace_in_files
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cli_replace_in_files(args):
|
|
13
|
+
|
|
14
|
+
launch_common(args)
|
|
15
|
+
|
|
16
|
+
search_term = read_file(args.search.resolve()).strip()
|
|
17
|
+
replace_term = read_file(args.insert.resolve()).strip()
|
|
18
|
+
|
|
19
|
+
target_dir = args.target.resolve()
|
|
20
|
+
logger.info(
|
|
21
|
+
"Replacing '%s' with '%s' in files in %s", search_term, replace_term, target_dir
|
|
22
|
+
)
|
|
23
|
+
count = replace_in_files(target_dir, search_term, replace_term, args.extension)
|
|
24
|
+
logger.info("Replaced in %d files", count)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def register(subparsers):
|
|
28
|
+
|
|
29
|
+
parser = subparsers.add_parser("filesystem")
|
|
30
|
+
subparsers = parser.add_subparsers(required=True)
|
|
31
|
+
|
|
32
|
+
replace_files_parser = subparsers.add_parser("replace_in_files")
|
|
33
|
+
|
|
34
|
+
replace_files_parser.add_argument(
|
|
35
|
+
"--target",
|
|
36
|
+
type=Path,
|
|
37
|
+
help="Path to the directory containing the files",
|
|
38
|
+
)
|
|
39
|
+
replace_files_parser.add_argument(
|
|
40
|
+
"--search", type=Path, help="File containing the term to be replaced"
|
|
41
|
+
)
|
|
42
|
+
replace_files_parser.add_argument(
|
|
43
|
+
"--insert", type=Path, help="File containing the term to be inserted"
|
|
44
|
+
)
|
|
45
|
+
replace_files_parser.add_argument(
|
|
46
|
+
"--extension",
|
|
47
|
+
type=str,
|
|
48
|
+
default="",
|
|
49
|
+
help="Only process files with this extension, if set",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
replace_files_parser.set_defaults(func=cli_replace_in_files)
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module has wrappers over Python built-ins for filsystem operations
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from ichec_platform_core.runtime import ctx
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def copy(src: Path, dst: Path) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Copy the file or directory from `src` to `dst`
|
|
15
|
+
:param src: The location to copy from
|
|
16
|
+
:param dst: The location to copy to
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
if ctx.can_modify():
|
|
20
|
+
shutil.copy(src, dst)
|
|
21
|
+
else:
|
|
22
|
+
ctx.add_cmd(f"copy {src} {dst}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def make_archive(archive: Path, fmt: str, src: Path, rename: bool = False) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Create an archive with name `archive_name` and chosen format, e.g. zip
|
|
28
|
+
from the content in `src`
|
|
29
|
+
:param Path archive_name: The path to the created archive
|
|
30
|
+
:param str fmt: The desired archive format, e.g. zip
|
|
31
|
+
:param Path src: The src of the content to be archived
|
|
32
|
+
:param bool rename: If true rename source directory to have the archive name
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
if ctx.can_modify():
|
|
36
|
+
if rename:
|
|
37
|
+
shutil.copytree(src, archive)
|
|
38
|
+
src = archive
|
|
39
|
+
shutil.make_archive(str(archive), fmt, src)
|
|
40
|
+
if rename:
|
|
41
|
+
shutil.rmtree(archive)
|
|
42
|
+
else:
|
|
43
|
+
ctx.add_cmd(f"make_archive {archive} {fmt} {src}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def unpack_archive(src: Path, dst: Path) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Extract the archive at `src` to `dst`
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
if ctx.can_modify():
|
|
52
|
+
shutil.unpack_archive(src, dst)
|
|
53
|
+
else:
|
|
54
|
+
ctx.add_cmd(f"unpack_archive {src} {dst}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def copy_file(src: Path, dst: Path):
|
|
58
|
+
"""
|
|
59
|
+
Copy a file in src to the target directory in dst
|
|
60
|
+
|
|
61
|
+
Make the dst directory if needed
|
|
62
|
+
"""
|
|
63
|
+
os.makedirs(dst, exist_ok=True)
|
|
64
|
+
shutil.copy(src, dst / src.name)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def copy_files(src: Path, dst: Path):
|
|
68
|
+
"""
|
|
69
|
+
Copy all files from the `src` directory to the `dst` directory
|
|
70
|
+
|
|
71
|
+
Create the dst directory first if needed.
|
|
72
|
+
|
|
73
|
+
:param Path src: The directory to copy from
|
|
74
|
+
:param Path dst: The directory to copy to
|
|
75
|
+
"""
|
|
76
|
+
os.makedirs(dst, exist_ok=True)
|
|
77
|
+
for direntry in src.iterdir():
|
|
78
|
+
if direntry.is_file():
|
|
79
|
+
shutil.copy(direntry, dst)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def copy_files_relative(paths: list[Path], src: Path, dst: Path):
|
|
83
|
+
"""
|
|
84
|
+
For each relative path in `paths` copy the file at `src/path` to
|
|
85
|
+
`dst/path`.
|
|
86
|
+
|
|
87
|
+
Tries to create the dst directory first.
|
|
88
|
+
|
|
89
|
+
:param list[Path] paths: The list of paths to copy
|
|
90
|
+
:param Path src: The source path to copy relative to
|
|
91
|
+
:param Path dst: The destination path to copy into.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
for path in paths:
|
|
95
|
+
os.makedirs(dst / path.parent, exist_ok=True)
|
|
96
|
+
shutil.copy(src / path, dst / path)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def clear_dir(src: Path):
|
|
100
|
+
"""
|
|
101
|
+
If there is a directory at `src` delete it and make a new one
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
if src.exists():
|
|
105
|
+
shutil.rmtree(src)
|
|
106
|
+
os.makedirs(src)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def read_file(src: Path):
|
|
110
|
+
"""
|
|
111
|
+
Read a file at the provided path
|
|
112
|
+
"""
|
|
113
|
+
with open(src, "r", encoding="utf-8") as f:
|
|
114
|
+
return f.read()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def read_file_lines(src: Path):
|
|
118
|
+
"""
|
|
119
|
+
Read a file as lines
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
with open(src, "r", encoding="utf-8") as f:
|
|
123
|
+
return f.readlines()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def write_file_lines(
|
|
127
|
+
src: Path, lines: list[str], make_parents: bool = True, make_exe: bool = False
|
|
128
|
+
):
|
|
129
|
+
"""
|
|
130
|
+
Write a file with lines
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
if make_parents:
|
|
134
|
+
os.makedirs(src.parent, exist_ok=True)
|
|
135
|
+
|
|
136
|
+
with open(src, "w", encoding="utf-8") as f:
|
|
137
|
+
for line in lines:
|
|
138
|
+
f.write(line)
|
|
139
|
+
|
|
140
|
+
if make_exe:
|
|
141
|
+
os.chmod(src, 0o0755)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def write_file(src: Path, content: str, make_parents: bool = True):
|
|
145
|
+
"""
|
|
146
|
+
Write a file
|
|
147
|
+
"""
|
|
148
|
+
if make_parents:
|
|
149
|
+
os.makedirs(src.parent, exist_ok=True)
|
|
150
|
+
|
|
151
|
+
with open(src, "w", encoding="utf-8") as f:
|
|
152
|
+
f.write(content)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def replace_in_file(src: Path, search_term: str, replace_term: str) -> bool:
|
|
156
|
+
"""
|
|
157
|
+
Replace the search term in a file
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
content = read_file_lines(src)
|
|
161
|
+
has_update = False
|
|
162
|
+
for idx, line in enumerate(content):
|
|
163
|
+
replaced_line = line.replace(search_term, replace_term)
|
|
164
|
+
if replaced_line != line:
|
|
165
|
+
content[idx] = replaced_line
|
|
166
|
+
has_update = True
|
|
167
|
+
if has_update:
|
|
168
|
+
write_file_lines(src, content)
|
|
169
|
+
return True
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def replace_in_files(
|
|
174
|
+
src: Path, search_term: str, replace_term: str, extension: str
|
|
175
|
+
) -> int:
|
|
176
|
+
"""
|
|
177
|
+
Replace a search term in multiple files
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
count = 0
|
|
181
|
+
for direntry in src.iterdir():
|
|
182
|
+
if direntry.is_file():
|
|
183
|
+
if not extension or direntry.suffix == f".{extension}":
|
|
184
|
+
replaced = replace_in_file(direntry, search_term, replace_term)
|
|
185
|
+
if replaced:
|
|
186
|
+
count += 1
|
|
187
|
+
return count
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def file_contains_string(src: Path, search_term: str) -> bool:
|
|
191
|
+
"""
|
|
192
|
+
True if a file contains the search term
|
|
193
|
+
"""
|
|
194
|
+
content = read_file(src)
|
|
195
|
+
return search_term in content
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_dirs(path: Path, prefix: str = "") -> list[Path]:
|
|
199
|
+
"""
|
|
200
|
+
Return all directories in a path, optionally filter directory
|
|
201
|
+
name on a prefix
|
|
202
|
+
"""
|
|
203
|
+
if prefix:
|
|
204
|
+
return [
|
|
205
|
+
entry
|
|
206
|
+
for entry in path.iterdir()
|
|
207
|
+
if entry.is_dir() and entry.name.startswith(prefix)
|
|
208
|
+
]
|
|
209
|
+
return [entry for entry in path.iterdir() if entry.is_dir()]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def is_file_with_ext(f, extension: str) -> bool:
|
|
213
|
+
"""
|
|
214
|
+
True if the path item is a file with given extension
|
|
215
|
+
"""
|
|
216
|
+
return f.is_file() and str(f).endswith(f".{extension}")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def get_files(path: Path, extension: str) -> list:
|
|
220
|
+
"""
|
|
221
|
+
Return all files in this dir with the provided extension
|
|
222
|
+
"""
|
|
223
|
+
return [f for f in path.glob("*") if is_file_with_ext(f, extension)]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def is_file_with_extensions(
|
|
227
|
+
f, extensions: tuple[str], excludes: list[str] | None = None
|
|
228
|
+
):
|
|
229
|
+
"""
|
|
230
|
+
True if the path item is a file and has one of the provided extensions
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
if not f.is_file():
|
|
234
|
+
return False
|
|
235
|
+
for ext in extensions:
|
|
236
|
+
check_str = str(f).lower()
|
|
237
|
+
if check_str.endswith(f".{ext.lower()}"):
|
|
238
|
+
if excludes:
|
|
239
|
+
for exclude in excludes:
|
|
240
|
+
if check_str.endswith(f"{exclude.lower()}.{ext.lower()}"):
|
|
241
|
+
return False
|
|
242
|
+
return True
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def get_files_recursive(
|
|
247
|
+
path: Path, extensions: tuple[str], excludes: list[str] | None = None
|
|
248
|
+
):
|
|
249
|
+
"""
|
|
250
|
+
Get all provided files recursively, filter on extensions and excludes
|
|
251
|
+
"""
|
|
252
|
+
return [
|
|
253
|
+
f for f in path.rglob("*") if is_file_with_extensions(f, extensions, excludes)
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def get_json_files(path: Path):
|
|
258
|
+
"""
|
|
259
|
+
Get all json files in this dir
|
|
260
|
+
"""
|
|
261
|
+
return get_files(path, extension="json")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def get_csv_files(path: Path):
|
|
265
|
+
"""
|
|
266
|
+
Get all csv files in this dir
|
|
267
|
+
"""
|
|
268
|
+
return get_files(path, extension="csv")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def setup_default_logger(log_filename: str | None = None):
|
|
5
|
+
fmt = "%(asctime)s%(msecs)03d | %(filename)s:%(lineno)s:%(funcName)s | %(message)s"
|
|
6
|
+
logging.basicConfig(
|
|
7
|
+
filename=log_filename,
|
|
8
|
+
filemode="w",
|
|
9
|
+
format=fmt,
|
|
10
|
+
datefmt="%Y%m%dT%H:%M:%S:",
|
|
11
|
+
level=logging.INFO,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LogLine:
|
|
16
|
+
"""
|
|
17
|
+
Helper class to represent a log line
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, timestamp: float, thread_id: int, message: str):
|
|
21
|
+
self.timestamp = timestamp
|
|
22
|
+
self.message = message
|
|
23
|
+
self.thread_id = thread_id
|
|
24
|
+
|
|
25
|
+
def __str__(self):
|
|
26
|
+
return f"{self.timestamp} | {self.thread_id} | {self.message}"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
This is the main entrypoint for the ichec_platform_core utility
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from ichec_platform_core.cli_utils import launch_common
|
|
11
|
+
import ichec_platform_core.network.cli
|
|
12
|
+
import ichec_platform_core.filesystem.cli
|
|
13
|
+
import ichec_platform_core.version_control.cli
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main_cli():
|
|
20
|
+
parser = argparse.ArgumentParser()
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--dry_run",
|
|
23
|
+
type=int,
|
|
24
|
+
default=0,
|
|
25
|
+
help="Dry run script - 0 can modify, 1 can read, 2 no modify - no read",
|
|
26
|
+
)
|
|
27
|
+
subparsers = parser.add_subparsers(required=True)
|
|
28
|
+
|
|
29
|
+
ichec_platform_core.network.cli.register(subparsers)
|
|
30
|
+
ichec_platform_core.filesystem.cli.register(subparsers)
|
|
31
|
+
ichec_platform_core.version_control.cli.register(subparsers)
|
|
32
|
+
|
|
33
|
+
args = parser.parse_args()
|
|
34
|
+
launch_common(args)
|
|
35
|
+
args.func(args)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
main_cli()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .http_client import * # NOQA
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from ichec_platform_core.cli_utils import launch_common
|
|
6
|
+
|
|
7
|
+
from .http_client import HttpClient
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def download(args):
|
|
13
|
+
|
|
14
|
+
launch_common(args)
|
|
15
|
+
logger.info("Attempting to download from %s", args.url)
|
|
16
|
+
|
|
17
|
+
headers = {args.token_header: args.token}
|
|
18
|
+
|
|
19
|
+
http_client = HttpClient()
|
|
20
|
+
http_client.download_file(args.url, args.download_dir, headers)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def register(subparsers):
|
|
24
|
+
|
|
25
|
+
parser = subparsers.add_parser("network")
|
|
26
|
+
|
|
27
|
+
subparsers = parser.add_subparsers(required=True)
|
|
28
|
+
|
|
29
|
+
download_parser = subparsers.add_parser("download")
|
|
30
|
+
|
|
31
|
+
download_parser.add_argument("url", type=str, help="Url to download")
|
|
32
|
+
download_parser.add_argument(
|
|
33
|
+
"--token", type=str, help="Optional auth token", default=""
|
|
34
|
+
)
|
|
35
|
+
download_parser.add_argument(
|
|
36
|
+
"--token_header", type=str, help="Optional auth token header key", default=""
|
|
37
|
+
)
|
|
38
|
+
download_parser.add_argument(
|
|
39
|
+
"--download_dir",
|
|
40
|
+
type=Path,
|
|
41
|
+
help="Directory to download to",
|
|
42
|
+
default=Path(os.getcwd()),
|
|
43
|
+
)
|
|
44
|
+
download_parser.set_defaults(func=download)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A basic low-dependency http client
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import urllib.request
|
|
6
|
+
import urllib.parse
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
from ichec_platform_core.runtime import ctx
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HttpClient:
|
|
15
|
+
"""
|
|
16
|
+
A basic http client with low dependencies (urllib)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.default_encode_charset = "utf-8"
|
|
21
|
+
self.default_decode_charset = "utf-8"
|
|
22
|
+
|
|
23
|
+
def post_json(self, url: str, payload, headers: dict[str, str] | None):
|
|
24
|
+
if not headers:
|
|
25
|
+
headers = {}
|
|
26
|
+
|
|
27
|
+
headers["content-type"] = "application/json"
|
|
28
|
+
return self.make_post_request(url, headers, json.dumps(payload))
|
|
29
|
+
|
|
30
|
+
def make_get_request(self, url: str, headers: dict[str, str] | None = None):
|
|
31
|
+
if not headers:
|
|
32
|
+
headers = {}
|
|
33
|
+
return self._make_request(url, "GET", headers)
|
|
34
|
+
|
|
35
|
+
def make_put_request(
|
|
36
|
+
self, url: str, headers: dict[str, str] | None = None, payload=None
|
|
37
|
+
):
|
|
38
|
+
if not headers:
|
|
39
|
+
headers = {}
|
|
40
|
+
return self._make_request(url, "PUT", headers, payload)
|
|
41
|
+
|
|
42
|
+
def make_post_request(
|
|
43
|
+
self, url: str, headers: dict[str, str] | None = None, payload=None
|
|
44
|
+
):
|
|
45
|
+
if not headers:
|
|
46
|
+
headers = {}
|
|
47
|
+
return self._make_request(url, "POST", headers, payload)
|
|
48
|
+
|
|
49
|
+
def download_file(
|
|
50
|
+
self, url: str, path: Path, headers: dict[str, str] | None = None
|
|
51
|
+
):
|
|
52
|
+
if not headers:
|
|
53
|
+
headers = {}
|
|
54
|
+
|
|
55
|
+
if not ctx.can_read():
|
|
56
|
+
ctx.add_cmd(f"download file {url} {path} {headers}")
|
|
57
|
+
|
|
58
|
+
with open(path, "wb") as f:
|
|
59
|
+
req = urllib.request.Request(url, headers=headers)
|
|
60
|
+
with urllib.request.urlopen(req) as response:
|
|
61
|
+
self._check_http_status(response)
|
|
62
|
+
f.write(response.read())
|
|
63
|
+
|
|
64
|
+
def upload_file(self, url: str, path: Path, headers: dict[str, str] | None = None):
|
|
65
|
+
|
|
66
|
+
if not headers:
|
|
67
|
+
headers = {}
|
|
68
|
+
|
|
69
|
+
if not ctx.can_modify():
|
|
70
|
+
ctx.add_cmd(f"upload file {url} {path} {headers}")
|
|
71
|
+
return ""
|
|
72
|
+
|
|
73
|
+
if not path.exists():
|
|
74
|
+
raise RuntimeError(f"Attempted to upload file without valid path: {path}")
|
|
75
|
+
|
|
76
|
+
headers["Content-Length"] = str(os.stat(path).st_size)
|
|
77
|
+
headers["Content-Type"] = "application/octet-stream"
|
|
78
|
+
|
|
79
|
+
with open(path, "rb") as f:
|
|
80
|
+
req = urllib.request.Request(url, headers=headers, data=f, method="PUT")
|
|
81
|
+
with urllib.request.urlopen(req) as response:
|
|
82
|
+
self._check_http_status(response)
|
|
83
|
+
response_str = response.read().decode(self._get_charset(response))
|
|
84
|
+
return response_str
|
|
85
|
+
|
|
86
|
+
def _make_request(
|
|
87
|
+
self, url: str, method: str, headers: dict[str, str] | None = None, payload=None
|
|
88
|
+
):
|
|
89
|
+
|
|
90
|
+
if not headers:
|
|
91
|
+
headers = {}
|
|
92
|
+
|
|
93
|
+
if method == "GET":
|
|
94
|
+
if not ctx.can_read():
|
|
95
|
+
ctx.add_cmd(f"make_request GET {url} {headers}")
|
|
96
|
+
return ""
|
|
97
|
+
elif method in ["PUT", "POST"]:
|
|
98
|
+
if not ctx.can_modify():
|
|
99
|
+
ctx.add_cmd(f"make_request {method} {url} {headers} {payload}")
|
|
100
|
+
return ""
|
|
101
|
+
|
|
102
|
+
req = urllib.request.Request(
|
|
103
|
+
url, headers=headers, data=self._endcode_payload(payload), method=method
|
|
104
|
+
)
|
|
105
|
+
with urllib.request.urlopen(req) as response:
|
|
106
|
+
self._check_http_status(response)
|
|
107
|
+
response_str = response.read().decode(self._get_charset(response))
|
|
108
|
+
return response_str
|
|
109
|
+
|
|
110
|
+
def _check_http_status(self, response):
|
|
111
|
+
if response.status >= 300:
|
|
112
|
+
body = response.read().decode(self._get_charset(response))
|
|
113
|
+
msg = f"code {response.status}, reason {response.reason}, and body {body}"
|
|
114
|
+
raise RuntimeError(f"Http Request failed with: {msg}")
|
|
115
|
+
|
|
116
|
+
def _endcode_payload(self, payload):
|
|
117
|
+
if not payload:
|
|
118
|
+
return payload
|
|
119
|
+
return payload.encode("utf-8")
|
|
120
|
+
|
|
121
|
+
def _get_charset(self, response):
|
|
122
|
+
charset = response.headers.get_content_charset()
|
|
123
|
+
if not charset:
|
|
124
|
+
charset = "utf-8"
|
|
125
|
+
return charset
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module has milestone related project elements
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from .person import Person
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Issue(BaseModel, frozen=True):
|
|
13
|
+
"""
|
|
14
|
+
An issue is a user story or defect to work on
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
title: str = ""
|
|
18
|
+
description: str = ""
|
|
19
|
+
start_date: datetime | None = None
|
|
20
|
+
due_date: datetime | None = None
|
|
21
|
+
assignee: Person | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Milestone(BaseModel, frozen=True):
|
|
25
|
+
"""
|
|
26
|
+
A milestone is a collection of work items with start and
|
|
27
|
+
planned end dates
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
title: str = ""
|
|
31
|
+
description: str = ""
|
|
32
|
+
start_date: datetime | None = None
|
|
33
|
+
due_date: datetime | None = None
|
|
34
|
+
issues: list[Issue] = []
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Class representing a person, eg a user on a platform
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Person(BaseModel, frozen=True):
|
|
9
|
+
"""
|
|
10
|
+
A person class - used for referencing project
|
|
11
|
+
contributors.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
name: str = ""
|
|
15
|
+
email: str = ""
|
|
16
|
+
orcid: str = ""
|
|
17
|
+
username: str
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module has functionality for representing and
|
|
3
|
+
manipulating project version numbers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import date
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Version(BaseModel, frozen=True):
|
|
12
|
+
"""
|
|
13
|
+
Representation of a package version number.
|
|
14
|
+
|
|
15
|
+
This resembles a semver scheme but doesn't necessarily
|
|
16
|
+
follow one.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
content (str): Version in string form x.y.zzzz
|
|
20
|
+
scheme (str): The versioning scheme to use, 'semver' or 'date'
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
major (int): Major version number
|
|
24
|
+
minor (int): Minor version number
|
|
25
|
+
patch (int): Path version number
|
|
26
|
+
scheme (str): Versioning scheme to use
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
major: int
|
|
30
|
+
minor: int
|
|
31
|
+
patch: int
|
|
32
|
+
scheme: str = "semver"
|
|
33
|
+
|
|
34
|
+
def as_string(self) -> str:
|
|
35
|
+
return f"{self.major}.{self.minor}.{self.patch}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def increment(version: Version, field="patch", today=date.today()) -> Version:
|
|
39
|
+
major = version.major
|
|
40
|
+
minor = version.minor
|
|
41
|
+
patch = version.patch
|
|
42
|
+
|
|
43
|
+
if version.scheme == "semver":
|
|
44
|
+
if field == "patch":
|
|
45
|
+
patch += 1
|
|
46
|
+
elif field == "minor":
|
|
47
|
+
patch = 0
|
|
48
|
+
minor += 1
|
|
49
|
+
elif field == "major":
|
|
50
|
+
major += 1
|
|
51
|
+
minor = 0
|
|
52
|
+
patch = 0
|
|
53
|
+
elif version.scheme == "date":
|
|
54
|
+
major = today.year - 2000
|
|
55
|
+
minor = today.month
|
|
56
|
+
if minor != version.minor:
|
|
57
|
+
patch = 1
|
|
58
|
+
else:
|
|
59
|
+
patch = version.patch + 1
|
|
60
|
+
return version.model_copy(update={"major": major, "minor": minor, "patch": patch})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse(content: str, scheme: str = "semver") -> Version:
|
|
64
|
+
"""
|
|
65
|
+
Parse the version from the input content, assumes 'major.minor.patch' format.
|
|
66
|
+
"""
|
|
67
|
+
major, minor, patch = content.split(".")
|
|
68
|
+
return Version(major=int(major), minor=int(minor), patch=int(patch), scheme=scheme)
|
|
File without changes
|