Skip to content

Commit 86404ba

Browse files
author
Thomas Desveaux
authored
Merge pull request #114 from dontnod/tds/improve-artifact-git
download-fileset: improve `_find_matching_artifact` for git revision
2 parents 32780a2 + 727c825 commit 86404ba

File tree

6 files changed

+378
-92
lines changed

6 files changed

+378
-92
lines changed

nimp/artifacts.py

Lines changed: 67 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222

2323
'''Provides functions for build artifacts'''
2424

25-
import copy
25+
from __future__ import annotations
26+
2627
import datetime
2728
import hashlib
2829
import json
@@ -34,6 +35,8 @@
3435
import stat
3536
import zipfile
3637
from pathlib import Path
38+
from typing import TYPE_CHECKING
39+
from typing import TypedDict
3740

3841
import requests
3942

@@ -52,16 +55,36 @@
5255
torf = None
5356

5457

55-
def _is_http_url(string):
58+
if TYPE_CHECKING:
59+
from typing import Any
60+
from typing import Iterable
61+
from typing import Mapping
62+
63+
StrPathLike = str | os.PathLike[str]
64+
65+
66+
class Artifact(TypedDict):
67+
revision: str
68+
uri: str
69+
70+
71+
def _is_http_url(string: str) -> bool:
5672
return re.match(r'^http[s]?:\/\/.*$', string) is not None
5773

5874

59-
def list_artifacts(artifact_pattern: str, format_arguments, api_context):
75+
def list_artifacts(
76+
artifact_pattern: str,
77+
format_arguments: Mapping[str, Any],
78+
api_context_: nimp.utils.git.GitApiContext | None,
79+
) -> list[Artifact]:
6080
'''List all artifacts and their revision using the provided pattern after formatting'''
6181

62-
format_arguments = copy.deepcopy(format_arguments)
63-
format_arguments['revision'] = '{revision}'
64-
artifact_pattern = artifact_pattern.format(**format_arguments)
82+
artifact_pattern = artifact_pattern.format_map(
83+
{
84+
**format_arguments,
85+
'revision': '{revision}',
86+
}
87+
)
6588

6689
if not _is_http_url(artifact_pattern):
6790
artifact_source = nimp.system.sanitize_path(os.path.dirname(artifact_pattern))
@@ -74,27 +97,25 @@ def list_artifacts(artifact_pattern: str, format_arguments, api_context):
7497
)
7598

7699
all_files = _list_files(artifact_source, False)
77-
all_artifacts = []
100+
all_artifacts: list[Artifact] = []
78101
for file_uri in all_files:
79102
file_name = os.path.basename(file_uri.rstrip('/'))
80103
artifact_match = artifact_regex.match(file_name)
81-
if artifact_match:
82-
group_revision = artifact_match.group('revision')
83-
sortable_revision = copy.deepcopy(group_revision)
84-
if api_context:
85-
sortable_revision = nimp.utils.git.get_gitea_commit_timestamp(api_context, group_revision)
86-
if sortable_revision is not None:
87-
artifact = {
88-
'revision': group_revision,
89-
'sortable_revision': sortable_revision,
90-
'uri': file_uri,
91-
}
92-
all_artifacts.append(artifact)
104+
if not artifact_match:
105+
continue
106+
107+
group_revision = artifact_match.group('revision')
108+
all_artifacts.append(
109+
{
110+
'revision': group_revision,
111+
'uri': file_uri,
112+
}
113+
)
93114
return all_artifacts
94115

95116

96-
def _list_files(source: str, recursive):
97-
all_files = []
117+
def _list_files(source: str, recursive: bool) -> list[str]:
118+
all_files: list[str] = []
98119

99120
if _is_http_url(source):
100121
if not source.endswith('/'):
@@ -128,7 +149,7 @@ def _list_files(source: str, recursive):
128149
return all_files
129150

130151

131-
def download_artifact(workspace_directory, artifact_uri):
152+
def download_artifact(workspace_directory: str, artifact_uri: str) -> str:
132153
'''Download an artifact to the workspace'''
133154

134155
download_directory = os.path.join(workspace_directory, '.nimp', 'downloads')
@@ -158,7 +179,7 @@ def download_artifact(workspace_directory, artifact_uri):
158179
return local_artifact_path
159180

160181

161-
def _download_file(file_uri, output_path):
182+
def _download_file(file_uri: str, output_path: StrPathLike) -> None:
162183
if os.path.exists(output_path):
163184
os.remove(output_path)
164185
output_directory = os.path.dirname(output_path)
@@ -174,7 +195,7 @@ def _download_file(file_uri, output_path):
174195
shutil.copyfile(file_uri, output_path)
175196

176197

177-
def _extract_archive(archive_path, output_path):
198+
def _extract_archive(archive_path: StrPathLike, output_path: StrPathLike) -> None:
178199
if os.path.exists(output_path):
179200
shutil.rmtree(output_path)
180201

