Skip to content

Commit b270159

Browse files
committed
feat: first commit
1 parent f1c4f36 commit b270159

File tree

16 files changed

+1084
-0
lines changed

16 files changed

+1084
-0
lines changed

.github/workflows/release.yml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
7+
workflow_dispatch:
8+
9+
jobs:
10+
release-build:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v4
16+
17+
- name: Install uv
18+
uses: astral-sh/setup-uv@v5
19+
20+
- name: Install python
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: "3.x"
24+
25+
- name: Install just
26+
uses: extractions/setup-just@v2
27+
env:
28+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29+
30+
- name: build release distributions
31+
run: |
32+
just build
33+
34+
- name: upload dists
35+
uses: actions/upload-artifact@v4
36+
with:
37+
name: release-dists
38+
path: dist/
39+
40+
publish-pypi:
41+
runs-on: ubuntu-latest
42+
name: Publish to PyPI
43+
if: "startsWith(github.ref, 'refs/tags/')"
44+
needs:
45+
- release-build
46+
permissions:
47+
id-token: write
48+
49+
steps:
50+
- name: Retrieve release distributions
51+
uses: actions/download-artifact@v4
52+
with:
53+
name: release-dists
54+
path: dist/
55+
56+
- name: Install uv
57+
uses: astral-sh/setup-uv@v5
58+
59+
- name: Publish release distributions to PyPI
60+
run: uv publish -v
61+
62+
# publish-release:
63+
# runs-on: ubuntu-latest
64+
# name: Publish to GitHub
65+
# if: "startsWith(github.ref, 'refs/tags/')"
66+
# needs:
67+
# - release-build
68+
# permissions:
69+
# contents: write
70+
# steps:
71+
# - uses: actions/download-artifact@v4
72+
# with:
73+
# name: release-dists
74+
# path: dist/
75+
# - name: Get tag name
76+
# run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
77+
# - name: Publish to GitHub
78+
# uses: softprops/action-gh-release@v2
79+
# with:
80+
# draft: true
81+
# files: dist/*
82+
# tag_name: ${{ env.RELEASE_VERSION }}

.github/workflows/test.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Unit Test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
merge_group:
8+
workflow_dispatch:
9+
10+
jobs:
11+
unit-test:
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
16+
architecture: ["x64"]
17+
name: Python ${{ matrix.python-version }} on ${{ matrix.architecture }} unit test
18+
env:
19+
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v4
23+
with:
24+
submodules: recursive
25+
26+
- name: Install uv
27+
uses: astral-sh/setup-uv@v5
28+
29+
- name: Install python
30+
uses: actions/setup-python@v5
31+
with:
32+
python-version: ${{ matrix.python-version }}
33+
architecture: ${{ matrix.architecture }}
34+
35+
- name: Install just
36+
uses: extractions/setup-just@v2
37+
env:
38+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39+
40+
- name: Install dependencies
41+
run: |
42+
just ci-install
43+
44+
- name: Run unit tests
45+
run: |
46+
just test

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

curseforge_api_wrapper/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from curseforge_api_wrapper.client import Client
2+
from curseforge_api_wrapper.models import (
3+
Mod,
4+
File,
5+
Fingerprint,
6+
Category,
7+
SearchResult,
8+
FingerprintResult,
9+
ModFilesResult,
10+
)
11+
12+
__all__ = [
13+
"Client",
14+
"Mod",
15+
"File",
16+
"Fingerprint",
17+
"Category",
18+
"SearchResult",
19+
"FingerprintResult",
20+
"ModFilesResult",
21+
]
22+
23+
# Define some metadata here:
24+
25+
__version__ = "1.0.0"
26+
__author__ = "z0z0r4"

