stouputils 1.12.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.
- stouputils/__init__.py +40 -0
- stouputils/__init__.pyi +14 -0
- stouputils/__main__.py +81 -0
- stouputils/_deprecated.py +37 -0
- stouputils/_deprecated.pyi +12 -0
- stouputils/all_doctests.py +160 -0
- stouputils/all_doctests.pyi +46 -0
- stouputils/applications/__init__.py +22 -0
- stouputils/applications/__init__.pyi +2 -0
- stouputils/applications/automatic_docs.py +634 -0
- stouputils/applications/automatic_docs.pyi +106 -0
- stouputils/applications/upscaler/__init__.py +39 -0
- stouputils/applications/upscaler/__init__.pyi +3 -0
- stouputils/applications/upscaler/config.py +128 -0
- stouputils/applications/upscaler/config.pyi +18 -0
- stouputils/applications/upscaler/image.py +247 -0
- stouputils/applications/upscaler/image.pyi +109 -0
- stouputils/applications/upscaler/video.py +287 -0
- stouputils/applications/upscaler/video.pyi +60 -0
- stouputils/archive.py +344 -0
- stouputils/archive.pyi +67 -0
- stouputils/backup.py +488 -0
- stouputils/backup.pyi +109 -0
- stouputils/collections.py +244 -0
- stouputils/collections.pyi +86 -0
- stouputils/continuous_delivery/__init__.py +27 -0
- stouputils/continuous_delivery/__init__.pyi +5 -0
- stouputils/continuous_delivery/cd_utils.py +243 -0
- stouputils/continuous_delivery/cd_utils.pyi +129 -0
- stouputils/continuous_delivery/github.py +522 -0
- stouputils/continuous_delivery/github.pyi +162 -0
- stouputils/continuous_delivery/pypi.py +91 -0
- stouputils/continuous_delivery/pypi.pyi +43 -0
- stouputils/continuous_delivery/pyproject.py +147 -0
- stouputils/continuous_delivery/pyproject.pyi +67 -0
- stouputils/continuous_delivery/stubs.py +86 -0
- stouputils/continuous_delivery/stubs.pyi +39 -0
- stouputils/ctx.py +408 -0
- stouputils/ctx.pyi +211 -0
- stouputils/data_science/config/get.py +51 -0
- stouputils/data_science/config/set.py +125 -0
- stouputils/data_science/data_processing/image/__init__.py +66 -0
- stouputils/data_science/data_processing/image/auto_contrast.py +79 -0
- stouputils/data_science/data_processing/image/axis_flip.py +58 -0
- stouputils/data_science/data_processing/image/bias_field_correction.py +74 -0
- stouputils/data_science/data_processing/image/binary_threshold.py +73 -0
- stouputils/data_science/data_processing/image/blur.py +59 -0
- stouputils/data_science/data_processing/image/brightness.py +54 -0
- stouputils/data_science/data_processing/image/canny.py +110 -0
- stouputils/data_science/data_processing/image/clahe.py +92 -0
- stouputils/data_science/data_processing/image/common.py +30 -0
- stouputils/data_science/data_processing/image/contrast.py +53 -0
- stouputils/data_science/data_processing/image/curvature_flow_filter.py +74 -0
- stouputils/data_science/data_processing/image/denoise.py +378 -0
- stouputils/data_science/data_processing/image/histogram_equalization.py +123 -0
- stouputils/data_science/data_processing/image/invert.py +64 -0
- stouputils/data_science/data_processing/image/laplacian.py +60 -0
- stouputils/data_science/data_processing/image/median_blur.py +52 -0
- stouputils/data_science/data_processing/image/noise.py +59 -0
- stouputils/data_science/data_processing/image/normalize.py +65 -0
- stouputils/data_science/data_processing/image/random_erase.py +66 -0
- stouputils/data_science/data_processing/image/resize.py +69 -0
- stouputils/data_science/data_processing/image/rotation.py +80 -0
- stouputils/data_science/data_processing/image/salt_pepper.py +68 -0
- stouputils/data_science/data_processing/image/sharpening.py +55 -0
- stouputils/data_science/data_processing/image/shearing.py +64 -0
- stouputils/data_science/data_processing/image/threshold.py +64 -0
- stouputils/data_science/data_processing/image/translation.py +71 -0
- stouputils/data_science/data_processing/image/zoom.py +83 -0
- stouputils/data_science/data_processing/image_augmentation.py +118 -0
- stouputils/data_science/data_processing/image_preprocess.py +183 -0
- stouputils/data_science/data_processing/prosthesis_detection.py +359 -0
- stouputils/data_science/data_processing/technique.py +481 -0
- stouputils/data_science/dataset/__init__.py +45 -0
- stouputils/data_science/dataset/dataset.py +292 -0
- stouputils/data_science/dataset/dataset_loader.py +135 -0
- stouputils/data_science/dataset/grouping_strategy.py +296 -0
- stouputils/data_science/dataset/image_loader.py +100 -0
- stouputils/data_science/dataset/xy_tuple.py +696 -0
- stouputils/data_science/metric_dictionnary.py +106 -0
- stouputils/data_science/metric_utils.py +847 -0
- stouputils/data_science/mlflow_utils.py +206 -0
- stouputils/data_science/models/abstract_model.py +149 -0
- stouputils/data_science/models/all.py +85 -0
- stouputils/data_science/models/base_keras.py +765 -0
- stouputils/data_science/models/keras/all.py +38 -0
- stouputils/data_science/models/keras/convnext.py +62 -0
- stouputils/data_science/models/keras/densenet.py +50 -0
- stouputils/data_science/models/keras/efficientnet.py +60 -0
- stouputils/data_science/models/keras/mobilenet.py +56 -0
- stouputils/data_science/models/keras/resnet.py +52 -0
- stouputils/data_science/models/keras/squeezenet.py +233 -0
- stouputils/data_science/models/keras/vgg.py +42 -0
- stouputils/data_science/models/keras/xception.py +38 -0
- stouputils/data_science/models/keras_utils/callbacks/__init__.py +20 -0
- stouputils/data_science/models/keras_utils/callbacks/colored_progress_bar.py +219 -0
- stouputils/data_science/models/keras_utils/callbacks/learning_rate_finder.py +148 -0
- stouputils/data_science/models/keras_utils/callbacks/model_checkpoint_v2.py +31 -0
- stouputils/data_science/models/keras_utils/callbacks/progressive_unfreezing.py +249 -0
- stouputils/data_science/models/keras_utils/callbacks/warmup_scheduler.py +66 -0
- stouputils/data_science/models/keras_utils/losses/__init__.py +12 -0
- stouputils/data_science/models/keras_utils/losses/next_generation_loss.py +56 -0
- stouputils/data_science/models/keras_utils/visualizations.py +416 -0
- stouputils/data_science/models/model_interface.py +939 -0
- stouputils/data_science/models/sandbox.py +116 -0
- stouputils/data_science/range_tuple.py +234 -0
- stouputils/data_science/scripts/augment_dataset.py +77 -0
- stouputils/data_science/scripts/exhaustive_process.py +133 -0
- stouputils/data_science/scripts/preprocess_dataset.py +70 -0
- stouputils/data_science/scripts/routine.py +168 -0
- stouputils/data_science/utils.py +285 -0
- stouputils/decorators.py +595 -0
- stouputils/decorators.pyi +242 -0
- stouputils/image.py +441 -0
- stouputils/image.pyi +172 -0
- stouputils/installer/__init__.py +18 -0
- stouputils/installer/__init__.pyi +5 -0
- stouputils/installer/common.py +67 -0
- stouputils/installer/common.pyi +39 -0
- stouputils/installer/downloader.py +101 -0
- stouputils/installer/downloader.pyi +24 -0
- stouputils/installer/linux.py +144 -0
- stouputils/installer/linux.pyi +39 -0
- stouputils/installer/main.py +223 -0
- stouputils/installer/main.pyi +57 -0
- stouputils/installer/windows.py +136 -0
- stouputils/installer/windows.pyi +31 -0
- stouputils/io.py +486 -0
- stouputils/io.pyi +213 -0
- stouputils/parallel.py +453 -0
- stouputils/parallel.pyi +211 -0
- stouputils/print.py +527 -0
- stouputils/print.pyi +146 -0
- stouputils/py.typed +1 -0
- stouputils-1.12.1.dist-info/METADATA +179 -0
- stouputils-1.12.1.dist-info/RECORD +138 -0
- stouputils-1.12.1.dist-info/WHEEL +4 -0
- stouputils-1.12.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from ..decorators import handle_error as handle_error
|
|
3
|
+
from ..io import clean_path as clean_path, json_load as json_load
|
|
4
|
+
from ..print import warning as warning
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
def load_credentials(credentials_path: str) -> dict[str, Any]:
|
|
8
|
+
''' Load credentials from a JSON or YAML file into a dictionary.
|
|
9
|
+
|
|
10
|
+
\tLoads credentials from either a JSON or YAML file and returns them as a dictionary.
|
|
11
|
+
\tThe file must contain the required credentials in the appropriate format.
|
|
12
|
+
|
|
13
|
+
\tArgs:
|
|
14
|
+
\t\tcredentials_path (str): Path to the credentials file (.json or .yml)
|
|
15
|
+
\tReturns:
|
|
16
|
+
\t\tdict[str, Any]: Dictionary containing the credentials
|
|
17
|
+
|
|
18
|
+
\tExample JSON format:
|
|
19
|
+
|
|
20
|
+
\t.. code-block:: json
|
|
21
|
+
|
|
22
|
+
\t\t{
|
|
23
|
+
\t\t\t"github": {
|
|
24
|
+
\t\t\t\t"username": "Stoupy51",
|
|
25
|
+
\t\t\t\t"api_key": "ghp_XXXXXXXXXXXXXXXXXXXXXXXXXX"
|
|
26
|
+
\t\t\t}
|
|
27
|
+
\t\t}
|
|
28
|
+
|
|
29
|
+
\tExample YAML format:
|
|
30
|
+
|
|
31
|
+
\t.. code-block:: yaml
|
|
32
|
+
|
|
33
|
+
\t\tgithub:
|
|
34
|
+
\t\t\tusername: "Stoupy51"
|
|
35
|
+
\t\t\tapi_key: "ghp_XXXXXXXXXXXXXXXXXXXXXXXXXX"
|
|
36
|
+
\t'''
|
|
37
|
+
def handle_response(response: requests.Response, error_message: str) -> None:
|
|
38
|
+
""" Handle a response from the API by raising an error if the response is not successful (status code not in 200-299).
|
|
39
|
+
|
|
40
|
+
\tArgs:
|
|
41
|
+
\t\tresponse\t\t(requests.Response): The response from the API
|
|
42
|
+
\t\terror_message\t(str): The error message to raise if the response is not successful
|
|
43
|
+
\t"""
|
|
44
|
+
def clean_version(version: str, keep: str = '') -> str:
|
|
45
|
+
''' Clean a version string
|
|
46
|
+
|
|
47
|
+
\tArgs:
|
|
48
|
+
\t\tversion\t(str): The version string to clean
|
|
49
|
+
\t\tkeep\t(str): The characters to keep in the version string
|
|
50
|
+
\tReturns:
|
|
51
|
+
\t\tstr: The cleaned version string
|
|
52
|
+
|
|
53
|
+
\t>>> clean_version("v1.e0.zfezf0.1.2.3zefz")
|
|
54
|
+
\t\'1.0.0.1.2.3\'
|
|
55
|
+
\t>>> clean_version("v1.e0.zfezf0.1.2.3zefz", keep="v")
|
|
56
|
+
\t\'v1.0.0.1.2.3\'
|
|
57
|
+
\t>>> clean_version("v1.2.3b", keep="ab")
|
|
58
|
+
\t\'1.2.3b\'
|
|
59
|
+
\t'''
|
|
60
|
+
def version_to_float(version: str, error: bool = True) -> Any:
|
|
61
|
+
''' Converts a version string into a float for comparison purposes.
|
|
62
|
+
\tThe version string is expected to follow the format of major.minor.patch.something_else....,
|
|
63
|
+
\twhere each part is separated by a dot and can be extended indefinitely.
|
|
64
|
+
\tSupports pre-release suffixes with numbers: devN/dN (dev), aN (alpha), bN (beta), rcN/cN (release candidate).
|
|
65
|
+
\tOrdering: 1.0.0 > 1.0.0rc2 > 1.0.0rc1 > 1.0.0b2 > 1.0.0b1 > 1.0.0a2 > 1.0.0a1 > 1.0.0dev1
|
|
66
|
+
|
|
67
|
+
\tArgs:
|
|
68
|
+
\t\tversion (str): The version string to convert. (e.g. "v1.0.0.1.2.3", "v2.0.0b2", "v1.0.0rc1")
|
|
69
|
+
\t\terror (bool): Return None on error instead of raising an exception
|
|
70
|
+
\tReturns:
|
|
71
|
+
\t\tfloat: The float representation of the version. (e.g. 0)
|
|
72
|
+
|
|
73
|
+
\t>>> version_to_float("v1.0.0")
|
|
74
|
+
\t1.0
|
|
75
|
+
\t>>> version_to_float("v1.0.0.1")
|
|
76
|
+
\t1.000000001
|
|
77
|
+
\t>>> version_to_float("v2.3.7")
|
|
78
|
+
\t2.003007
|
|
79
|
+
\t>>> version_to_float("v1.0.0.1.2.3")
|
|
80
|
+
\t1.0000000010020031
|
|
81
|
+
\t>>> version_to_float("v2.0") > version_to_float("v1.0.0.1")
|
|
82
|
+
\tTrue
|
|
83
|
+
\t>>> version_to_float("v2.0.0") > version_to_float("v2.0.0rc") > version_to_float("v2.0.0b") > version_to_float("v2.0.0a") > version_to_float("v2.0.0dev")
|
|
84
|
+
\tTrue
|
|
85
|
+
\t>>> version_to_float("v1.0.0b") > version_to_float("v1.0.0a")
|
|
86
|
+
\tTrue
|
|
87
|
+
\t>>> version_to_float("v1.0.0") > version_to_float("v1.0.0b")
|
|
88
|
+
\tTrue
|
|
89
|
+
\t>>> version_to_float("v3.0.0a") > version_to_float("v2.9.9")
|
|
90
|
+
\tTrue
|
|
91
|
+
\t>>> version_to_float("v1.2.3b") < version_to_float("v1.2.3")
|
|
92
|
+
\tTrue
|
|
93
|
+
\t>>> version_to_float("1.0.0") == version_to_float("v1.0.0")
|
|
94
|
+
\tTrue
|
|
95
|
+
\t>>> version_to_float("2.0.0.0.0.0.1b") > version_to_float("2.0.0.0.0.0.1a")
|
|
96
|
+
\tTrue
|
|
97
|
+
\t>>> version_to_float("2.0.0.0.0.0.1") > version_to_float("2.0.0.0.0.0.1b")
|
|
98
|
+
\tTrue
|
|
99
|
+
\t>>> version_to_float("v1.0.0rc") == version_to_float("v1.0.0c")
|
|
100
|
+
\tTrue
|
|
101
|
+
\t>>> version_to_float("v1.0.0c") > version_to_float("v1.0.0b")
|
|
102
|
+
\tTrue
|
|
103
|
+
\t>>> version_to_float("v1.0.0d") < version_to_float("v1.0.0a")
|
|
104
|
+
\tTrue
|
|
105
|
+
\t>>> version_to_float("v1.0.0dev") < version_to_float("v1.0.0a")
|
|
106
|
+
\tTrue
|
|
107
|
+
\t>>> version_to_float("v1.0.0dev") == version_to_float("v1.0.0d")
|
|
108
|
+
\tTrue
|
|
109
|
+
\t>>> version_to_float("v1.0.0rc2") > version_to_float("v1.0.0rc1")
|
|
110
|
+
\tTrue
|
|
111
|
+
\t>>> version_to_float("v1.0.0b2") > version_to_float("v1.0.0b1")
|
|
112
|
+
\tTrue
|
|
113
|
+
\t>>> version_to_float("v1.0.0a2") > version_to_float("v1.0.0a1")
|
|
114
|
+
\tTrue
|
|
115
|
+
\t>>> version_to_float("v1.0.0dev2") > version_to_float("v1.0.0dev1")
|
|
116
|
+
\tTrue
|
|
117
|
+
\t>>> version_to_float("v1.0.0") > version_to_float("v1.0.0rc2") > version_to_float("v1.0.0rc1")
|
|
118
|
+
\tTrue
|
|
119
|
+
\t>>> version_to_float("v1.0.0rc1") > version_to_float("v1.0.0b2")
|
|
120
|
+
\tTrue
|
|
121
|
+
\t>>> version_to_float("v1.0.0b1") > version_to_float("v1.0.0a2")
|
|
122
|
+
\tTrue
|
|
123
|
+
\t>>> version_to_float("v1.0.0a1") > version_to_float("v1.0.0dev2")
|
|
124
|
+
\tTrue
|
|
125
|
+
\t>>> versions = ["v1.0.0", "v1.0.0rc2", "v1.0.0rc1", "v1.0.0b2", "v1.0.0b1", "v1.0.0a2", "v1.0.0a1", "v1.0.0dev2", "v1.0.0dev1"]
|
|
126
|
+
\t>>> sorted_versions = sorted(versions, key=version_to_float, reverse=True)
|
|
127
|
+
\t>>> sorted_versions == versions
|
|
128
|
+
\tTrue
|
|
129
|
+
\t'''
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
""" This module contains utilities for continuous delivery on GitHub.
|
|
2
|
+
|
|
3
|
+
- upload_to_github: Upload the project to GitHub using the credentials and the configuration
|
|
4
|
+
(make a release and upload the assets, handle existing tag, generate changelog, etc.)
|
|
5
|
+
|
|
6
|
+
.. image:: https://raw.githubusercontent.com/Stoupy51/stouputils/refs/heads/main/assets/continuous_delivery/github_module.gif
|
|
7
|
+
:alt: stouputils upload_to_github examples
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Imports
|
|
11
|
+
import os
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ..decorators import handle_error, measure_time
|
|
15
|
+
from ..io import clean_path
|
|
16
|
+
from ..print import info, progress, warning
|
|
17
|
+
from .cd_utils import clean_version, handle_response, version_to_float
|
|
18
|
+
|
|
19
|
+
# Constants
|
|
20
|
+
GITHUB_API_URL: str = "https://api.github.com"
|
|
21
|
+
PROJECT_ENDPOINT: str = f"{GITHUB_API_URL}/repos"
|
|
22
|
+
COMMIT_TYPES: dict[str, str] = {
|
|
23
|
+
"feat": "Features",
|
|
24
|
+
"fix": "Bug Fixes",
|
|
25
|
+
"docs": "Documentation",
|
|
26
|
+
"style": "Style",
|
|
27
|
+
"refactor": "Code Refactoring",
|
|
28
|
+
"perf": "Performance Improvements",
|
|
29
|
+
"test": "Tests",
|
|
30
|
+
"build": "Build System",
|
|
31
|
+
"ci": "CI/CD",
|
|
32
|
+
"chore": "Chores",
|
|
33
|
+
"revert": "Reverts",
|
|
34
|
+
"uwu": "UwU ༼ つ ◕_◕ ༽つ",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def validate_credentials(credentials: dict[str, dict[str, str]]) -> tuple[str, dict[str, str]]:
|
|
38
|
+
""" Get and validate GitHub credentials
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
credentials (dict[str, dict[str, str]]): Credentials for the GitHub API
|
|
42
|
+
Returns:
|
|
43
|
+
tuple[str, dict[str, str]]:
|
|
44
|
+
str: Owner (the username of the account to use)
|
|
45
|
+
|
|
46
|
+
dict[str, str]: Headers (for the requests to the GitHub API)
|
|
47
|
+
"""
|
|
48
|
+
if "github" not in credentials:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
"The credentials file must contain a 'github' key, which is a dictionary containing a 'api_key' key"
|
|
51
|
+
"(a PAT for the GitHub API: https://github.com/settings/tokens) "
|
|
52
|
+
"and a 'username' key (the username of the account to use)"
|
|
53
|
+
)
|
|
54
|
+
if "api_key" not in credentials["github"]:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
"The credentials file must contain a 'github' key, which is a dictionary containing a 'api_key' key"
|
|
57
|
+
"(a PAT for the GitHub API: https://github.com/settings/tokens) "
|
|
58
|
+
"and a 'username' key (the username of the account to use)"
|
|
59
|
+
)
|
|
60
|
+
if "username" not in credentials["github"]:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"The credentials file must contain a 'github' key, which is a dictionary containing a 'api_key' key"
|
|
63
|
+
"(a PAT for the GitHub API: https://github.com/settings/tokens) "
|
|
64
|
+
"and a 'username' key (the username of the account to use)"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
api_key: str = credentials["github"]["api_key"]
|
|
68
|
+
owner: str = credentials["github"]["username"]
|
|
69
|
+
headers: dict[str, str] = {"Authorization": f"Bearer {api_key}"}
|
|
70
|
+
return owner, headers
|
|
71
|
+
|
|
72
|
+
def validate_config(github_config: dict[str, Any]) -> tuple[str, str, str, list[str]]:
|
|
73
|
+
""" Validate GitHub configuration
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
github_config (dict[str, str]): Configuration for the GitHub project
|
|
77
|
+
Returns:
|
|
78
|
+
tuple[str, str, str, list[str]]:
|
|
79
|
+
str: Project name on GitHub
|
|
80
|
+
|
|
81
|
+
str: Version of the project
|
|
82
|
+
|
|
83
|
+
str: Build folder path containing zip files to upload to the release
|
|
84
|
+
|
|
85
|
+
list[str]: List of zip files to upload to the release
|
|
86
|
+
"""
|
|
87
|
+
if "project_name" not in github_config:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
"The github_config file must contain a 'project_name' key, "
|
|
90
|
+
"which is the name of the project on GitHub"
|
|
91
|
+
)
|
|
92
|
+
if "version" not in github_config:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
"The github_config file must contain a 'version' key, "
|
|
95
|
+
"which is the version of the project"
|
|
96
|
+
)
|
|
97
|
+
if "build_folder" not in github_config:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"The github_config file must contain a 'build_folder' key, "
|
|
100
|
+
"which is the folder containing the build of the project "
|
|
101
|
+
"(datapack and resourcepack zip files)"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
project_name: str = github_config["project_name"]
|
|
105
|
+
version: str = github_config["version"]
|
|
106
|
+
build_folder: str = github_config["build_folder"]
|
|
107
|
+
endswith: list[str] = github_config.get("endswith", [])
|
|
108
|
+
|
|
109
|
+
return project_name, version, build_folder, endswith
|
|
110
|
+
|
|
111
|
+
def handle_existing_tag(owner: str, project_name: str, version: str, headers: dict[str, str]) -> bool:
|
|
112
|
+
""" Check if tag exists and handle deletion if needed
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
owner (str): GitHub username
|
|
116
|
+
project_name (str): Name of the GitHub repository
|
|
117
|
+
version (str): Version to check for existing tag
|
|
118
|
+
headers (dict[str, str]): Headers for GitHub API requests
|
|
119
|
+
Returns:
|
|
120
|
+
bool: True if the tag was deleted or if it was not found, False otherwise
|
|
121
|
+
"""
|
|
122
|
+
# Get the tag URL and check if it exists
|
|
123
|
+
import requests
|
|
124
|
+
tag_url: str = f"{PROJECT_ENDPOINT}/{owner}/{project_name}/git/refs/tags/v{version}"
|
|
125
|
+
response: requests.Response = requests.get(tag_url, headers=headers)
|
|
126
|
+
|
|
127
|
+
# If the tag exists, ask the user if they want to delete it
|
|
128
|
+
if response.status_code == 200:
|
|
129
|
+
warning(f"A tag v{version} already exists. Do you want to delete it? (y/N): ")
|
|
130
|
+
if input().lower() == "y":
|
|
131
|
+
delete_existing_release(owner, project_name, version, headers)
|
|
132
|
+
delete_existing_tag(tag_url, headers)
|
|
133
|
+
return True
|
|
134
|
+
else:
|
|
135
|
+
return False
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
def delete_existing_release(owner: str, project_name: str, version: str, headers: dict[str, str]) -> None:
|
|
139
|
+
""" Delete existing release for a version
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
owner (str): GitHub username
|
|
143
|
+
project_name (str): Name of the GitHub repository
|
|
144
|
+
version (str): Version of the release to delete
|
|
145
|
+
headers (dict[str, str]): Headers for GitHub API requests
|
|
146
|
+
"""
|
|
147
|
+
# Get the release URL and check if it exists
|
|
148
|
+
import requests
|
|
149
|
+
releases_url: str = f"{PROJECT_ENDPOINT}/{owner}/{project_name}/releases/tags/v{version}"
|
|
150
|
+
release_response: requests.Response = requests.get(releases_url, headers=headers)
|
|
151
|
+
|
|
152
|
+
# If the release exists, delete it
|
|
153
|
+
if release_response.status_code == 200:
|
|
154
|
+
release_id: int = release_response.json()["id"]
|
|
155
|
+
delete_release: requests.Response = requests.delete(
|
|
156
|
+
f"{PROJECT_ENDPOINT}/{owner}/{project_name}/releases/{release_id}",
|
|
157
|
+
headers=headers
|
|
158
|
+
)
|
|
159
|
+
handle_response(delete_release, "Failed to delete existing release")
|
|
160
|
+
info(f"Deleted existing release for v{version}")
|
|
161
|
+
|
|
162
|
+
def delete_existing_tag(tag_url: str, headers: dict[str, str]) -> None:
|
|
163
|
+
""" Delete existing tag
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
tag_url (str): URL of the tag to delete
|
|
167
|
+
headers (dict[str, str]): Headers for GitHub API requests
|
|
168
|
+
"""
|
|
169
|
+
import requests
|
|
170
|
+
delete_response: requests.Response = requests.delete(tag_url, headers=headers)
|
|
171
|
+
handle_response(delete_response, "Failed to delete existing tag")
|
|
172
|
+
info("Deleted existing tag")
|
|
173
|
+
|
|
174
|
+
def get_latest_tag(
|
|
175
|
+
owner: str, project_name: str, version: str, headers: dict[str, str]
|
|
176
|
+
) -> tuple[str, str] | tuple[None, None]:
|
|
177
|
+
""" Get latest tag information
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
owner (str): GitHub username
|
|
181
|
+
project_name (str): Name of the GitHub repository
|
|
182
|
+
version (str): Version to remove from the list of tags
|
|
183
|
+
headers (dict[str, str]): Headers for GitHub API requests
|
|
184
|
+
Returns:
|
|
185
|
+
str|None: SHA of the latest tag commit, None if no tags exist
|
|
186
|
+
str|None: Version number of the latest tag, None if no tags exist
|
|
187
|
+
"""
|
|
188
|
+
# Get the tags list
|
|
189
|
+
import requests
|
|
190
|
+
tags_url: str = f"{PROJECT_ENDPOINT}/{owner}/{project_name}/tags"
|
|
191
|
+
response = requests.get(tags_url, headers=headers)
|
|
192
|
+
handle_response(response, "Failed to get tags")
|
|
193
|
+
tags: list[dict[str, Any]] = response.json()
|
|
194
|
+
|
|
195
|
+
# Remove the version from the list of tags and sort the tags by their float values
|
|
196
|
+
tags = [tag for tag in tags if tag["name"] != f"v{version}"]
|
|
197
|
+
tags.sort(key=lambda x: version_to_float(x.get("name", "0")), reverse=True)
|
|
198
|
+
|
|
199
|
+
# If there are no tags, return None
|
|
200
|
+
if len(tags) == 0:
|
|
201
|
+
return None, None
|
|
202
|
+
else:
|
|
203
|
+
return tags[0]["commit"]["sha"], clean_version(tags[0]["name"], keep="ab")
|
|
204
|
+
|
|
205
|
+
def get_commits_since_tag(
|
|
206
|
+
owner: str, project_name: str, latest_tag_sha: str|None, headers: dict[str, str]
|
|
207
|
+
) -> list[dict[str, Any]]:
|
|
208
|
+
""" Get commits since last tag
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
owner (str): GitHub username
|
|
212
|
+
project_name (str): Name of the GitHub repository
|
|
213
|
+
latest_tag_sha (str|None): SHA of the latest tag commit
|
|
214
|
+
headers (dict[str, str]): Headers for GitHub API requests
|
|
215
|
+
Returns:
|
|
216
|
+
list[dict]: List of commits since the last tag
|
|
217
|
+
"""
|
|
218
|
+
# Get the commits URL and parameters
|
|
219
|
+
import requests
|
|
220
|
+
commits_url: str = f"{PROJECT_ENDPOINT}/{owner}/{project_name}/commits"
|
|
221
|
+
commits_params: dict[str, str] = {"per_page": "100"}
|
|
222
|
+
|
|
223
|
+
# Initialize tag_date as None
|
|
224
|
+
tag_date: str|None = None # type: ignore
|
|
225
|
+
|
|
226
|
+
# If there is a latest tag, use it to get the commits since the tag date
|
|
227
|
+
if latest_tag_sha:
|
|
228
|
+
|
|
229
|
+
# Get the date of the latest tag
|
|
230
|
+
tag_commit_url = f"{PROJECT_ENDPOINT}/{owner}/{project_name}/commits/{latest_tag_sha}"
|
|
231
|
+
tag_response = requests.get(tag_commit_url, headers=headers)
|
|
232
|
+
handle_response(tag_response, "Failed to get tag commit")
|
|
233
|
+
tag_date: str = tag_response.json()["commit"]["committer"]["date"]
|
|
234
|
+
|
|
235
|
+
# Use the date as the 'since' parameter to get all commits after that date
|
|
236
|
+
commits_params["since"] = tag_date
|
|
237
|
+
|
|
238
|
+
# Paginate through all commits
|
|
239
|
+
commits: list[dict[str, Any]] = []
|
|
240
|
+
page = 1
|
|
241
|
+
while True:
|
|
242
|
+
params = commits_params.copy()
|
|
243
|
+
params["page"] = str(page)
|
|
244
|
+
response = requests.get(commits_url, headers=headers, params=params)
|
|
245
|
+
handle_response(response, "Failed to get commits")
|
|
246
|
+
page_commits = response.json()
|
|
247
|
+
if not page_commits:
|
|
248
|
+
break
|
|
249
|
+
commits.extend(page_commits)
|
|
250
|
+
if len(page_commits) < 100:
|
|
251
|
+
break
|
|
252
|
+
page += 1
|
|
253
|
+
|
|
254
|
+
# Filter commits only if we have a tag_date
|
|
255
|
+
if tag_date:
|
|
256
|
+
commits = [c for c in commits if c["commit"]["committer"]["date"] != tag_date]
|
|
257
|
+
return commits
|
|
258
|
+
|
|
259
|
+
def generate_changelog(
|
|
260
|
+
commits: list[dict[str, Any]], owner: str, project_name: str, latest_tag_version: str|None, version: str
|
|
261
|
+
) -> str:
|
|
262
|
+
""" Generate changelog from commits. They must follow the conventional commits convention.
|
|
263
|
+
|
|
264
|
+
Convention format: <type>: <description> or <type>(<sub-category>): <description>
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
commits (list[dict]): List of commits to generate changelog from
|
|
268
|
+
owner (str): GitHub username
|
|
269
|
+
project_name (str): Name of the GitHub repository
|
|
270
|
+
latest_tag_version (str|None): Version number of the latest tag
|
|
271
|
+
version (str): Current version being released
|
|
272
|
+
Returns:
|
|
273
|
+
str: Generated changelog text
|
|
274
|
+
Source:
|
|
275
|
+
https://www.conventionalcommits.org/en/v1.0.0/
|
|
276
|
+
"""
|
|
277
|
+
# Initialize the commit groups
|
|
278
|
+
commit_groups: dict[str, list[tuple[str, str, str | None]]] = {}
|
|
279
|
+
|
|
280
|
+
# Iterate over the commits
|
|
281
|
+
for commit in commits:
|
|
282
|
+
message: str = commit["commit"]["message"].split("\n")[0]
|
|
283
|
+
sha: str = commit["sha"]
|
|
284
|
+
|
|
285
|
+
# If the message contains a colon, split the message into a type and a description
|
|
286
|
+
if ":" in message:
|
|
287
|
+
commit_type_part, desc = message.split(":", 1)
|
|
288
|
+
|
|
289
|
+
# Check for breaking change indicator (!)
|
|
290
|
+
is_breaking: bool = False
|
|
291
|
+
if "!" in commit_type_part:
|
|
292
|
+
is_breaking = True
|
|
293
|
+
commit_type_part = commit_type_part.replace("!", "")
|
|
294
|
+
|
|
295
|
+
# Extract sub-category if present (e.g., 'feat(Project)' -> 'feat', 'Project')
|
|
296
|
+
sub_category: str|None = None
|
|
297
|
+
if "(" in commit_type_part and ")" in commit_type_part:
|
|
298
|
+
# Extract the base type (before parentheses)
|
|
299
|
+
commit_type: str = commit_type_part.split('(')[0].split('/')[0]
|
|
300
|
+
# Extract the sub-category (between parentheses)
|
|
301
|
+
sub_category = commit_type_part.split('(')[1].split(')')[0]
|
|
302
|
+
else:
|
|
303
|
+
# No sub-category, just clean the type
|
|
304
|
+
commit_type: str = commit_type_part.split('/')[0]
|
|
305
|
+
|
|
306
|
+
# Clean the type to only keep letters
|
|
307
|
+
commit_type = "".join(c for c in commit_type.lower().strip() if c in "abcdefghijklmnopqrstuvwxyz")
|
|
308
|
+
commit_type = COMMIT_TYPES.get(commit_type, commit_type.title())
|
|
309
|
+
|
|
310
|
+
# Prepend emoji if breaking change
|
|
311
|
+
formatted_desc = f"🚨 {desc.strip()}" if is_breaking else desc.strip()
|
|
312
|
+
|
|
313
|
+
# Add the commit to the commit groups
|
|
314
|
+
if commit_type not in commit_groups:
|
|
315
|
+
commit_groups[commit_type] = []
|
|
316
|
+
commit_groups[commit_type].append((formatted_desc, sha, sub_category))
|
|
317
|
+
|
|
318
|
+
# Initialize the changelog
|
|
319
|
+
changelog: str = "## Changelog\n\n"
|
|
320
|
+
|
|
321
|
+
# Iterate over the commit groups
|
|
322
|
+
for commit_type in sorted(commit_groups.keys()):
|
|
323
|
+
changelog += f"### {commit_type}\n"
|
|
324
|
+
|
|
325
|
+
# Group commits by sub-category
|
|
326
|
+
sub_category_groups: dict[str|None, list[tuple[str, str, str|None]]] = {}
|
|
327
|
+
for desc, sha, sub_category in commit_groups[commit_type]:
|
|
328
|
+
if sub_category not in sub_category_groups:
|
|
329
|
+
sub_category_groups[sub_category] = []
|
|
330
|
+
sub_category_groups[sub_category].append((desc, sha, sub_category))
|
|
331
|
+
|
|
332
|
+
# Sort sub-categories (None comes first, then alphabetical)
|
|
333
|
+
sorted_sub_categories = sorted(
|
|
334
|
+
sub_category_groups.keys(),
|
|
335
|
+
key=lambda x: (x is None, x or "")
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Iterate over sub-categories
|
|
339
|
+
for sub_category in sorted_sub_categories:
|
|
340
|
+
|
|
341
|
+
# Add commits for this sub-category
|
|
342
|
+
for desc, sha, _ in reversed(sub_category_groups[sub_category]):
|
|
343
|
+
|
|
344
|
+
# Prepend sub-category to description if present
|
|
345
|
+
if sub_category:
|
|
346
|
+
words: list[str] = [
|
|
347
|
+
word[0].upper() + word[1:] # We don't use title() because we don't want to lowercase any letter
|
|
348
|
+
for word in sub_category.replace('_', ' ').split()
|
|
349
|
+
]
|
|
350
|
+
formatted_sub_category: str = ' '.join(words)
|
|
351
|
+
formatted_desc = f"[{formatted_sub_category}] {desc}"
|
|
352
|
+
else:
|
|
353
|
+
formatted_desc = desc
|
|
354
|
+
changelog += f"- {formatted_desc} ([{sha[:7]}](https://github.com/{owner}/{project_name}/commit/{sha}))\n"
|
|
355
|
+
|
|
356
|
+
changelog += "\n"
|
|
357
|
+
|
|
358
|
+
# Add the full changelog link if there is a latest tag and return the changelog
|
|
359
|
+
if latest_tag_version:
|
|
360
|
+
changelog += f"**Full Changelog**: https://github.com/{owner}/{project_name}/compare/v{latest_tag_version}...v{version}\n"
|
|
361
|
+
return changelog
|
|
362
|
+
|
|
363
|
+
def create_tag(owner: str, project_name: str, version: str, headers: dict[str, str]) -> None:
|
|
364
|
+
""" Create a new tag
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
owner (str): GitHub username
|
|
368
|
+
project_name (str): Name of the GitHub repository
|
|
369
|
+
version (str): Version for the new tag
|
|
370
|
+
headers (dict[str, str]): Headers for GitHub API requests
|
|
371
|
+
"""
|
|
372
|
+
# Message and prepare urls
|
|
373
|
+
import requests
|
|
374
|
+
progress(f"Creating tag v{version}")
|
|
375
|
+
create_tag_url: str = f"{PROJECT_ENDPOINT}/{owner}/{project_name}/git/refs"
|
|
376
|
+
latest_commit_url: str = f"{PROJECT_ENDPOINT}/{owner}/{project_name}/git/refs/heads/main"
|
|
377
|
+
|
|
378
|
+
# Get the latest commit SHA
|
|
379
|
+
commit_response: requests.Response = requests.get(latest_commit_url, headers=headers)
|
|
380
|
+
handle_response(commit_response, "Failed to get latest commit")
|
|
381
|
+
commit_sha: str = commit_response.json()["object"]["sha"]
|
|
382
|
+
|
|
383
|
+
# Create the tag
|
|
384
|
+
tag_data: dict[str, str] = {
|
|
385
|
+
"ref": f"refs/tags/v{version}",
|
|
386
|
+
"sha": commit_sha
|
|
387
|
+
}
|
|
388
|
+
response: requests.Response = requests.post(create_tag_url, headers=headers, json=tag_data)
|
|
389
|
+
handle_response(response, "Failed to create tag")
|
|
390
|
+
|
|
391
|
+
def create_release(owner: str, project_name: str, version: str, changelog: str, headers: dict[str, str]) -> int:
|
|
392
|
+
""" Create a new release
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
owner (str): GitHub username
|
|
396
|
+
project_name (str): Name of the GitHub repository
|
|
397
|
+
version (str): Version for the new release
|
|
398
|
+
changelog (str): Changelog text for the release
|
|
399
|
+
headers (dict[str, str]): Headers for GitHub API requests
|
|
400
|
+
Returns:
|
|
401
|
+
int: ID of the created release
|
|
402
|
+
"""
|
|
403
|
+
# Message and prepare urls
|
|
404
|
+
import requests
|
|
405
|
+
progress(f"Creating release v{version}")
|
|
406
|
+
release_url: str = f"{PROJECT_ENDPOINT}/{owner}/{project_name}/releases"
|
|
407
|
+
release_data: dict[str, str|bool] = {
|
|
408
|
+
"tag_name": f"v{version}",
|
|
409
|
+
"name": f"{project_name} [v{version}]",
|
|
410
|
+
"body": changelog,
|
|
411
|
+
"draft": False,
|
|
412
|
+
"prerelease": False
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
# Create the release and return the release ID
|
|
416
|
+
response: requests.Response = requests.post(release_url, headers=headers, json=release_data)
|
|
417
|
+
handle_response(response, "Failed to create release")
|
|
418
|
+
return response.json()["id"]
|
|
419
|
+
|
|
420
|
+
def upload_assets(
|
|
421
|
+
owner: str, project_name: str, release_id: int, build_folder: str, headers: dict[str, str], endswith: list[str]
|
|
422
|
+
) -> None:
|
|
423
|
+
""" Upload release assets
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
owner (str): GitHub username
|
|
427
|
+
project_name (str): Name of the GitHub repository
|
|
428
|
+
release_id (int): ID of the release to upload assets to
|
|
429
|
+
build_folder (str): Folder containing assets to upload
|
|
430
|
+
headers (dict[str, str]): Headers for GitHub API requests
|
|
431
|
+
endswith (list[str]): List of files to upload to the release
|
|
432
|
+
(every file ending with one of these strings will be uploaded)
|
|
433
|
+
"""
|
|
434
|
+
endswith_tuple: tuple[str, ...] = tuple(endswith)
|
|
435
|
+
|
|
436
|
+
# If there is no build folder, return
|
|
437
|
+
if not build_folder:
|
|
438
|
+
return
|
|
439
|
+
progress("Uploading assets")
|
|
440
|
+
|
|
441
|
+
# Get the release details
|
|
442
|
+
import requests
|
|
443
|
+
release_url: str = f"{PROJECT_ENDPOINT}/{owner}/{project_name}/releases/{release_id}"
|
|
444
|
+
response: requests.Response = requests.get(release_url, headers=headers)
|
|
445
|
+
handle_response(response, "Failed to get release details")
|
|
446
|
+
upload_url_template: str = response.json()["upload_url"]
|
|
447
|
+
upload_url_base: str = upload_url_template.split("{", maxsplit=1)[0]
|
|
448
|
+
|
|
449
|
+
# Iterate over the files in the build folder
|
|
450
|
+
for file in os.listdir(build_folder):
|
|
451
|
+
if file.endswith(endswith_tuple):
|
|
452
|
+
file_path: str = f"{clean_path(build_folder)}/{file}"
|
|
453
|
+
with open(file_path, "rb") as f:
|
|
454
|
+
|
|
455
|
+
# Prepare the headers and params
|
|
456
|
+
headers_with_content: dict[str, str] = {
|
|
457
|
+
**headers,
|
|
458
|
+
"Content-Type": "application/zip"
|
|
459
|
+
}
|
|
460
|
+
params: dict[str, str] = {"name": file}
|
|
461
|
+
|
|
462
|
+
# Upload the file
|
|
463
|
+
response: requests.Response = requests.post(
|
|
464
|
+
upload_url_base,
|
|
465
|
+
headers=headers_with_content,
|
|
466
|
+
params=params,
|
|
467
|
+
data=f.read()
|
|
468
|
+
)
|
|
469
|
+
handle_response(response, f"Failed to upload {file}")
|
|
470
|
+
progress(f"Uploaded {file}")
|
|
471
|
+
|
|
472
|
+
@measure_time(message="Uploading to GitHub took")
|
|
473
|
+
@handle_error()
|
|
474
|
+
def upload_to_github(credentials: dict[str, Any], github_config: dict[str, Any]) -> str:
|
|
475
|
+
""" Upload the project to GitHub using the credentials and the configuration
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
credentials (dict[str, Any]): Credentials for the GitHub API
|
|
479
|
+
github_config (dict[str, Any]): Configuration for the GitHub project
|
|
480
|
+
Returns:
|
|
481
|
+
str: Generated changelog text
|
|
482
|
+
Examples:
|
|
483
|
+
|
|
484
|
+
.. code-block:: python
|
|
485
|
+
|
|
486
|
+
> upload_to_github(
|
|
487
|
+
credentials={
|
|
488
|
+
"github": {
|
|
489
|
+
"api_key": "ghp_...",
|
|
490
|
+
"username": "Stoupy"
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
github_config={
|
|
494
|
+
"project_name": "stouputils",
|
|
495
|
+
"version": "1.0.0",
|
|
496
|
+
"build_folder": "build",
|
|
497
|
+
"endswith": [".zip"]
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
"""
|
|
501
|
+
import requests # type: ignore # noqa: F401
|
|
502
|
+
|
|
503
|
+
# Validate credentials and configuration
|
|
504
|
+
owner, headers = validate_credentials(credentials)
|
|
505
|
+
project_name, version, build_folder, endswith = validate_config(github_config)
|
|
506
|
+
|
|
507
|
+
# Handle existing tag
|
|
508
|
+
can_create: bool = handle_existing_tag(owner, project_name, version, headers)
|
|
509
|
+
|
|
510
|
+
# Get the latest tag and commits since the tag
|
|
511
|
+
latest_tag_sha, latest_tag_version = get_latest_tag(owner, project_name, version, headers)
|
|
512
|
+
commits: list[dict[str, Any]] = get_commits_since_tag(owner, project_name, latest_tag_sha, headers)
|
|
513
|
+
changelog: str = generate_changelog(commits, owner, project_name, latest_tag_version, version)
|
|
514
|
+
|
|
515
|
+
# Create the tag and release if needed
|
|
516
|
+
if can_create:
|
|
517
|
+
create_tag(owner, project_name, version, headers)
|
|
518
|
+
release_id: int = create_release(owner, project_name, version, changelog, headers)
|
|
519
|
+
upload_assets(owner, project_name, release_id, build_folder, headers, endswith)
|
|
520
|
+
info(f"Project '{project_name}' updated on GitHub!")
|
|
521
|
+
return changelog
|
|
522
|
+
|