stouputils 1.2.16__tar.gz → 1.2.17__tar.gz
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.
- {stouputils-1.2.16 → stouputils-1.2.17}/PKG-INFO +1 -1
- {stouputils-1.2.16 → stouputils-1.2.17}/pyproject.toml +1 -1
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/all_doctests.py +2 -2
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/applications/automatic_docs.py +7 -7
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/archive.py +2 -2
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/backup.py +7 -7
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/collections.py +1 -1
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/continuous_delivery/cd_utils.py +4 -4
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/continuous_delivery/github.py +26 -26
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/continuous_delivery/pyproject.py +2 -2
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/ctx.py +87 -11
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/decorators.py +4 -4
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/image.py +4 -4
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/io.py +2 -2
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/parallel.py +2 -2
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/print.py +8 -9
- {stouputils-1.2.16 → stouputils-1.2.17}/.gitignore +0 -0
- {stouputils-1.2.16 → stouputils-1.2.17}/LICENSE +0 -0
- {stouputils-1.2.16 → stouputils-1.2.17}/README.md +0 -0
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/__init__.py +0 -0
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/applications/__init__.py +0 -0
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/continuous_delivery/__init__.py +0 -0
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/continuous_delivery/pypi.py +0 -0
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/dont_look/zip_file_override.py +0 -0
- {stouputils-1.2.16 → stouputils-1.2.17}/stouputils/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stouputils
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.17
|
|
4
4
|
Summary: Stouputils is a collection of utility modules designed to simplify and enhance the development process. It includes a range of tools for tasks such as execution of doctests, display utilities, decorators, as well as context managers, and many more.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Stoupy51/stouputils
|
|
6
6
|
Project-URL: Issues, https://github.com/Stoupy51/stouputils/issues
|
|
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "stouputils"
|
|
8
|
-
version = "1.2.
|
|
8
|
+
version = "1.2.17"
|
|
9
9
|
description = "Stouputils is a collection of utility modules designed to simplify and enhance the development process. It includes a range of tools for tasks such as execution of doctests, display utilities, decorators, as well as context managers, and many more."
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -30,10 +30,10 @@ def launch_tests(root_dir: str, importing_errors: LogLevels = LogLevels.WARNING_
|
|
|
30
30
|
root_dir (str): Root directory to search for modules
|
|
31
31
|
importing_errors (LogLevels): Log level for the errors when importing modules
|
|
32
32
|
strict (bool): Modify the force_raise_exception variable to True in the decorators module
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
Returns:
|
|
35
35
|
int: The number of failed tests
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
Examples:
|
|
38
38
|
>>> launch_tests("unknown_dir")
|
|
39
39
|
Traceback (most recent call last):
|
|
@@ -276,7 +276,7 @@ def generate_index_rst(
|
|
|
276
276
|
get_versions_function: Callable[[str, str], list[str]] = get_versions_from_github,
|
|
277
277
|
) -> None:
|
|
278
278
|
""" Generate index.rst from README.md content.
|
|
279
|
-
|
|
279
|
+
|
|
280
280
|
Args:
|
|
281
281
|
readme_path (str): Path to the README.md file
|
|
282
282
|
index_path (str): Path where index.rst should be created
|
|
@@ -291,10 +291,10 @@ def generate_index_rst(
|
|
|
291
291
|
|
|
292
292
|
# Generate version selector
|
|
293
293
|
version_selector: str = "\n\n**Versions**: "
|
|
294
|
-
|
|
294
|
+
|
|
295
295
|
# Get versions from GitHub
|
|
296
296
|
version_list: list[str] = get_versions_function(github_user, github_repo)
|
|
297
|
-
|
|
297
|
+
|
|
298
298
|
# Create version links
|
|
299
299
|
version_links: list[str] = []
|
|
300
300
|
for version in version_list:
|
|
@@ -303,7 +303,7 @@ def generate_index_rst(
|
|
|
303
303
|
else:
|
|
304
304
|
version_links.append(f"`v{version} <../v{version}/index.html>`_")
|
|
305
305
|
version_selector += ", ".join(version_links)
|
|
306
|
-
|
|
306
|
+
|
|
307
307
|
# Generate module documentation section
|
|
308
308
|
project_module: str = project.lower()
|
|
309
309
|
module_docs: str = f"""
|
|
@@ -325,7 +325,7 @@ def generate_index_rst(
|
|
|
325
325
|
{'-' * 100}
|
|
326
326
|
{module_docs}
|
|
327
327
|
"""
|
|
328
|
-
|
|
328
|
+
|
|
329
329
|
# Write the RST file
|
|
330
330
|
with open(index_path, "w", encoding="utf-8") as f:
|
|
331
331
|
f.write(rst_content)
|
|
@@ -444,7 +444,7 @@ def update_documentation(
|
|
|
444
444
|
# Modify build directory if version is specified
|
|
445
445
|
latest_dir: str = f"{html_dir}/latest"
|
|
446
446
|
build_dir: str = latest_dir if not version else f"{html_dir}/v{version}"
|
|
447
|
-
|
|
447
|
+
|
|
448
448
|
# Create directories if they don't exist
|
|
449
449
|
for dir in [modules_dir, static_dir, templates_dir]:
|
|
450
450
|
os.makedirs(dir, exist_ok=True)
|
|
@@ -495,7 +495,7 @@ def update_documentation(
|
|
|
495
495
|
project_dir=project_dir if project_dir else f"{root_path}/{project}",
|
|
496
496
|
build_dir=build_dir,
|
|
497
497
|
)
|
|
498
|
-
|
|
498
|
+
|
|
499
499
|
# Add index.html to the build directory that redirects to the latest version
|
|
500
500
|
generate_redirect_function(f"{html_dir}/index.html")
|
|
501
501
|
|
|
@@ -86,7 +86,7 @@ def repair_zip_file(file_path: str, destination: str) -> bool:
|
|
|
86
86
|
destination (str): Destination of the new file
|
|
87
87
|
Returns:
|
|
88
88
|
bool: Always returns True unless any strong error
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
Examples:
|
|
91
91
|
|
|
92
92
|
.. code-block:: python
|
|
@@ -113,7 +113,7 @@ def repair_zip_file(file_path: str, destination: str) -> bool:
|
|
|
113
113
|
new_zip_file.writestr(file_name, zip_file.read(file_name))
|
|
114
114
|
except KeyboardInterrupt:
|
|
115
115
|
continue
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
return True
|
|
118
118
|
|
|
119
119
|
|
|
@@ -155,11 +155,11 @@ def create_delta_backup(source_path: str, destination_folder: str, exclude_patte
|
|
|
155
155
|
for file in files:
|
|
156
156
|
full_path: str = clean_path(os.path.join(root, file))
|
|
157
157
|
arcname: str = clean_path(os.path.relpath(full_path, start=os.path.dirname(source_path)))
|
|
158
|
-
|
|
158
|
+
|
|
159
159
|
# Skip file if it matches any exclude pattern
|
|
160
160
|
if exclude_patterns and any(fnmatch.fnmatch(arcname, pattern) for pattern in exclude_patterns):
|
|
161
161
|
continue
|
|
162
|
-
|
|
162
|
+
|
|
163
163
|
file_hash: str | None = get_file_hash(full_path)
|
|
164
164
|
if file_hash is None:
|
|
165
165
|
continue
|
|
@@ -170,7 +170,7 @@ def create_delta_backup(source_path: str, destination_folder: str, exclude_patte
|
|
|
170
170
|
zip_info: zipfile.ZipInfo = zipfile.ZipInfo(arcname)
|
|
171
171
|
zip_info.compress_type = zipfile.ZIP_DEFLATED
|
|
172
172
|
zip_info.comment = file_hash.encode() # Store hash in comment
|
|
173
|
-
|
|
173
|
+
|
|
174
174
|
# Read and write file in chunks
|
|
175
175
|
with open(full_path, "rb") as f:
|
|
176
176
|
with zipf.open(zip_info, "w", force_zip64=True) as zf:
|
|
@@ -179,20 +179,20 @@ def create_delta_backup(source_path: str, destination_folder: str, exclude_patte
|
|
|
179
179
|
has_changes = True
|
|
180
180
|
except Exception as e:
|
|
181
181
|
warning(f"Error writing file {full_path} to backup: {e}")
|
|
182
|
-
|
|
182
|
+
|
|
183
183
|
# Track current files for deletion detection
|
|
184
184
|
if arcname in previous_files:
|
|
185
185
|
previous_files.remove(arcname)
|
|
186
186
|
else:
|
|
187
187
|
arcname: str = clean_path(os.path.basename(source_path))
|
|
188
188
|
file_hash: str | None = get_file_hash(source_path)
|
|
189
|
-
|
|
189
|
+
|
|
190
190
|
if file_hash is not None and not is_file_in_any_previous_backup(arcname, file_hash, previous_backups):
|
|
191
191
|
try:
|
|
192
192
|
zip_info: zipfile.ZipInfo = zipfile.ZipInfo(arcname)
|
|
193
193
|
zip_info.compress_type = zipfile.ZIP_DEFLATED
|
|
194
194
|
zip_info.comment = file_hash.encode()
|
|
195
|
-
|
|
195
|
+
|
|
196
196
|
with open(source_path, "rb") as f:
|
|
197
197
|
with zipf.open(zip_info, "w", force_zip64=True) as zf:
|
|
198
198
|
for chunk in iter(lambda: f.read(4096), b""):
|
|
@@ -260,7 +260,7 @@ def consolidate_backups(zip_path: str, destination_zip: str) -> None:
|
|
|
260
260
|
and filename not in final_files \
|
|
261
261
|
and filename not in deleted_files:
|
|
262
262
|
final_files.add(filename)
|
|
263
|
-
|
|
263
|
+
|
|
264
264
|
# Copy file in chunks
|
|
265
265
|
with zipf_in.open(inf, "r") as source:
|
|
266
266
|
with zipf_out.open(inf, "w", force_zip64=True) as target:
|
|
@@ -19,7 +19,7 @@ def unique_list(list_to_clean: list[Any], method: Literal["id", "hash", "str"] =
|
|
|
19
19
|
method (Literal["id", "hash", "str"]): The method to use to identify duplicates
|
|
20
20
|
Returns:
|
|
21
21
|
list[Any]: The cleaned list
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
Examples:
|
|
24
24
|
>>> unique_list([1, 2, 3, 2, 1], method="id")
|
|
25
25
|
[1, 2, 3]
|
|
@@ -50,7 +50,7 @@ def load_credentials(credentials_path: str) -> dict[str, Any]:
|
|
|
50
50
|
# Check if the file exists
|
|
51
51
|
if not os.path.exists(credentials_path):
|
|
52
52
|
raise FileNotFoundError(f"Credentials file not found at '{credentials_path}'")
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
# Load the file if it's a JSON file
|
|
55
55
|
if credentials_path.endswith(".json"):
|
|
56
56
|
return super_json_load(credentials_path)
|
|
@@ -59,7 +59,7 @@ def load_credentials(credentials_path: str) -> dict[str, Any]:
|
|
|
59
59
|
elif credentials_path.endswith((".yml", ".yaml")):
|
|
60
60
|
with open(credentials_path, "r") as f:
|
|
61
61
|
return yaml.safe_load(f)
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
# Else, raise an error
|
|
64
64
|
else:
|
|
65
65
|
raise ValueError("Credentials file must be .json or .yml format")
|
|
@@ -87,7 +87,7 @@ def clean_version(version: str, keep: str = "") -> str:
|
|
|
87
87
|
keep (str): The characters to keep in the version string
|
|
88
88
|
Returns:
|
|
89
89
|
str: The cleaned version string
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
>>> clean_version("v1.e0.zfezf0.1.2.3zefz")
|
|
92
92
|
'1.0.0.1.2.3'
|
|
93
93
|
>>> clean_version("v1.e0.zfezf0.1.2.3zefz", keep="v")
|
|
@@ -107,7 +107,7 @@ def version_to_float(version: str) -> float:
|
|
|
107
107
|
version (str): The version string to convert. (e.g. "v1.0.0.1.2.3")
|
|
108
108
|
Returns:
|
|
109
109
|
float: The float representation of the version. (e.g. 0)
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
>>> version_to_float("v1.0.0")
|
|
112
112
|
1.0
|
|
113
113
|
>>> version_to_float("v1.0.0.1")
|
|
@@ -41,7 +41,7 @@ def validate_credentials(credentials: dict[str, dict[str, str]]) -> tuple[str, d
|
|
|
41
41
|
Returns:
|
|
42
42
|
tuple[str, dict[str, str]]:
|
|
43
43
|
str: Owner (the username of the account to use)
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
dict[str, str]: Headers (for the requests to the GitHub API)
|
|
46
46
|
"""
|
|
47
47
|
if "github" not in credentials:
|
|
@@ -50,7 +50,7 @@ def validate_credentials(credentials: dict[str, dict[str, str]]) -> tuple[str, d
|
|
|
50
50
|
raise ValueError("The credentials file must contain a 'github' key, which is a dictionary containing a 'api_key' key (a PAT for the GitHub API: https://github.com/settings/tokens) and a 'username' key (the username of the account to use)")
|
|
51
51
|
if "username" not in credentials["github"]:
|
|
52
52
|
raise ValueError("The credentials file must contain a 'github' key, which is a dictionary containing a 'api_key' key (a PAT for the GitHub API: https://github.com/settings/tokens) and a 'username' key (the username of the account to use)")
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
api_key: str = credentials["github"]["api_key"]
|
|
55
55
|
owner: str = credentials["github"]["username"]
|
|
56
56
|
headers: dict[str, str] = {"Authorization": f"Bearer {api_key}"}
|
|
@@ -64,11 +64,11 @@ def validate_config(github_config: dict[str, Any]) -> tuple[str, str, str, list[
|
|
|
64
64
|
Returns:
|
|
65
65
|
tuple[str, str, str, list[str]]:
|
|
66
66
|
str: Project name on GitHub
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
str: Version of the project
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
str: Build folder path containing zip files to upload to the release
|
|
71
|
-
|
|
71
|
+
|
|
72
72
|
list[str]: List of zip files to upload to the release
|
|
73
73
|
"""
|
|
74
74
|
if "project_name" not in github_config:
|
|
@@ -182,10 +182,10 @@ def get_commits_since_tag(owner: str, project_name: str, latest_tag_sha: str|Non
|
|
|
182
182
|
# Get the commits URL and parameters
|
|
183
183
|
commits_url: str = f"{PROJECT_ENDPOINT}/{owner}/{project_name}/commits"
|
|
184
184
|
commits_params: dict[str, str] = {"per_page": "100"}
|
|
185
|
-
|
|
185
|
+
|
|
186
186
|
# Initialize tag_date as None
|
|
187
187
|
tag_date: str|None = None # type: ignore
|
|
188
|
-
|
|
188
|
+
|
|
189
189
|
# If there is a latest tag, use it to get the commits since the tag date
|
|
190
190
|
if latest_tag_sha:
|
|
191
191
|
|
|
@@ -194,10 +194,10 @@ def get_commits_since_tag(owner: str, project_name: str, latest_tag_sha: str|Non
|
|
|
194
194
|
tag_response = requests.get(tag_commit_url, headers=headers)
|
|
195
195
|
handle_response(tag_response, "Failed to get tag commit")
|
|
196
196
|
tag_date: str = tag_response.json()["commit"]["committer"]["date"]
|
|
197
|
-
|
|
197
|
+
|
|
198
198
|
# Use the date as the 'since' parameter to get all commits after that date
|
|
199
199
|
commits_params["since"] = tag_date
|
|
200
|
-
|
|
200
|
+
|
|
201
201
|
# Get the commits
|
|
202
202
|
response = requests.get(commits_url, headers=headers, params=commits_params)
|
|
203
203
|
handle_response(response, "Failed to get commits")
|
|
@@ -210,7 +210,7 @@ def get_commits_since_tag(owner: str, project_name: str, latest_tag_sha: str|Non
|
|
|
210
210
|
|
|
211
211
|
def generate_changelog(commits: list[dict[str, Any]], owner: str, project_name: str, latest_tag_version: str|None, version: str) -> str:
|
|
212
212
|
""" Generate changelog from commits. They must follow the conventional commits convention.
|
|
213
|
-
|
|
213
|
+
|
|
214
214
|
Convention format: <type>: <description>
|
|
215
215
|
|
|
216
216
|
Args:
|
|
@@ -226,21 +226,21 @@ def generate_changelog(commits: list[dict[str, Any]], owner: str, project_name:
|
|
|
226
226
|
"""
|
|
227
227
|
# Initialize the commit groups
|
|
228
228
|
commit_groups: dict[str, list[tuple[str, str]]] = {}
|
|
229
|
-
|
|
229
|
+
|
|
230
230
|
# Iterate over the commits
|
|
231
231
|
for commit in commits:
|
|
232
232
|
message: str = commit["commit"]["message"].split("\n")[0]
|
|
233
233
|
sha: str = commit["sha"]
|
|
234
|
-
|
|
234
|
+
|
|
235
235
|
# If the message contains a colon, split the message into a type and a description
|
|
236
236
|
if ":" in message:
|
|
237
237
|
type_, desc = message.split(":", 1)
|
|
238
|
-
|
|
238
|
+
|
|
239
239
|
# Clean the type
|
|
240
240
|
type_ = type_.split('(')[0]
|
|
241
241
|
type_ = "".join(c for c in type_.lower().strip() if c in "abcdefghijklmnopqrstuvwxyz")
|
|
242
242
|
type_ = COMMIT_TYPES.get(type_, type_.title())
|
|
243
|
-
|
|
243
|
+
|
|
244
244
|
# Add the commit to the commit groups
|
|
245
245
|
if type_ not in commit_groups:
|
|
246
246
|
commit_groups[type_] = []
|
|
@@ -248,7 +248,7 @@ def generate_changelog(commits: list[dict[str, Any]], owner: str, project_name:
|
|
|
248
248
|
|
|
249
249
|
# Initialize the changelog
|
|
250
250
|
changelog: str = "## Changelog\n\n"
|
|
251
|
-
|
|
251
|
+
|
|
252
252
|
# Iterate over the commit groups
|
|
253
253
|
for type_ in sorted(commit_groups.keys()):
|
|
254
254
|
changelog += f"### {type_}\n"
|
|
@@ -257,10 +257,10 @@ def generate_changelog(commits: list[dict[str, Any]], owner: str, project_name:
|
|
|
257
257
|
for desc, sha in commit_groups[type_][::-1]:
|
|
258
258
|
changelog += f"- {desc} ([{sha[:7]}](https://github.com/{owner}/{project_name}/commit/{sha}))\n"
|
|
259
259
|
changelog += "\n"
|
|
260
|
-
|
|
260
|
+
|
|
261
261
|
# Add the full changelog link if there is a latest tag and return the changelog
|
|
262
262
|
if latest_tag_version:
|
|
263
|
-
changelog += f"**Full Changelog**: https://github.com/{owner}/{project_name}/compare/v{latest_tag_version}...v{version}\n"
|
|
263
|
+
changelog += f"**Full Changelog**: https://github.com/{owner}/{project_name}/compare/v{latest_tag_version}...v{version}\n"
|
|
264
264
|
return changelog
|
|
265
265
|
|
|
266
266
|
def create_tag(owner: str, project_name: str, version: str, headers: dict[str, str]) -> None:
|
|
@@ -276,12 +276,12 @@ def create_tag(owner: str, project_name: str, version: str, headers: dict[str, s
|
|
|
276
276
|
progress(f"Creating tag v{version}")
|
|
277
277
|
create_tag_url: str = f"{PROJECT_ENDPOINT}/{owner}/{project_name}/git/refs"
|
|
278
278
|
latest_commit_url: str = f"{PROJECT_ENDPOINT}/{owner}/{project_name}/git/refs/heads/main"
|
|
279
|
-
|
|
279
|
+
|
|
280
280
|
# Get the latest commit SHA
|
|
281
281
|
commit_response: requests.Response = requests.get(latest_commit_url, headers=headers)
|
|
282
282
|
handle_response(commit_response, "Failed to get latest commit")
|
|
283
283
|
commit_sha: str = commit_response.json()["object"]["sha"]
|
|
284
|
-
|
|
284
|
+
|
|
285
285
|
# Create the tag
|
|
286
286
|
tag_data: dict[str, str] = {
|
|
287
287
|
"ref": f"refs/tags/v{version}",
|
|
@@ -312,7 +312,7 @@ def create_release(owner: str, project_name: str, version: str, changelog: str,
|
|
|
312
312
|
"draft": False,
|
|
313
313
|
"prerelease": False
|
|
314
314
|
}
|
|
315
|
-
|
|
315
|
+
|
|
316
316
|
# Create the release and return the release ID
|
|
317
317
|
response: requests.Response = requests.post(release_url, headers=headers, json=release_data)
|
|
318
318
|
handle_response(response, "Failed to create release")
|
|
@@ -335,13 +335,13 @@ def upload_assets(owner: str, project_name: str, release_id: int, build_folder:
|
|
|
335
335
|
if not build_folder:
|
|
336
336
|
return
|
|
337
337
|
progress("Uploading assets")
|
|
338
|
-
|
|
338
|
+
|
|
339
339
|
# Get the release details
|
|
340
340
|
response: requests.Response = requests.get(f"{PROJECT_ENDPOINT}/{owner}/{project_name}/releases/{release_id}", headers=headers)
|
|
341
341
|
handle_response(response, "Failed to get release details")
|
|
342
342
|
upload_url_template: str = response.json()["upload_url"]
|
|
343
343
|
upload_url_base: str = upload_url_template.split("{", maxsplit=1)[0]
|
|
344
|
-
|
|
344
|
+
|
|
345
345
|
# Iterate over the files in the build folder
|
|
346
346
|
for file in os.listdir(build_folder):
|
|
347
347
|
if file.endswith(endswith_tuple):
|
|
@@ -354,7 +354,7 @@ def upload_assets(owner: str, project_name: str, release_id: int, build_folder:
|
|
|
354
354
|
"Content-Type": "application/zip"
|
|
355
355
|
}
|
|
356
356
|
params: dict[str, str] = {"name": file}
|
|
357
|
-
|
|
357
|
+
|
|
358
358
|
# Upload the file
|
|
359
359
|
response: requests.Response = requests.post(
|
|
360
360
|
upload_url_base,
|
|
@@ -400,17 +400,17 @@ def upload_to_github(credentials: dict[str, Any], github_config: dict[str, Any])
|
|
|
400
400
|
|
|
401
401
|
# Handle existing tag
|
|
402
402
|
can_create: bool = handle_existing_tag(owner, project_name, version, headers)
|
|
403
|
-
|
|
403
|
+
|
|
404
404
|
# Get the latest tag and commits since the tag
|
|
405
405
|
latest_tag_sha, latest_tag_version = get_latest_tag(owner, project_name, version, headers)
|
|
406
406
|
commits: list[dict[str, Any]] = get_commits_since_tag(owner, project_name, latest_tag_sha, headers)
|
|
407
407
|
changelog: str = generate_changelog(commits, owner, project_name, latest_tag_version, version)
|
|
408
|
-
|
|
408
|
+
|
|
409
409
|
# Create the tag and release if needed
|
|
410
410
|
if can_create:
|
|
411
411
|
create_tag(owner, project_name, version, headers)
|
|
412
412
|
release_id: int = create_release(owner, project_name, version, changelog, headers)
|
|
413
|
-
upload_assets(owner, project_name, release_id, build_folder, headers, endswith)
|
|
413
|
+
upload_assets(owner, project_name, release_id, build_folder, headers, endswith)
|
|
414
414
|
info(f"Project '{project_name}' updated on GitHub!")
|
|
415
415
|
return changelog
|
|
416
416
|
|
|
@@ -52,12 +52,12 @@ def format_toml_lists(content: str) -> str:
|
|
|
52
52
|
# Split into key and values parts
|
|
53
53
|
key, values = line.split("=", 1)
|
|
54
54
|
values = values.strip()
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
# Check if values portion is a list
|
|
57
57
|
if values.startswith("[") and values.endswith("]"):
|
|
58
58
|
# Parse list values, removing empty entries
|
|
59
59
|
values = [v.strip() for v in values[1:-1].split(",") if v.strip()]
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
# For lists with multiple items, format across multiple lines
|
|
62
62
|
if len(values) > 1:
|
|
63
63
|
formatted_lines.append(f"{key}= [")
|
|
@@ -12,15 +12,16 @@ This module provides context managers for temporarily silencing output.
|
|
|
12
12
|
import os
|
|
13
13
|
import sys
|
|
14
14
|
from typing import IO, TextIO, Callable, Any
|
|
15
|
-
from .print import
|
|
15
|
+
from .print import remove_colors
|
|
16
16
|
from .io import super_open
|
|
17
17
|
|
|
18
|
+
|
|
18
19
|
# Context manager to temporarily silence output
|
|
19
20
|
class Muffle:
|
|
20
21
|
""" Context manager that temporarily silences output.
|
|
21
22
|
|
|
22
23
|
Alternative to stouputils.decorators.silent()
|
|
23
|
-
|
|
24
|
+
|
|
24
25
|
Examples:
|
|
25
26
|
>>> with Muffle():
|
|
26
27
|
... print("This will not be printed")
|
|
@@ -54,6 +55,55 @@ class Muffle:
|
|
|
54
55
|
sys.stderr = self.original_stderr
|
|
55
56
|
|
|
56
57
|
|
|
58
|
+
# TeeMultiOutput class to duplicate output to multiple file-like objects
|
|
59
|
+
class TeeMultiOutput(object):
|
|
60
|
+
""" File-like object that duplicates output to multiple file objects.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
*files (IO[Any]): One or more file-like objects that have write and flush methods
|
|
64
|
+
strip_colors (bool): Whether to strip ANSI color codes from output sent to non-stdout/stderr files
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
>>> f = open("logfile.txt", "w")
|
|
68
|
+
>>> original_stdout = sys.stdout
|
|
69
|
+
>>> sys.stdout = TeeMultiOutput(sys.stdout, f)
|
|
70
|
+
>>> print("Hello World") # Output goes to both console and file
|
|
71
|
+
>>> sys.stdout = original_stdout
|
|
72
|
+
>>> f.close()
|
|
73
|
+
"""
|
|
74
|
+
def __init__(self, *files: IO[Any], strip_colors: bool = True) -> None:
|
|
75
|
+
self.files: tuple[IO[Any], ...] = files
|
|
76
|
+
""" File-like objects to write to """
|
|
77
|
+
self.strip_colors: bool = strip_colors
|
|
78
|
+
""" Whether to strip ANSI color codes from output sent to non-stdout/stderr files """
|
|
79
|
+
|
|
80
|
+
def write(self, obj: str) -> None:
|
|
81
|
+
""" Write the object to all files.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
obj (str): String to write
|
|
85
|
+
"""
|
|
86
|
+
for f in self.files:
|
|
87
|
+
# Strip colors for files that are not stdout or stderr
|
|
88
|
+
if self.strip_colors and f not in (sys.stdout, sys.stderr):
|
|
89
|
+
f.write(remove_colors(obj))
|
|
90
|
+
else:
|
|
91
|
+
f.write(obj)
|
|
92
|
+
|
|
93
|
+
def flush(self) -> None:
|
|
94
|
+
""" Flush all files. """
|
|
95
|
+
for f in self.files:
|
|
96
|
+
f.flush()
|
|
97
|
+
|
|
98
|
+
# Add other methods that might be expected from a file-like object
|
|
99
|
+
def isatty(self) -> bool:
|
|
100
|
+
""" Return whether the first file is connected to a tty-like device. """
|
|
101
|
+
return hasattr(self.files[0], 'isatty') and self.files[0].isatty()
|
|
102
|
+
|
|
103
|
+
def fileno(self) -> int:
|
|
104
|
+
""" Return the file descriptor of the first file. """
|
|
105
|
+
return self.files[0].fileno()
|
|
106
|
+
|
|
57
107
|
# Context manager to log to a file
|
|
58
108
|
class LogToFile:
|
|
59
109
|
""" Context manager to log to a file.
|
|
@@ -65,6 +115,8 @@ class LogToFile:
|
|
|
65
115
|
path (str): Path to the log file
|
|
66
116
|
mode (str): Mode to open the file in (default: "w")
|
|
67
117
|
encoding (str): Encoding to use for the file (default: "utf-8")
|
|
118
|
+
tee_stdout (bool): Whether to redirect stdout to the file (default: True)
|
|
119
|
+
tee_stderr (bool): Whether to redirect stderr to the file (default: True)
|
|
68
120
|
|
|
69
121
|
Examples:
|
|
70
122
|
.. code-block:: python
|
|
@@ -72,30 +124,54 @@ class LogToFile:
|
|
|
72
124
|
> import stouputils as stp
|
|
73
125
|
> with stp.LogToFile("output.log"):
|
|
74
126
|
> stp.info("This will be logged to output.log and printed normally")
|
|
127
|
+
> print("This will also be logged")
|
|
75
128
|
"""
|
|
76
|
-
def __init__(
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
path: str,
|
|
132
|
+
mode: str = "w",
|
|
133
|
+
encoding: str = "utf-8",
|
|
134
|
+
tee_stdout: bool = True,
|
|
135
|
+
tee_stderr: bool = True
|
|
136
|
+
) -> None:
|
|
77
137
|
self.path: str = path
|
|
78
138
|
""" Attribute remembering path to the log file """
|
|
79
139
|
self.mode: str = mode
|
|
80
140
|
""" Attribute remembering mode to open the file in """
|
|
81
141
|
self.encoding: str = encoding
|
|
82
142
|
""" Attribute remembering encoding to use for the file """
|
|
143
|
+
self.tee_stdout: bool = tee_stdout
|
|
144
|
+
""" Whether to redirect stdout to the file """
|
|
145
|
+
self.tee_stderr: bool = tee_stderr
|
|
146
|
+
""" Whether to redirect stderr to the file """
|
|
83
147
|
self.file: IO[Any] = super_open(self.path, mode=self.mode, encoding=self.encoding)
|
|
84
148
|
""" Attribute remembering opened file """
|
|
149
|
+
self.original_stdout: TextIO = sys.stdout
|
|
150
|
+
""" Original stdout before redirection """
|
|
151
|
+
self.original_stderr: TextIO = sys.stderr
|
|
152
|
+
""" Original stderr before redirection """
|
|
85
153
|
|
|
86
154
|
def __enter__(self) -> None:
|
|
87
|
-
""" Enter context manager which opens the log file and
|
|
88
|
-
#
|
|
89
|
-
|
|
155
|
+
""" Enter context manager which opens the log file and redirects stdout/stderr """
|
|
156
|
+
# Redirect stdout and stderr if requested
|
|
157
|
+
if self.tee_stdout:
|
|
158
|
+
sys.stdout = TeeMultiOutput(self.original_stdout, self.file)
|
|
159
|
+
|
|
160
|
+
if self.tee_stderr:
|
|
161
|
+
sys.stderr = TeeMultiOutput(self.original_stderr, self.file)
|
|
90
162
|
|
|
91
163
|
def __exit__(self, exc_type: type[BaseException]|None, exc_val: BaseException|None, exc_tb: Any|None) -> None:
|
|
92
|
-
""" Exit context manager which closes the log file and
|
|
164
|
+
""" Exit context manager which closes the log file and restores stdout/stderr """
|
|
165
|
+
# Restore original stdout and stderr
|
|
166
|
+
if self.tee_stdout:
|
|
167
|
+
sys.stdout = self.original_stdout
|
|
168
|
+
|
|
169
|
+
if self.tee_stderr:
|
|
170
|
+
sys.stderr = self.original_stderr
|
|
171
|
+
|
|
93
172
|
# Close file
|
|
94
173
|
self.file.close()
|
|
95
174
|
|
|
96
|
-
# Remove file from logging_to list
|
|
97
|
-
logging_to.discard(self.file)
|
|
98
|
-
|
|
99
175
|
@staticmethod
|
|
100
176
|
def common(logs_folder: str, filepath: str, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
|
|
101
177
|
""" Common code used at the beginning of a program to launch main function
|
|
@@ -108,7 +184,7 @@ class LogToFile:
|
|
|
108
184
|
**kwargs (dict[str, Any]): Keyword arguments to pass to the main function
|
|
109
185
|
Returns:
|
|
110
186
|
Any: Return value of the main function
|
|
111
|
-
|
|
187
|
+
|
|
112
188
|
Examples:
|
|
113
189
|
>>> if __name__ == "__main__":
|
|
114
190
|
... LogToFile.common(f"{ROOT}/logs", __file__, main)
|
|
@@ -86,7 +86,7 @@ def measure_time(
|
|
|
86
86
|
perf_counter (bool): Whether to use time.perf_counter_ns or time.time_ns
|
|
87
87
|
Returns:
|
|
88
88
|
Callable: Decorator to measure the time of the function.
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
Examples:
|
|
91
91
|
.. code-block:: python
|
|
92
92
|
|
|
@@ -171,7 +171,7 @@ def handle_error(
|
|
|
171
171
|
LogLevels.WARNING_TRACEBACK: Show as warning with traceback
|
|
172
172
|
LogLevels.ERROR_TRACEBACK: Show as error with traceback
|
|
173
173
|
LogLevels.RAISE_EXCEPTION: Raise exception (as if the decorator didn't exist)
|
|
174
|
-
|
|
174
|
+
|
|
175
175
|
Examples:
|
|
176
176
|
.. code-block:: python
|
|
177
177
|
|
|
@@ -179,7 +179,7 @@ def handle_error(
|
|
|
179
179
|
> def test():
|
|
180
180
|
> raise ValueError("Let's fail")
|
|
181
181
|
> test() # [WARNING HH:MM:SS] Error during test: (ValueError) Let's fail
|
|
182
|
-
"""
|
|
182
|
+
"""
|
|
183
183
|
# Update error_log if needed
|
|
184
184
|
if force_raise_exception:
|
|
185
185
|
error_log = LogLevels.RAISE_EXCEPTION
|
|
@@ -279,7 +279,7 @@ def deprecated(
|
|
|
279
279
|
LogLevels.RAISE_EXCEPTION: Raise exception
|
|
280
280
|
Returns:
|
|
281
281
|
Callable[..., Any]: Decorator that marks a function as deprecated
|
|
282
|
-
|
|
282
|
+
|
|
283
283
|
Examples:
|
|
284
284
|
.. code-block:: python
|
|
285
285
|
|
|
@@ -15,7 +15,7 @@ def image_resize(
|
|
|
15
15
|
) -> Any:
|
|
16
16
|
""" Resize an image while preserving its aspect ratio by default.
|
|
17
17
|
Scales the image so that its largest dimension equals max_result_size.
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
Args:
|
|
20
20
|
image (Image.Image | np.ndarray): The image to resize.
|
|
21
21
|
max_result_size (int): Maximum size for the largest dimension.
|
|
@@ -54,7 +54,7 @@ def image_resize(
|
|
|
54
54
|
# Convert numpy array to PIL Image if needed
|
|
55
55
|
if isinstance(image, np.ndarray):
|
|
56
56
|
image = Image.fromarray(image)
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
if keep_aspect_ratio:
|
|
59
59
|
|
|
60
60
|
# Get original image dimensions
|
|
@@ -66,7 +66,7 @@ def image_resize(
|
|
|
66
66
|
|
|
67
67
|
# Calculate scaling factor
|
|
68
68
|
scale: float = max_result_size / max_dimension
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
# Calculate new dimensions while preserving aspect ratio
|
|
71
71
|
new_width: int = int(width * scale)
|
|
72
72
|
new_height: int = int(height * scale)
|
|
@@ -76,7 +76,7 @@ def image_resize(
|
|
|
76
76
|
else:
|
|
77
77
|
# If not keeping aspect ratio, resize to square with max_result_size
|
|
78
78
|
new_image: Image.Image = image.resize((max_result_size, max_result_size), resampling)
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
# Return the image in the requested format
|
|
81
81
|
if return_type == np.ndarray:
|
|
82
82
|
return np.array(new_image)
|
|
@@ -183,7 +183,7 @@ def super_json_dump(data: Any, file: IO[Any]|None = None, max_level: int = 2, in
|
|
|
183
183
|
indent (str): The indentation character (default: '\t')
|
|
184
184
|
Returns:
|
|
185
185
|
str: The content of the file in every case
|
|
186
|
-
|
|
186
|
+
|
|
187
187
|
>>> super_json_dump({"a": [[1,2,3]], "b": 2}, max_level = 2)
|
|
188
188
|
'{\\n\\t"a": [\\n\\t\\t[1,2,3]\\n\\t],\\n\\t"b": 2\\n}\\n'
|
|
189
189
|
"""
|
|
@@ -209,7 +209,7 @@ def super_json_dump(data: Any, file: IO[Any]|None = None, max_level: int = 2, in
|
|
|
209
209
|
for char in finishes:
|
|
210
210
|
to_replace: str = "\n" + indent * max_level + char
|
|
211
211
|
content = content.replace(to_replace, char)
|
|
212
|
-
|
|
212
|
+
|
|
213
213
|
# Write file content and return it
|
|
214
214
|
content += "\n"
|
|
215
215
|
if file:
|
|
@@ -72,7 +72,7 @@ def __handle_parameters(
|
|
|
72
72
|
if use_starmap:
|
|
73
73
|
args = [(func, arg) for arg in args] # type: ignore
|
|
74
74
|
func = __starmap # type: ignore
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
# Prepare delayed function calls if delay_first_calls is set
|
|
77
77
|
if delay_first_calls > 0:
|
|
78
78
|
args = [
|
|
@@ -80,7 +80,7 @@ def __handle_parameters(
|
|
|
80
80
|
for i, arg in enumerate(args)
|
|
81
81
|
]
|
|
82
82
|
func = __delayed_call # type: ignore
|
|
83
|
-
|
|
83
|
+
|
|
84
84
|
return desc, func, args
|
|
85
85
|
|
|
86
86
|
@handle_error(error_log=LogLevels.ERROR_TRACEBACK)
|
|
@@ -10,7 +10,7 @@ If a message is printed multiple times, it will be displayed as "(xN) message" w
|
|
|
10
10
|
# Imports
|
|
11
11
|
import sys
|
|
12
12
|
import time
|
|
13
|
-
from typing import Callable, TextIO,
|
|
13
|
+
from typing import Callable, TextIO, Any
|
|
14
14
|
|
|
15
15
|
# Colors constants
|
|
16
16
|
RESET: str = "\033[0m"
|
|
@@ -22,9 +22,6 @@ MAGENTA: str = "\033[95m"
|
|
|
22
22
|
CYAN: str = "\033[96m"
|
|
23
23
|
LINE_UP: str = "\033[1A"
|
|
24
24
|
|
|
25
|
-
# Logging utilities
|
|
26
|
-
logging_to: set[IO[Any]] = set() # Used by LogToFile context manager
|
|
27
|
-
|
|
28
25
|
def remove_colors(text: str) -> str:
|
|
29
26
|
""" Remove the colors from a text """
|
|
30
27
|
for color in [RESET, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, LINE_UP]:
|
|
@@ -65,22 +62,24 @@ def info(*values: Any, color: str = GREEN, text: str = "INFO ", prefix: str = ""
|
|
|
65
62
|
file (TextIO|list[TextIO]): File(s) to write the message to (default: sys.stdout)
|
|
66
63
|
print_kwargs (dict): Keyword arguments to pass to the print function
|
|
67
64
|
"""
|
|
65
|
+
# Use stdout if no file is specified
|
|
68
66
|
if file is None:
|
|
69
67
|
file = sys.stdout
|
|
68
|
+
|
|
69
|
+
# If file is a list, recursively call info() for each file
|
|
70
70
|
if isinstance(file, list):
|
|
71
71
|
for f in file:
|
|
72
72
|
info(*values, color=color, text=text, prefix=prefix, file=f, **print_kwargs)
|
|
73
73
|
else:
|
|
74
|
+
# Build the message with prefix, color, text and timestamp
|
|
74
75
|
message: str = f"{prefix}{color}[{text} {current_time()}]"
|
|
76
|
+
|
|
77
|
+
# If this is a repeated print, add a line up and counter
|
|
75
78
|
if is_same_print(*values, color=color, text=text, prefix=prefix, **print_kwargs):
|
|
76
79
|
message = f"{LINE_UP}{message} (x{nb_values})"
|
|
77
80
|
|
|
78
|
-
# Print
|
|
81
|
+
# Print the message with the values and reset color
|
|
79
82
|
print(message, *values, RESET, file=file, **print_kwargs)
|
|
80
|
-
if logging_to:
|
|
81
|
-
print_kwargs["flush"] = True
|
|
82
|
-
for log_file in logging_to:
|
|
83
|
-
print(remove_colors(message), *(remove_colors(str(v)) for v in values), file=log_file, **print_kwargs)
|
|
84
83
|
|
|
85
84
|
def debug(*values: Any, **print_kwargs: Any) -> None:
|
|
86
85
|
""" Print a debug message looking like "[DEBUG HH:MM:SS] message" in cyan by default. """
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|