modrinth4py 0.0.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.
@@ -0,0 +1,39 @@
1
+ from modrinth4py.client import ModrinthClient
2
+ from modrinth4py.exceptions import ModrinthAPIError
3
+ from modrinth4py.types import (
4
+ DictKV,
5
+ ListDictKV,
6
+ SideSupport,
7
+ ProjectStatus,
8
+ RequestedStatus,
9
+ ProjectType,
10
+ VersionType,
11
+ VersionStatus,
12
+ RequestedVersionStatus,
13
+ DonationUrl,
14
+ NewVersion,
15
+ NewProject,
16
+ ProjectUpdate,
17
+ VersionUpdate,
18
+ GalleryImage,
19
+ )
20
+
21
+ __all__ = [
22
+ "ModrinthClient",
23
+ "ModrinthAPIError",
24
+ "DictKV",
25
+ "ListDictKV",
26
+ "SideSupport",
27
+ "ProjectStatus",
28
+ "RequestedStatus",
29
+ "ProjectType",
30
+ "VersionType",
31
+ "VersionStatus",
32
+ "RequestedVersionStatus",
33
+ "DonationUrl",
34
+ "NewVersion",
35
+ "NewProject",
36
+ "ProjectUpdate",
37
+ "VersionUpdate",
38
+ "GalleryImage",
39
+ ]
modrinth4py/client.py ADDED
@@ -0,0 +1,311 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import threading
6
+ import time
7
+ from concurrent.futures import ThreadPoolExecutor, as_completed
8
+ from contextlib import ExitStack
9
+ from pathlib import Path
10
+ from typing import Optional, List, Any, IO, Iterator, Callable, Dict
11
+
12
+ import requests
13
+ from requests.adapters import HTTPAdapter
14
+ from urllib3 import Retry
15
+ from importlib.metadata import version, PackageNotFoundError
16
+
17
+ from modrinth4py.exceptions import ModrinthAPIError
18
+ from modrinth4py.types import (
19
+ DictKV,
20
+ ListDictKV,
21
+ NewProject,
22
+ NewVersion,
23
+ GalleryImage,
24
+ ProjectUpdate,
25
+ VersionUpdate,
26
+ )
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ try:
31
+ __version__ = version("modrinth4py")
32
+ except PackageNotFoundError:
33
+ __version__ = "unknown"
34
+
35
+ def _cut_game_versions_until(cutoff_version: str, versions: List[DictKV]) -> List[DictKV]:
36
+ result: List[DictKV] = []
37
+ for version in versions:
38
+ result.append(version)
39
+ if version.get("version") == cutoff_version:
40
+ break
41
+ return result
42
+
43
+
44
+ class ModrinthClient:
45
+ """
46
+ HTTP client for the Modrinth API.
47
+
48
+ Rate limiting is handled automatically. Requests are retried on 429/5xx responses.
49
+ For concurrent workloads, use :meth:`parallel_requests`.
50
+
51
+ :param token: Modrinth API token (PAT).
52
+ :param api_url: Base URL for the API. Defaults to ``https://api.modrinth.com``.
53
+ :param user_agent: Value sent as the ``User-Agent`` header.
54
+ :param debug: Enable debug-level request/response logging via stdlib ``logging``.
55
+ :param verbose_debug: Also log full request bodies (implies ``debug=True``).
56
+ """
57
+
58
+ DEFAULT_API_URL = "https://api.modrinth.com"
59
+
60
+ def __init__(
61
+ self,
62
+ token: str,
63
+ api_url: str = DEFAULT_API_URL,
64
+ user_agent: str = f"modrinth4py/{__version__}",
65
+ debug: bool = False,
66
+ verbose_debug: bool = False,
67
+ ) -> None:
68
+ self.api_url = api_url
69
+ self._debug = debug or verbose_debug
70
+ self._verbose_debug = verbose_debug
71
+
72
+ self._session = requests.Session()
73
+ self._session.headers.update({"Authorization": token, "User-Agent": user_agent})
74
+
75
+ self._ratelimit_lock = threading.Lock()
76
+ self._ratelimit_limit = 300
77
+ self._ratelimit_remaining = 300
78
+ self._ratelimit_reset = 0
79
+
80
+ def _make_thread_session(self) -> requests.Session:
81
+ s = requests.Session()
82
+ s.headers.update(self._session.headers)
83
+ s.headers["Connection"] = "close"
84
+
85
+ retry = Retry(
86
+ total=10,
87
+ backoff_factor=0.3,
88
+ status_forcelist=[429, 500, 502, 503, 504],
89
+ )
90
+ adapter = HTTPAdapter(max_retries=retry, pool_connections=1, pool_maxsize=1)
91
+ s.mount("https://", adapter)
92
+ return s
93
+
94
+ def _update_ratelimit(self, response: requests.Response) -> None:
95
+ with self._ratelimit_lock:
96
+ try:
97
+ self._ratelimit_limit = int(response.headers.get("X-Ratelimit-Limit", self._ratelimit_limit))
98
+ self._ratelimit_remaining = int(response.headers.get("X-Ratelimit-Remaining", self._ratelimit_remaining))
99
+ self._ratelimit_reset = int(response.headers.get("X-Ratelimit-Reset", 0))
100
+ except (ValueError, TypeError):
101
+ pass
102
+
103
+ if self._debug:
104
+ logger.debug(
105
+ "rate limit updated: limit=%d remaining=%d reset=%d",
106
+ self._ratelimit_limit,
107
+ self._ratelimit_remaining,
108
+ self._ratelimit_reset,
109
+ )
110
+
111
+ def _respect_ratelimit(self) -> None:
112
+ with self._ratelimit_lock:
113
+ if self._ratelimit_remaining > 0:
114
+ return
115
+
116
+ sleep_time = self._ratelimit_reset
117
+ logger.warning("rate limited - sleeping %d seconds", sleep_time)
118
+ self._ratelimit_remaining = self._ratelimit_limit
119
+ self._ratelimit_reset = 0
120
+
121
+ if sleep_time > 0:
122
+ time.sleep(sleep_time)
123
+
124
+ def _log_request(self, prepped: requests.PreparedRequest) -> None:
125
+ logger.debug("%s %s", prepped.method, prepped.url)
126
+ for k, v in prepped.headers.items():
127
+ if k.lower() == "authorization":
128
+ v = f"{v[:10]}..." if len(v) > 10 else "***"
129
+ logger.debug(" %s: %s", k, v)
130
+
131
+ if prepped.body is None:
132
+ logger.debug(" body: (none)")
133
+ elif "json" in prepped.headers.get("Content-Type", "").lower() and isinstance(prepped.body, bytes):
134
+ try:
135
+ logger.debug(" body: %s", json.dumps(json.loads(prepped.body.decode()), indent=2))
136
+ except Exception:
137
+ logger.debug(" body (raw): %r", prepped.body)
138
+ elif isinstance(prepped.body, (str, bytes)):
139
+ preview = prepped.body[:200] if isinstance(prepped.body, str) else prepped.body[:200].decode(errors="replace")
140
+ logger.debug(" body: %s", preview)
141
+ else:
142
+ logger.debug(" body: <binary/multipart>")
143
+
144
+ def _request(self, method: str, endpoint: str, api_version: int = 2, **kwargs) -> Any:
145
+ url = f"{self.api_url}/v{api_version}{endpoint}"
146
+
147
+ thread = threading.current_thread()
148
+ session: Optional[requests.Session] = getattr(thread, "_modrinth4py_session", None)
149
+ if session is None:
150
+ session = self._make_thread_session()
151
+ setattr(thread, "_modrinth4py_session", session)
152
+
153
+ self._respect_ratelimit()
154
+
155
+ response: Optional[requests.Response] = None
156
+ try:
157
+ prepped = session.prepare_request(requests.Request(method, url, **kwargs))
158
+
159
+ if self._verbose_debug:
160
+ self._log_request(prepped)
161
+ elif self._debug:
162
+ logger.debug("%s %s", method, url)
163
+
164
+ settings = session.merge_environment_settings(prepped.url, {}, None, None, None)
165
+ response = session.send(prepped, **settings)
166
+
167
+ if self._debug:
168
+ logger.debug("response: status=%d length=%d", response.status_code, len(response.content))
169
+
170
+ self._update_ratelimit(response)
171
+ response.raise_for_status()
172
+ return response.json() if response.text else {}
173
+
174
+ except requests.HTTPError as exc:
175
+ if response is not None:
176
+ logger.error("HTTP %d: %s", response.status_code, response.text[:500])
177
+ try:
178
+ error_body = response.json() if response is not None else {}
179
+ except Exception:
180
+ error_body = {"error": response.text if response else "no response"}
181
+
182
+ raise ModrinthAPIError(
183
+ str(exc),
184
+ status_code=response.status_code if response is not None else None,
185
+ response=error_body,
186
+ ) from exc
187
+
188
+ @staticmethod
189
+ def _to_dict(obj: Any) -> DictKV:
190
+ return {k: v for k, v in obj.__dict__.items() if v is not None}
191
+
192
+ @staticmethod
193
+ def _open_files(paths: List[Path]) -> Iterator[Dict[str, IO[bytes]]]:
194
+ with ExitStack() as stack:
195
+ yield {f"file{i}": stack.enter_context(p.open("rb")) for i, p in enumerate(paths)}
196
+
197
+ def get_project(self, id_or_slug: str) -> DictKV:
198
+ """Fetch a project by its ID or slug."""
199
+ return self._request("GET", f"/project/{id_or_slug}")
200
+
201
+ def create_project(self, project: NewProject, icon_path: Optional[Path] = None) -> DictKV:
202
+ """Create a new project. Optionally attach an icon."""
203
+ payload = self._to_dict(project)
204
+ if "donation_urls" in payload:
205
+ payload["donation_urls"] = [du.__dict__ for du in payload["donation_urls"]]
206
+
207
+ if icon_path:
208
+ with icon_path.open("rb") as icon_file:
209
+ return self._request("POST", "/project", data={"data": json.dumps(payload)}, files={"icon": icon_file})
210
+ return self._request("POST", "/project", data={"data": json.dumps(payload)})
211
+
212
+ def modify_project(self, id_or_slug: str, update: ProjectUpdate) -> None:
213
+ """Partially update a project."""
214
+ self._request("PATCH", f"/project/{id_or_slug}", json=self._to_dict(update))
215
+
216
+ def change_project_icon(self, id_or_slug: str, icon_path: Path, ext: str) -> None:
217
+ """Replace the icon of a project."""
218
+ with icon_path.open("rb") as icon_file:
219
+ self._request("PATCH", f"/project/{id_or_slug}/icon", params={"ext": ext}, data=icon_file)
220
+
221
+ def get_project_versions(self, id_or_slug: str) -> ListDictKV:
222
+ """List all versions of a project."""
223
+ return self._request("GET", f"/project/{id_or_slug}/version")
224
+
225
+ def get_version(self, version_id: str) -> DictKV:
226
+ """Fetch a single version by its ID."""
227
+ return self._request("GET", f"/version/{version_id}")
228
+
229
+ def create_version(
230
+ self,
231
+ version: NewVersion,
232
+ file_paths: List[Path],
233
+ primary_file: Optional[str] = None,
234
+ ) -> DictKV:
235
+ """Upload a new version with one or more files."""
236
+ with ExitStack() as stack:
237
+ files = {f"file{i}": stack.enter_context(p.open("rb")) for i, p in enumerate(file_paths)}
238
+ payload: DictKV = {**version.__dict__, "file_parts": list(files.keys())}
239
+ if primary_file:
240
+ payload["primary_file"] = primary_file
241
+ return self._request("POST", "/version", data={"data": json.dumps(payload)}, files=files)
242
+
243
+ def modify_version(self, version_id: str, update: VersionUpdate) -> None:
244
+ """Partially update a version."""
245
+ self._request("PATCH", f"/version/{version_id}", json=self._to_dict(update))
246
+
247
+ def delete_version(self, version_id: str) -> None:
248
+ """Delete a version permanently."""
249
+ self._request("DELETE", f"/version/{version_id}")
250
+
251
+ def add_gallery_image(self, id_or_slug: str, image: GalleryImage) -> None:
252
+ """Upload an image to a project's gallery."""
253
+ params: DictKV = {"ext": image.ext, "featured": str(image.featured).lower()}
254
+ if image.title:
255
+ params["title"] = image.title
256
+ if image.description:
257
+ params["description"] = image.description
258
+ if image.ordering is not None:
259
+ params["ordering"] = image.ordering
260
+
261
+ with image.image_path.open("rb") as img_file:
262
+ self._request("POST", f"/project/{id_or_slug}/gallery", params=params, data=img_file)
263
+
264
+ def delete_gallery_image(self, id_or_slug: str, image_url: str) -> None:
265
+ """Remove an image from a project's gallery by its URL."""
266
+ self._request("DELETE", f"/project/{id_or_slug}/gallery", params={"url": image_url})
267
+
268
+ def get_organization_projects(self, organization_id: str) -> ListDictKV:
269
+ """List all projects belonging to an organization."""
270
+ return self._request("GET", f"/organization/{organization_id}/projects", api_version=3)
271
+
272
+ def get_game_versions(self) -> ListDictKV:
273
+ """Return all known game versions from the Modrinth tag API."""
274
+ return self._request("GET", "/tag/game_version")
275
+
276
+ def get_game_versions_until(self, cutoff_version: str) -> ListDictKV:
277
+ """Return game versions from newest down to and including ``cutoff_version``."""
278
+ return _cut_game_versions_until(cutoff_version, self.get_game_versions())
279
+
280
+ def get_loaders(self) -> ListDictKV:
281
+ """Return all known mod loaders from the Modrinth tag API."""
282
+ return self._request("GET", "/tag/loader")
283
+
284
+ @staticmethod
285
+ def parallel_requests(requests_list: List[Callable[[], Any]], max_parallel: int = 6) -> List[Any]:
286
+ """
287
+ Execute a list of callables in parallel using a thread pool.
288
+
289
+ Each callable should be a zero-argument function that calls one of the
290
+ client methods (e.g. ``lambda: client.get_project("sodium")``).
291
+ Results are returned in the same order as the input list.
292
+ Raises the first exception encountered, if any.
293
+ """
294
+ results: List[Any] = [None] * len(requests_list)
295
+
296
+ def _wrap(fn: Callable[[], Any]) -> Any:
297
+ try:
298
+ return fn()
299
+ except Exception as exc:
300
+ return exc
301
+
302
+ with ThreadPoolExecutor(max_workers=max_parallel) as executor:
303
+ future_to_index = {executor.submit(_wrap, fn): i for i, fn in enumerate(requests_list)}
304
+ for future in as_completed(future_to_index):
305
+ results[future_to_index[future]] = future.result()
306
+
307
+ for result in results:
308
+ if isinstance(result, Exception):
309
+ raise result
310
+
311
+ return results
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, Dict, Any
4
+
5
+
6
+ class ModrinthAPIError(Exception):
7
+ def __init__(
8
+ self,
9
+ message: str,
10
+ status_code: Optional[int] = None,
11
+ response: Optional[Dict[str, Any]] = None,
12
+ ) -> None:
13
+ super().__init__(message)
14
+ self.status_code = status_code
15
+ self.response = response or {}
modrinth4py/types.py ADDED
@@ -0,0 +1,159 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from pathlib import Path
6
+ from typing import Optional, List, Dict, Any
7
+
8
+ DictKV = Dict[str, Any]
9
+ ListDictKV = List[DictKV]
10
+
11
+
12
+ class SideSupport(str, Enum):
13
+ REQUIRED = "required"
14
+ OPTIONAL = "optional"
15
+ UNSUPPORTED = "unsupported"
16
+ UNKNOWN = "unknown"
17
+
18
+
19
+ class ProjectStatus(str, Enum):
20
+ APPROVED = "approved"
21
+ ARCHIVED = "archived"
22
+ REJECTED = "rejected"
23
+ DRAFT = "draft"
24
+ UNLISTED = "unlisted"
25
+ PROCESSING = "processing"
26
+ WITHHELD = "withheld"
27
+ SCHEDULED = "scheduled"
28
+ PRIVATE = "private"
29
+ UNKNOWN = "unknown"
30
+
31
+
32
+ class RequestedStatus(str, Enum):
33
+ APPROVED = "approved"
34
+ ARCHIVED = "archived"
35
+ UNLISTED = "unlisted"
36
+ PRIVATE = "private"
37
+ DRAFT = "draft"
38
+
39
+
40
+ class ProjectType(str, Enum):
41
+ MOD = "mod"
42
+ MODPACK = "modpack"
43
+ RESOURCEPACK = "resourcepack"
44
+
45
+
46
+ class VersionType(str, Enum):
47
+ RELEASE = "release"
48
+ BETA = "beta"
49
+ ALPHA = "alpha"
50
+
51
+
52
+ class VersionStatus(str, Enum):
53
+ LISTED = "listed"
54
+ ARCHIVED = "archived"
55
+ DRAFT = "draft"
56
+ UNLISTED = "unlisted"
57
+ SCHEDULED = "scheduled"
58
+ UNKNOWN = "unknown"
59
+
60
+
61
+ class RequestedVersionStatus(str, Enum):
62
+ LISTED = "listed"
63
+ ARCHIVED = "archived"
64
+ DRAFT = "draft"
65
+ UNLISTED = "unlisted"
66
+
67
+
68
+ @dataclass
69
+ class DonationUrl:
70
+ id: str
71
+ platform: str
72
+ url: str
73
+
74
+
75
+ @dataclass
76
+ class NewVersion:
77
+ name: str
78
+ version_number: str
79
+ project_id: str
80
+ game_versions: List[str]
81
+ loaders: List[str]
82
+ version_type: VersionType
83
+ featured: bool = True
84
+ changelog: Optional[str] = None
85
+ dependencies: Optional[List[DictKV]] = None
86
+ status: VersionStatus = VersionStatus.LISTED
87
+ requested_status: Optional[RequestedVersionStatus] = None
88
+
89
+
90
+ @dataclass
91
+ class NewProject:
92
+ slug: str
93
+ title: str
94
+ description: str
95
+ categories: List[str]
96
+ client_side: SideSupport
97
+ server_side: SideSupport
98
+ body: str
99
+ project_type: ProjectType
100
+ initial_versions: Optional[List[NewVersion]] = field(default_factory=list)
101
+ additional_categories: Optional[List[str]] = None
102
+ issues_url: Optional[str] = None
103
+ source_url: Optional[str] = None
104
+ organization_id: Optional[str] = None
105
+ wiki_url: Optional[str] = None
106
+ discord_url: Optional[str] = None
107
+ donation_urls: Optional[List[DonationUrl]] = None
108
+ requested_status: Optional[RequestedStatus] = RequestedStatus.UNLISTED
109
+ status: Optional[RequestedStatus] = RequestedStatus.DRAFT
110
+ license_id: Optional[str] = None
111
+ license_url: Optional[str] = None
112
+ is_draft: bool = True
113
+
114
+
115
+ @dataclass
116
+ class ProjectUpdate:
117
+ slug: Optional[str] = None
118
+ title: Optional[str] = None
119
+ description: Optional[str] = None
120
+ categories: Optional[List[str]] = None
121
+ client_side: Optional[str] = None
122
+ server_side: Optional[str] = None
123
+ body: Optional[str] = None
124
+ status: Optional[str] = None
125
+ requested_status: Optional[str] = None
126
+ additional_categories: Optional[List[str]] = None
127
+ issues_url: Optional[str] = None
128
+ source_url: Optional[str] = None
129
+ wiki_url: Optional[str] = None
130
+ discord_url: Optional[str] = None
131
+ donation_urls: Optional[List[Dict[str, Any]]] = None
132
+ license_id: Optional[str] = None
133
+ license_url: Optional[str] = None
134
+ moderation_message: Optional[str] = None
135
+ moderation_message_body: Optional[str] = None
136
+
137
+
138
+ @dataclass
139
+ class VersionUpdate:
140
+ name: Optional[str] = None
141
+ version_number: Optional[str] = None
142
+ changelog: Optional[str] = None
143
+ dependencies: Optional[List[DictKV]] = None
144
+ game_versions: Optional[List[str]] = None
145
+ version_type: Optional[VersionType] = None
146
+ loaders: Optional[List[str]] = None
147
+ featured: Optional[bool] = None
148
+ status: Optional[VersionStatus] = None
149
+ requested_status: Optional[RequestedVersionStatus] = None
150
+
151
+
152
+ @dataclass
153
+ class GalleryImage:
154
+ image_path: Path
155
+ ext: str
156
+ featured: bool
157
+ title: Optional[str] = None
158
+ description: Optional[str] = None
159
+ ordering: Optional[int] = None
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: modrinth4py
3
+ Version: 0.0.1
4
+ Summary: A Python API client for the Modrinth API
5
+ Author-email: rotgruengelb <daniel+modrinth4py@rotgruengelb.net>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/rotgruengelb/modrinth4py
8
+ Project-URL: Documentation, https://github.com/rotgruengelb/modrinth4py#readme
9
+ Project-URL: Source, https://github.com/rotgruengelb/modrinth4py
10
+ Project-URL: Tracker, https://github.com/rotgruengelb/modrinth4py/issues
11
+ Keywords: modrinth,api,client,minecraft,mods
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Development Status :: 4 - Beta
17
+ Requires-Python: >=3.12
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: requests>=2.32.5
21
+ Dynamic: license-file
22
+
23
+ # modrinth4py
24
+
25
+ A Python API client for the [Modrinth](https://modrinth.com) API.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install modrinth4py
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```python
36
+ from modrinth4py import ModrinthClient, NewProject, SideSupport, ProjectType
37
+
38
+ client = ModrinthClient(token="your-modrinth-pat")
39
+
40
+ project = client.get_project("sodium")
41
+ print(project["title"])
42
+ ```
43
+
44
+ ### Creating a project
45
+
46
+ ```python
47
+ from modrinth4py import NewProject, SideSupport, ProjectType, RequestedStatus
48
+
49
+ new_project = NewProject(
50
+ slug="my-mod",
51
+ title="My Mod",
52
+ description="A short description",
53
+ categories=["utility"],
54
+ client_side=SideSupport.REQUIRED,
55
+ server_side=SideSupport.OPTIONAL,
56
+ body="## My Mod\nLong-form description here.",
57
+ project_type=ProjectType.MOD,
58
+ )
59
+
60
+ result = client.create_project(new_project)
61
+ ```
62
+
63
+ ### Uploading a version
64
+
65
+ ```python
66
+ from pathlib import Path
67
+ from modrinth4py import NewVersion, VersionType
68
+
69
+ version = NewVersion(
70
+ name="1.0.0",
71
+ version_number="1.0.0",
72
+ project_id="your-id",
73
+ game_versions=["1.21"],
74
+ loaders=["fabric"],
75
+ version_type=VersionType.RELEASE,
76
+ changelog="Initial release.",
77
+ )
78
+
79
+ client.create_version(version, file_paths=[Path("my-mod-1.0.0.jar")])
80
+ ```
81
+
82
+ ### Modifying a project
83
+
84
+ ```python
85
+ from modrinth4py import ModrinthClient, ProjectUpdate
86
+
87
+ client.modify_project("your-slug", ProjectUpdate(
88
+ discord_url="https://discord.gg/your-invite",
89
+ license_id="MIT",
90
+ ))
91
+ ```
92
+
93
+ ### Parallel requests
94
+
95
+ ```python
96
+ projects = ModrinthClient.parallel_requests([
97
+ lambda: client.get_project("sodium"),
98
+ lambda: client.get_project("lithium"),
99
+ lambda: client.get_project("iris"),
100
+ ])
101
+ ```
102
+
103
+ ### Logging
104
+
105
+ `modrinth4py` uses Python's standard `logging` module under the `modrinth4py.client` logger.
106
+ Pass `debug=True` to `ModrinthClient` to enable debug output, or configure your own handler:
107
+
108
+ ```python
109
+ import logging
110
+ logging.basicConfig(level=logging.DEBUG)
111
+ ```
112
+
113
+ ## License
114
+
115
+ MIT
@@ -0,0 +1,9 @@
1
+ modrinth4py/__init__.py,sha256=Cp3MxwrWb4gmpeQ1qKI_ZZ4S1STEh6sFZ8LlmsrxBBc,764
2
+ modrinth4py/client.py,sha256=sGZgxWkDFssVput4FsQPI_j3nFjcnot3m0vmhekbAXY,12574
3
+ modrinth4py/exceptions.py,sha256=S_IFnnp6RFiovbq8K59UMIAF3mXsY8hQAI9-l1DfpVY,386
4
+ modrinth4py/types.py,sha256=fEtqSPbFgkO_RejCuRotEYntZCnNWlcNFo5WShOnXGI,4058
5
+ modrinth4py-0.0.1.dist-info/licenses/LICENSE,sha256=IL7dz7u4PKqCktqBnjQPytohvbRCQHJGJOKt47Fdr4g,1078
6
+ modrinth4py-0.0.1.dist-info/METADATA,sha256=lGrUnp8XZoa2lqJMLteFKrpAN4P6aYD2h5OqRy3yewk,2822
7
+ modrinth4py-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ modrinth4py-0.0.1.dist-info/top_level.txt,sha256=1b3U9jcyKxeA-JEDg-XY7zm6kmRAI12ggEY2ufgndlI,12
9
+ modrinth4py-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 rotgruengelb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1 @@
1
+ modrinth4py