curseforge_api_wrapper/client.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
from typing import Optional, Union, List
2+
from enum import Enum
3+
import json
4+
5+
from curseforge_api_wrapper.models import (
6+
Mod,
7+
File,
8+
Fingerprint,
9+
Category,
10+
SearchResult,
11+
FingerprintResult,
12+
ModFilesResult,
13+
)
14+
from curseforge_api_wrapper.network import request
15+
16+
17+
class ModLoaderType(Enum):
18+
"""
19+
ModLoaderType
20+
21+
0=Any
22+
1=Forge
23+
2=Cauldron
24+
3=LiteLoader
25+
4=Fabric
26+
5=Quilt
27+
6=NeoForge
28+
"""
29+
30+
Any = 0
31+
Forge = 1
32+
Cauldron = 2
33+
LiteLoader = 3
34+
Fabric = 4
35+
Quilt = 5
36+
NeoForge = 6
37+
38+
39+
class ModsSearchSortField(Enum):
40+
Featured = 1
41+
Popularity = 2
42+
LastUpdated = 3
43+
Name = 4
44+
Author = 5
45+
TotalDownloads = 6
46+
Category = 7
47+
GameVersion = 8
48+
EarlyAccess = 9
49+
FeaturedReleased = 10
50+
ReleasedDate = 11
51+
Rating = 12
52+
53+
54+
class SortOrder(Enum):
55+
Asc = "asc"
56+
Desc = "desc"
57+
58+
59+
class Client:
60+
def __init__(self, api_key: str, endpoint: str = "https://api.curseforge.com"):
61+
self.api_key = api_key
62+
self.headers = {"x-api-key": api_key, "Accept": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.54"}
63+
self.endpoint = endpoint
64+
65+
def search_mods(
66+
self,
67+
gameId: int,
68+
classId: Optional[int] = None,
69+
categoryId: Optional[int] = None,
70+
categoryIds: Optional[str] = None,
71+
gameVersion: Optional[str] = None,
72+
gameVersions: Optional[str] = None,
73+
searchFilter: Optional[str] = None,
74+
sortField: Optional[Union[int, ModsSearchSortField]] = None,
75+
sortOrder: Optional[Union[str, SortOrder]] = None,
76+
modLoaderType: Optional[Union[int, ModLoaderType]] = None,
77+
modLoaderTypes: Optional[str] = None,
78+
gameVersionTypeId: Optional[int] = None,
79+
authorId: Optional[int] = None,
80+
primaryAuthorId: Optional[int] = None,
81+
slug: Optional[str] = None,
82+
index: Optional[int] = None,
83+
pageSize: Optional[int] = None,
84+
) -> SearchResult:
85+
url = f"{self.endpoint}/v1/mods/search"
86+
params = {
87+
"gameId": gameId,
88+
"classId": classId,
89+
"categoryId": categoryId,
90+
"categoryIds": categoryIds,
91+
"gameVersion": gameVersion,
92+
"gameVersions": gameVersions,
93+
"searchFilter": searchFilter,
94+
"sortField": (
95+
(sortField if type(sortField) is int else sortField.value)
96+
if sortField
97+
else None
98+
),
99+
"sortOrder": (
100+
(sortOrder if type(sortOrder) is str else sortOrder.value)
101+
if sortOrder
102+
else None
103+
),
104+
"modLoaderType": (
105+
modLoaderType
106+
if type(modLoaderType) is int
107+
else modLoaderType.value if modLoaderType else None
108+
),
109+
"modLoaderTypes": modLoaderTypes,
110+
"gameVersionTypeId": gameVersionTypeId,
111+
"authorId": authorId,
112+
"primaryAuthorId": primaryAuthorId,
113+
"slug": slug,
114+
"index": index,
115+
"pageSize": pageSize,
116+
}
117+
res = request(
118+
url,
119+
headers=self.headers,
120+
params=params,
121+
)
122+
return SearchResult(**res)
123+
124+
def get_mod(self, modId: int) -> Mod:
125+
url = f"{self.endpoint}/v1/mods/{modId}"
126+
res = request(url, headers=self.headers)
127+
return Mod(**res["data"])
128+
129+
def get_mods(self, modIds: List[int]) -> List[Mod]:
130+
url = f"{self.endpoint}/v1/mods"
131+
res = request(url, method="POST", headers=self.headers, json={"modIds": modIds})
132+
return [Mod(**item) for item in res["data"]]
133+
134+
def get_mod_files(
135+
self,
136+
modId: int,
137+
gameVersion: Optional[str] = None,
138+
modLoaderType: Optional[Union[int, ModLoaderType]] = None,
139+
gameVersionTypeId: Optional[int] = None,
140+
index: Optional[int] = None,
141+
pageSize: Optional[int] = None,
142+
) -> ModFilesResult:
143+
"""
144+
Get mod files
145+
"""
146+
url = f"{self.endpoint}/v1/mods/{modId}/files"
147+
res = request(
148+
url,
149+
headers=self.headers,
150+
json={
151+
"gameVersion": gameVersion,
152+
"modLoaderType": (
153+
modLoaderType
154+
if type(modLoaderType) is int
155+
else modLoaderType.value if modLoaderType else None
156+
),
157+
"gameVersionTypeId": gameVersionTypeId,
158+
"index": index,
159+
"pageSize": pageSize,
160+
},
161+
)
162+
return ModFilesResult(**res)
163+
164+
def get_file(self, modId: int, fileId: int) -> File:
165+
url = f"{self.endpoint}/v1/mods/{modId}/files/{fileId}"
166+
res = request(url, headers=self.headers)
167+
return File(**res["data"])
168+
169+
def get_files(self, fileIds: List[int]) -> List[File]:
170+
url = f"{self.endpoint}/v1/mods/files"
171+
res = request(
172+
url, method="POST", headers=self.headers, json={"fileIds": fileIds}
173+
)
174+
return [File(**item) for item in res["data"]]
175+
176+
def get_file_download_url(self, modId: int, fileId: int) -> str:
177+
url = f"{self.endpoint}/v1/mods/{modId}/files/{fileId}/download-url"
178+
res = request(url, headers=self.headers)
179+
return res["data"]
180+
181+
def get_fingerprint(
182+
self, fingerprints: List[int], gameId: Optional[int] = None
183+
) -> Fingerprint:
184+
url = (
185+
f"{self.endpoint}/v1/fingerprints"
186+
if not gameId
187+
else f"{self.endpoint}/v1/fingerprints/{gameId}"
188+
)
189+
data = {"fingerprints": fingerprints}
190+
res = request(url, method="POST", headers=self.headers, json=data)
191+
return FingerprintResult(**res["data"])
192+
193+
194+
def get_categories(
195+
self,
196+
gameId: int,
197+
classId: Optional[int] = None,
198+
classesOnly: Optional[bool] = False,
199+
) -> List[Category]:
200+
url = f"{self.endpoint}/v1/categories"
201+
res = request(
202+
url,
203+
headers=self.headers,
204+
params={"gameId": gameId, "classId": classId, "classesOnly": classesOnly},
205+
)
206+
return [Category(**item) for item in res["data"]]

0 commit comments

Comments
 (0)