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)
|
cursefetch/cursefetch.py
ADDED
|
@@ -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,,
|