|
| 1 | +import asyncio |
1 | 2 | import json |
2 | | -import os |
3 | 3 | import re |
4 | | -import urllib.request as request |
5 | | -import urllib.parse as parse |
| 4 | +from pathlib import Path |
| 5 | +from urllib.parse import quote |
| 6 | +import urllib.request |
6 | 7 |
|
7 | | -pattern = re.compile(r"(.+)-([\d.]+)-shaded\.jar") |
| 8 | +# Compile pattern once at module level |
| 9 | +ARTIFACT_PATTERN = re.compile(r"(.+)-([\d.]+)-shaded\.jar") |
8 | 10 |
|
9 | 11 |
|
10 | | -def read_properties(path: str) -> dict: |
11 | | - with open(path, "r") as file: |
12 | | - return json.load(file) |
| 12 | +class JenkinsAPIError(Exception): |
| 13 | + """Custom exception for Jenkins API related errors.""" |
13 | 14 |
|
| 15 | + pass |
| 16 | + |
| 17 | + |
| 18 | +async def read_properties(path): |
| 19 | + """Read and parse a JSON properties file asynchronously. |
| 20 | +
|
| 21 | + Args: |
| 22 | + path: Path to the JSON file |
| 23 | +
|
| 24 | + Returns: |
| 25 | + Dictionary containing the properties |
| 26 | +
|
| 27 | + Raises: |
| 28 | + FileNotFoundError: If the file doesn't exist |
| 29 | + json.JSONDecodeError: If the file contains invalid JSON |
| 30 | + """ |
| 31 | + try: |
| 32 | + loop = asyncio.get_event_loop() |
| 33 | + content = await loop.run_in_executor(None, path.read_text, "utf-8") |
| 34 | + return json.loads(content) |
| 35 | + except FileNotFoundError: |
| 36 | + print(f"Error: File not found: {path}") |
| 37 | + raise |
| 38 | + except json.JSONDecodeError as e: |
| 39 | + print(f"Error: Invalid JSON in {path}: {e}") |
| 40 | + raise |
| 41 | + |
| 42 | + |
| 43 | +async def read_folder(path): |
| 44 | + """Read all JSON property files from a folder asynchronously. |
| 45 | +
|
| 46 | + Args: |
| 47 | + path: Path to the folder containing property files |
| 48 | +
|
| 49 | + Returns: |
| 50 | + List of dictionaries containing properties from each file |
| 51 | + """ |
| 52 | + folder_path = Path(path) |
| 53 | + |
| 54 | + if not folder_path.exists(): |
| 55 | + print(f"Warning: Folder not found: {path}") |
| 56 | + return [] |
| 57 | + |
| 58 | + json_files = [ |
| 59 | + f for f in folder_path.iterdir() if f.is_file() and f.suffix == ".json" |
| 60 | + ] |
| 61 | + |
| 62 | + # Read all files concurrently |
| 63 | + tasks = [read_properties(file) for file in json_files] |
| 64 | + results = await asyncio.gather(*tasks, return_exceptions=True) |
14 | 65 |
|
15 | | -def read_folder(path: str) -> list[dict]: |
16 | 66 | properties_arr = [] |
17 | | - for file in os.listdir(path): |
18 | | - properties = read_properties(os.path.join(path, file)) |
19 | | - properties_arr.append(properties) |
| 67 | + for file, result in zip(json_files, results): |
| 68 | + if isinstance(result, Exception): |
| 69 | + print(f"Error reading {file}: {result}") |
| 70 | + else: |
| 71 | + properties_arr.append(result) |
| 72 | + |
20 | 73 | return properties_arr |
21 | 74 |
|
22 | 75 |
|
23 | | -def fetch_from_official(jenkins_name: str) -> (str, str): |
24 | | - api_url = f"https://ci.codemc.io/job/BetterGUI-MC/job/{parse.quote(jenkins_name)}/api/json?tree=builds[url]" |
| 76 | +async def fetch_from_official(jenkins_name): |
| 77 | + """Fetch version and download URL from Jenkins CI asynchronously. |
| 78 | +
|
| 79 | + Args: |
| 80 | + jenkins_name: Name of the Jenkins job |
| 81 | +
|
| 82 | + Returns: |
| 83 | + Tuple of (version, artifact_url) |
| 84 | +
|
| 85 | + Raises: |
| 86 | + JenkinsAPIError: If unable to fetch or parse Jenkins data |
| 87 | + """ |
| 88 | + api_url = f"https://ci.codemc.io/job/BetterGUI-MC/job/{quote(jenkins_name)}/api/json?tree=builds[url]" |
25 | 89 | print(f"Jenkins URL: {api_url}") |
26 | | - api_res = json.load(request.urlopen(api_url)) |
27 | | - build_urls = [build["url"] for build in api_res["builds"]] |
| 90 | + |
| 91 | + try: |
| 92 | + loop = asyncio.get_event_loop() |
| 93 | + response = await loop.run_in_executor( |
| 94 | + None, lambda: urllib.request.urlopen(api_url, timeout=30) |
| 95 | + ) |
| 96 | + data = response.read().decode('utf-8') |
| 97 | + api_res = json.loads(data) |
| 98 | + except urllib.error.URLError as e: |
| 99 | + raise JenkinsAPIError(f"Failed to fetch Jenkins API: {e}") |
| 100 | + except json.JSONDecodeError as e: |
| 101 | + raise JenkinsAPIError(f"Invalid JSON response from Jenkins: {e}") |
| 102 | + |
| 103 | + build_urls = [build["url"] for build in api_res.get("builds", [])] |
| 104 | + |
| 105 | + if not build_urls: |
| 106 | + raise JenkinsAPIError("No builds found") |
| 107 | + |
28 | 108 | for build_url in build_urls: |
29 | | - normalized_build_url = build_url[0:-1] if build_url.endswith("/") else build_url |
30 | | - build_api_url = f"{normalized_build_url}/api/json?tree=artifacts[fileName,relativePath]" |
| 109 | + normalized_build_url = build_url.rstrip("/") |
| 110 | + build_api_url = ( |
| 111 | + f"{normalized_build_url}/api/json?tree=artifacts[fileName,relativePath]" |
| 112 | + ) |
31 | 113 | print(f"Build URL: {build_api_url}") |
32 | | - build_res = json.load(request.urlopen(build_api_url)) |
33 | | - artifacts = build_res["artifacts"] |
| 114 | + |
| 115 | + try: |
| 116 | + loop = asyncio.get_event_loop() |
| 117 | + response = await loop.run_in_executor( |
| 118 | + None, lambda: urllib.request.urlopen(build_api_url, timeout=30) |
| 119 | + ) |
| 120 | + data = response.read().decode('utf-8') |
| 121 | + build_res = json.loads(data) |
| 122 | + except (urllib.error.URLError, json.JSONDecodeError) as e: |
| 123 | + print(f"Warning: Failed to fetch build {build_url}: {e}") |
| 124 | + continue |
| 125 | + |
| 126 | + artifacts = build_res.get("artifacts", []) |
| 127 | + |
34 | 128 | for artifact in artifacts: |
35 | | - file_name = artifact["fileName"] |
36 | | - relative_path = artifact["relativePath"] |
37 | | - matcher = re.search(pattern, file_name) |
38 | | - try: |
| 129 | + file_name = artifact.get("fileName", "") |
| 130 | + relative_path = artifact.get("relativePath", "") |
| 131 | + matcher = ARTIFACT_PATTERN.search(file_name) |
| 132 | + |
| 133 | + if matcher: |
39 | 134 | version = matcher.group(2) |
40 | 135 | artifact_url = f"{normalized_build_url}/artifact/{relative_path}" |
41 | 136 | print(f"Found: {file_name}") |
42 | 137 | return version, artifact_url |
43 | | - except: |
44 | | - continue |
45 | | - raise Exception("No download link & version found") |
46 | 138 |
|
| 139 | + raise JenkinsAPIError("No valid artifact found in any build") |
| 140 | + |
| 141 | + |
| 142 | +async def convert( |
| 143 | + properties, |
| 144 | + file_extension = ".jar", |
| 145 | +): |
| 146 | + """Convert properties to the output format asynchronously. |
| 147 | +
|
| 148 | + Args: |
| 149 | + properties: Dictionary containing addon properties |
| 150 | + file_extension: File extension to append to the name |
47 | 151 |
|
48 | | -def convert(properties: dict, file_extension: str = ".jar") -> (str, dict): |
49 | | - prop_name = properties["name"] |
| 152 | + Returns: |
| 153 | + Tuple of (name, values_dict) |
| 154 | + """ |
| 155 | + prop_name = properties.get("name", "Unknown") |
50 | 156 | name = prop_name |
51 | | - print(f"Adding {name}") |
| 157 | + print(f"Processing {name}") |
| 158 | + |
52 | 159 | values = { |
53 | 160 | "file-name": name + file_extension, |
54 | | - "description": properties["description"], |
55 | | - "authors": properties["author"], |
56 | | - "source-code": properties["code"], |
57 | | - "wiki": properties["wiki"] |
| 161 | + "description": properties.get("description", ""), |
| 162 | + "authors": properties.get("author", ""), |
| 163 | + "source-code": properties.get("code", ""), |
| 164 | + "wiki": properties.get("wiki", ""), |
58 | 165 | } |
59 | 166 |
|
60 | | - prop_type = properties["type"] if "type" in properties else "" |
| 167 | + prop_type = properties.get("type", "") |
| 168 | + |
61 | 169 | if prop_type == "official": |
62 | | - jenkins_name = properties["jenkins"] |
63 | | - version, download_link = fetch_from_official(jenkins_name) |
64 | | - values["version"] = version |
65 | | - values["direct-link"] = download_link |
| 170 | + jenkins_name = properties.get("jenkins") |
| 171 | + if not jenkins_name: |
| 172 | + print(f"Warning: No Jenkins name specified for {name}") |
| 173 | + values["version"] = "unknown" |
| 174 | + values["direct-link"] = "" |
| 175 | + else: |
| 176 | + try: |
| 177 | + version, download_link = await fetch_from_official(jenkins_name) |
| 178 | + values["version"] = version |
| 179 | + values["direct-link"] = download_link |
| 180 | + except JenkinsAPIError as e: |
| 181 | + print(f"Error fetching from Jenkins for {name}: {e}") |
| 182 | + values["version"] = "unknown" |
| 183 | + values["direct-link"] = "" |
66 | 184 | else: |
67 | | - values["version"] = properties["version"] |
68 | | - values["direct-link"] = properties["download"] |
| 185 | + values["version"] = properties.get("version", "unknown") |
| 186 | + values["direct-link"] = properties.get("download", "") |
69 | 187 |
|
| 188 | + print(f"Completed {name}") |
70 | 189 | return name, values |
71 | 190 |
|
72 | 191 |
|
73 | | -def write(path: str, properties_dict: dict): |
74 | | - with open(path, "w") as file: |
75 | | - json_str = json.dumps(properties_dict, separators=(",", ":")) |
76 | | - file.write(json_str) |
| 192 | +async def write(path, properties_dict): |
| 193 | + """Write properties dictionary to a JSON file asynchronously. |
| 194 | +
|
| 195 | + Args: |
| 196 | + path: Output file path |
| 197 | + properties_dict: Dictionary to write |
| 198 | + """ |
| 199 | + try: |
| 200 | + loop = asyncio.get_event_loop() |
| 201 | + json_str = json.dumps( |
| 202 | + properties_dict, separators=(",", ":"), ensure_ascii=False |
| 203 | + ) |
| 204 | + await loop.run_in_executor(None, Path(path).write_text, json_str, "utf-8") |
| 205 | + print(f"\nSuccessfully wrote to {path}") |
| 206 | + except IOError as e: |
| 207 | + print(f"Error writing to {path}: {e}") |
| 208 | + raise |
| 209 | + |
77 | 210 |
|
| 211 | +async def process_all_addons(properties_list): |
| 212 | + """Process all addons concurrently and merge results. |
78 | 213 |
|
79 | | -def main(): |
80 | | - arr = read_folder("addons") |
| 214 | + Args: |
| 215 | + properties_list: List of addon properties |
| 216 | +
|
| 217 | + Returns: |
| 218 | + Dictionary with all processed addons merged |
| 219 | + """ |
| 220 | + # Process all addons concurrently |
| 221 | + tasks = [convert(properties) for properties in properties_list] |
| 222 | + results = await asyncio.gather(*tasks, return_exceptions=True) |
| 223 | + |
| 224 | + # Merge results |
81 | 225 | converted = {} |
82 | | - for properties in arr: |
83 | | - name, values = convert(properties) |
84 | | - converted[name] = values |
85 | | - write("addons.json", converted) |
| 226 | + for result in results: |
| 227 | + if isinstance(result, Exception): |
| 228 | + print(f"Error converting addon: {result}") |
| 229 | + else: |
| 230 | + name, values = result |
| 231 | + converted[name] = values |
| 232 | + |
| 233 | + return converted |
| 234 | + |
| 235 | + |
| 236 | +async def main(): |
| 237 | + """Main function to process addons and generate output JSON asynchronously.""" |
| 238 | + print("Starting async addon processing...\n") |
| 239 | + |
| 240 | + # Read all property files |
| 241 | + properties_list = await read_folder("addons") |
| 242 | + |
| 243 | + if not properties_list: |
| 244 | + print("No addon properties found. Exiting.") |
| 245 | + return |
| 246 | + |
| 247 | + print(f"Found {len(properties_list)} addon(s) to process\n") |
| 248 | + |
| 249 | + # Process all addons concurrently |
| 250 | + converted = await process_all_addons(properties_list) |
| 251 | + |
| 252 | + if converted: |
| 253 | + await write("addons.json", converted) |
| 254 | + print(f"\nProcessed {len(converted)} addon(s) successfully") |
| 255 | + else: |
| 256 | + print("No addons were successfully processed.") |
86 | 257 |
|
87 | 258 |
|
88 | | -if __name__ == '__main__': |
89 | | - main() |
| 259 | +if __name__ == "__main__": |
| 260 | + asyncio.run(main()) |
0 commit comments