cursefetch 0.1.0__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.
cursefetch/__init__.py ADDED
@@ -0,0 +1,170 @@
1
+ import argparse
2
+ import json
3
+ import os
4
+ import traceback
5
+
6
+ import cursefetch.cursefetch as cf
7
+
8
+ from . import download
9
+
10
+
11
+ def main() -> None:
12
+ print("Hello from CurseFetch!")
13
+
14
+ argparser = argparse.ArgumentParser(
15
+ description="Fetch the latest version of a CurseForge project."
16
+ )
17
+ argparser.add_argument("project_id", help="The ID of the CurseForge project.")
18
+ subparsers = argparser.add_subparsers(dest="command")
19
+
20
+ # list versions
21
+ list_parser = subparsers.add_parser(
22
+ "list-version", help="List the versions of the project."
23
+ )
24
+ list_parser.add_argument(
25
+ "-d",
26
+ "--details",
27
+ help="Show detailed information about each version (in JSON format).",
28
+ action="store_true",
29
+ )
30
+
31
+ # download version
32
+ download_parser = subparsers.add_parser(
33
+ "download", help="Download a specific version of the project."
34
+ )
35
+ download_parser.add_argument(
36
+ "version",
37
+ help="The version name or id to download (default: latest).",
38
+ default="latest",
39
+ )
40
+ download_parser.add_argument("--simulate-version-selection", action="store_true")
41
+ download_parser.add_argument(
42
+ "-t",
43
+ "--release-type",
44
+ help="The release type to filter by. (default: none). (only applicable when version is 'latest')",
45
+ choices=["release", "beta", "alpha"],
46
+ default=None,
47
+ )
48
+ download_parser.add_argument(
49
+ "--version-order",
50
+ help="The method to order versions by when selecting the latest version. (default: default)",
51
+ choices=["default", "semver"],
52
+ default="default",
53
+ )
54
+ download_parser.add_argument(
55
+ "-o", "--output", help="The path of the output file/directory."
56
+ )
57
+ download_parser.add_argument(
58
+ "-u",
59
+ "--uncompress",
60
+ help="Uncompress the downloaded file to the output directory.",
61
+ action="store_true",
62
+ )
63
+
64
+ args = argparser.parse_args()
65
+ if args.command == "list-version":
66
+ command_list_version(args)
67
+ elif args.command == "download":
68
+ command_download(args)
69
+ else:
70
+ argparser.print_help()
71
+
72
+
73
+ def command_list_version(args):
74
+ try:
75
+ list = cf.get_version_list(args.project_id)
76
+ if args.details:
77
+ print(json.dumps(list, indent=2))
78
+ else:
79
+ print_version_list_simple(list)
80
+ except Exception as e:
81
+ print("Failed to fetch version list.")
82
+ print(e)
83
+
84
+
85
+ def print_version_list_simple(version_list: list) -> None:
86
+ # calculate the maximum length of the id, name, and release type fields for formatting
87
+ id_max_length = max(len(str(v["id"])) for v in version_list)
88
+ name_max_length = max(len(v["displayName"]) for v in version_list)
89
+ file_date_max_length = max(len(str(v["fileDate"])) for v in version_list)
90
+
91
+ def release_type_to_str(
92
+ release_type: int | str, dict={1: "release", 2: "beta", 3: "alpha"}
93
+ ) -> str:
94
+ if isinstance(release_type, int):
95
+ return dict.get(release_type, "unknown")
96
+ return release_type
97
+
98
+ for v in version_list:
99
+ id = str(v["id"]).rjust(id_max_length)
100
+ name = str(v["displayName"]).ljust(name_max_length)
101
+ release_type = release_type_to_str(v["releaseType"])
102
+ # the length of "release" and "unknown" is 7
103
+ release_type = release_type.rjust(7)
104
+ file_date = str(v["fileDate"]).ljust(file_date_max_length)
105
+ print(f"({id}) {name} {release_type} {file_date}")
106
+
107
+
108
+ def command_download(args):
109
+ try:
110
+ version = args.version
111
+ # the selected version info dict
112
+ version_info = None
113
+
114
+ # grab the version list
115
+ list = None
116
+ try:
117
+ list = cf.get_version_list(args.project_id)
118
+ except:
119
+ print("Failed to fetch version list.")
120
+ raise
121
+
122
+ if version == "latest":
123
+ # handle latest version selection
124
+ try:
125
+ version_info = cf.select_latest_version(
126
+ list, release_type=args.release_type, order_by=args.version_order
127
+ )
128
+ except:
129
+ print("Failed to select the latest version.")
130
+ raise
131
+ else:
132
+ # or find the version by name or id
133
+ for v in list:
134
+ if v["displayName"] == version or str(v["id"]) == version:
135
+ version_info = v
136
+ break
137
+
138
+ # fast fail if we couldn't find the version
139
+ if version_info is None:
140
+ raise ValueError("Unable to find the specified version.")
141
+
142
+ # dump simulation info and return
143
+ if args.simulate_version_selection:
144
+ print("Selected version")
145
+ print_version_list_simple([version_info])
146
+ return
147
+
148
+ # show the selected version info
149
+ print(f"Version: {version_info['displayName']} (id: {version_info['id']})")
150
+
151
+ # download the file
152
+ output_path = args.output if not args.uncompress else "temp_download.zip"
153
+ try:
154
+ download.download_url(version_info["downloadUrl"], output_path)
155
+ except:
156
+ print("Failed to download the version.")
157
+ raise
158
+
159
+ # uncompress the file if requested
160
+ if args.uncompress:
161
+ print("Uncompressing the downloaded file")
162
+ try:
163
+ download.uncompress_zip(output_path, args.output)
164
+ except:
165
+ print("Failed to uncompress the downloaded file.")
166
+ raise
167
+
168
+ except Exception as e:
169
+ print("Failed to download the version.")
170
+ traceback.print_exception(e)
@@ -0,0 +1,82 @@
1
+ import os
2
+ from typing import Literal
3
+
4
+ import requests
5
+
6
+
7
+ def get_version_list(project_id: str) -> list:
8
+ """
9
+ Fetches the list of all versions for a given CurseForge project ID.
10
+ :param project_id: The ID of the CurseForge project to fetch versions for.
11
+ :return: A list of version information.
12
+ """
13
+ PER_REQUEST_COUNT = 50
14
+ url = f"https://api.curseforge.com/v1/mods/{project_id}/files?pageSize={PER_REQUEST_COUNT}"
15
+
16
+ api_key = os.getenv("CF_API_KEY")
17
+ if api_key is None:
18
+ raise ValueError("CF_API_KEY environment variable is not set.")
19
+
20
+ all = []
21
+ try:
22
+ resp = requests.get(url, headers={"X-API-KEY": api_key})
23
+ resp.raise_for_status()
24
+ data = resp.json()
25
+ if "data" in data and len(data["data"]) > 0:
26
+ all.extend(data["data"])
27
+ else:
28
+ raise ValueError("Unable to find any files for the given project ID.")
29
+ while "pagination" in data and len(all) < data["pagination"]["totalCount"]:
30
+ # fetch all pages until we have all the data
31
+ resp = requests.get(
32
+ f"{url}&index={data['pagination']['index'] + PER_REQUEST_COUNT}",
33
+ headers={"X-API-KEY": api_key},
34
+ )
35
+ resp.raise_for_status()
36
+ data = resp.json()
37
+ all.extend(data["data"])
38
+ return all
39
+ except requests.RequestException as e:
40
+ print(f"Error fetching data from CurseForge API: {e}")
41
+ raise
42
+ except (KeyError, IndexError) as e:
43
+ print(f"Error parsing data from CurseForge API: {e}")
44
+ raise
45
+
46
+
47
+ def select_latest_version(
48
+ version_list: list,
49
+ release_type: Literal["release", "beta", "alpha", 1, 2, 3] | None = None,
50
+ order_by: Literal["default", "semver"] = "default",
51
+ ) -> dict:
52
+ """
53
+ Select the latest version from a list of versions, optionally filtering by release type and ordering by semver.
54
+ :param version_list: The list of versions to select from.
55
+ :param release_type: The release type to filter by (release, beta, alpha, or their corresponding integers).
56
+ :param order_by: The method to order versions by (default or semver).
57
+ :return: The latest version information.
58
+ """
59
+
60
+ # convert releaseType to int if it's a string
61
+ if isinstance(release_type, str):
62
+ release_type = {"release": 1, "beta": 2, "alpha": 3}[release_type]
63
+ release_type: int # hint type
64
+
65
+ # filter the list by releaseType if it's not None
66
+ if release_type is not None:
67
+ version_list = [v for v in version_list if v["releaseType"] == release_type]
68
+ if len(version_list) == 0:
69
+ raise ValueError("No versions found for the given release type.")
70
+
71
+ if order_by == "semver":
72
+ from packaging.version import InvalidVersion, Version
73
+
74
+ try:
75
+ return max(version_list, key=lambda v: Version(v["displayName"]))
76
+ except InvalidVersion:
77
+ # there's a version that doesn't follow semver, so we can't sort by semver
78
+ # FIXME: However, we used a package that may not fully support semver, so find a workaround when someone needs.
79
+ raise
80
+ else:
81
+ # curseforge responds with a upload-order sorted list, so the first item is the latest version
82
+ return version_list[0]
cursefetch/download.py ADDED
@@ -0,0 +1,35 @@
1
+ import os
2
+ import zipfile
3
+
4
+ import requests
5
+ from tqdm import tqdm
6
+
7
+
8
+ def download_url(url: str, destination: str):
9
+ with requests.get(url, stream=True) as r:
10
+ r.raise_for_status()
11
+ total_size = int(r.headers.get("Content-Length", 0))
12
+ with (
13
+ open(destination, "wb") as f,
14
+ # use tqdm to show a progress bar while downloading
15
+ tqdm(
16
+ desc=destination,
17
+ total=total_size,
18
+ unit="iB",
19
+ unit_scale=True,
20
+ unit_divisor=1024,
21
+ ) as bar,
22
+ ):
23
+ for chunk in r.iter_content(chunk_size=8192):
24
+ if chunk: # filter out keep-alive new chunks
25
+ size = f.write(chunk)
26
+ bar.update(size)
27
+
28
+
29
+ def uncompress_zip(zip_path: str, destination: str):
30
+ # ensure the destination directory exists
31
+ if not os.path.exists(destination):
32
+ os.makedirs(destination)
33
+
34
+ with zipfile.ZipFile(zip_path, "r") as zip:
35
+ zip.extractall(destination)
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: cursefetch
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author-email: Taskeren <r0yalist@outlook.com>
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: packaging>=26.0
8
+ Requires-Dist: requests>=2.33.0
9
+ Requires-Dist: tqdm>=4.67.3
@@ -0,0 +1,7 @@
1
+ cursefetch/__init__.py,sha256=pjykjIv3-Ah7aikv1q01Zi_HsoOLtU4IoyHTOLH4RJw,5613
2
+ cursefetch/cursefetch.py,sha256=87voT_6iFIfTtC_F50AyutStw2oVEp1su_3DEVQn-Ws,3361
3
+ cursefetch/download.py,sha256=TNx83hNDBmfoPHYXTQPaMjMcFbNOK3y9G9vWuVGW_t0,1076
4
+ cursefetch-0.1.0.dist-info/METADATA,sha256=M3-zdfiSBjblrN5FjLyXVbBdIBEM2aP7XGKl_g7W4gw,250
5
+ cursefetch-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
6
+ cursefetch-0.1.0.dist-info/entry_points.txt,sha256=3RXhRth86L5eawsTBcu1eilBxVeinMqs_p-fzM_JdIE,47
7
+ cursefetch-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cursefetch = cursefetch:main