@@ -192,7 +213,7 @@ def _extract_archive(archive_path, output_path):
192213
os.remove(inner_archive_path)
193214

194215

195-
def install_artifact(artifact_path, destination_directory):
216+
def install_artifact(artifact_path: str, destination_directory: StrPathLike) -> None:
196217
'''Install an artifact in the workspace'''
197218

198219
if not os.path.exists(artifact_path):
@@ -213,7 +234,7 @@ def install_artifact(artifact_path, destination_directory):
213234
_try_make_executable(destination)
214235

215236

216-
def _try_make_executable(file_path):
237+
def _try_make_executable(file_path: str) -> None:
217238
if platform.system() == 'Windows':
218239
return
219240

@@ -231,14 +252,20 @@ def _try_make_executable(file_path):
231252
logging.warning('Failed to make file executable: %s (FilePath: %s)', exception, file_path)
232253

233254

234-
def _try_rename(src, dst, max_attempts=5, retry_delay=2):
235-
def _rename():
255+
def _try_rename(src: StrPathLike, dst: StrPathLike, max_attempts: int = 5, retry_delay: int = 2) -> None:
256+
def _rename() -> None:
236257
os.rename(src, dst)
237258

238259
nimp.system.try_execute(_rename, OSError, attempt_maximum=max_attempts, retry_delay=retry_delay)
239260

240261

241-
def create_artifact(artifact_path, file_collection, archive, compress, dry_run):
262+
def create_artifact(
263+
artifact_path: str,
264+
file_collection: Iterable[tuple[StrPathLike, StrPathLike]],
265+
archive: bool,
266+
compress: bool,
267+
dry_run: bool,
268+
) -> None:
242269
'''Create an artifact'''
243270

244271
if os.path.isfile(artifact_path + '.zip') or os.path.isdir(artifact_path):
@@ -292,7 +319,7 @@ def create_artifact(artifact_path, file_collection, archive, compress, dry_run):
292319
shutil.move(artifact_path_tmp, artifact_path)
293320

294321

295-
def create_torrent(artifact_path, announce, dry_run):
322+
def create_torrent(artifact_path: StrPathLike, announce: str | None, dry_run: bool) -> None:
296323
'''Create a torrent for an existing artifact'''
297324

298325
if torf is None:
@@ -329,7 +356,7 @@ def create_torrent(artifact_path, announce, dry_run):
329356
tmp_torrent_path.rename(torrent_path)
330357

331358

332-
def create_hash(artifact_path, hash_method, dry_run):
359+
def create_hash(artifact_path: str, hash_method, dry_run: bool) -> None:
333360
artifact_full_path = _find_artifact(artifact_path)
334361
if not artifact_full_path:
335362
raise FileNotFoundError(f'Artifact not found: {artifact_path}')
@@ -343,26 +370,23 @@ def create_hash(artifact_path, hash_method, dry_run):
343370
json.dump({hash_method: file_hash}, fh)
344371

345372

346-
def get_file_hash(file_path, hash_method):
373+
def get_file_hash(file_path: StrPathLike, hash_method: str):
347374
'''helper function to parse potentially big files'''
348-
assert getattr(hashlib, hash_method)()
349-
hash_lib = getattr(hashlib, hash_method)()
375+
hasher = hashlib.new(hash_method)
350376

351377
_BLOCK_SIZE = 65536
352378

353379
with open(file_path, 'rb') as fh:
354-
file_buffer = fh.read(_BLOCK_SIZE)
355-
while len(file_buffer) > 0:
356-
hash_lib.update(file_buffer)
357-
file_buffer = fh.read(_BLOCK_SIZE)
358-
file_hash = hash_lib.hexdigest()
380+
while buffer := fh.read(_BLOCK_SIZE):
381+
hasher.update(buffer)
382+
file_hash = hasher.hexdigest()
359383

360-
logging.debug(f"{file_path} {hash_method}: {file_hash}")
384+
logging.debug("%s %s: %s", file_path, hash_method, file_hash)
361385
return file_hash
362386

363387

364388
# TODO (l.cahour): this is workaround the fact we don't use artifact objects containing the info we need
365-
def _find_artifact(artifact_path):
389+
def _find_artifact(artifact_path: str) -> str | None:
366390
if os.path.isfile(artifact_path + '.zip'):
367391
return artifact_path + '.zip'
368392
elif os.path.isdir(artifact_path):
@@ -373,8 +397,8 @@ def _find_artifact(artifact_path):
373397

374398
# TODO (l.cahour): this is a naive first attempt at using a wrapper class to clean how we handle artifacts saving
375399
# Try to make this better and use it everywhere else in the future
376-
class TempArtifact(object):
377-
def __init__(self, file_path, mode, force=False):
400+
class TempArtifact:
401+
def __init__(self, file_path: StrPathLike, mode, force: bool = False) -> None:
378402
self.name = file_path
379403
self.temp = f'{file_path}.tmp'
380404
self.mode = mode

0 commit comments

Comments
 (0)