modsmith 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.
- modsmith/__init__.py +36 -0
- modsmith/_support.py +81 -0
- modsmith/errors.py +115 -0
- modsmith/instances.py +524 -0
- modsmith/java.py +294 -0
- modsmith/launcher.py +256 -0
- modsmith/loaders/__init__.py +23 -0
- modsmith/loaders/_base.py +157 -0
- modsmith/loaders/_fabric.py +166 -0
- modsmith/loaders/_forge.py +312 -0
- modsmith/loaders/_install_profile.py +359 -0
- modsmith/loaders/_processors.py +223 -0
- modsmith/minecraft/__init__.py +15 -0
- modsmith/minecraft/_game_dir.py +168 -0
- modsmith/minecraft/_maven.py +75 -0
- modsmith/minecraft/_remote.py +115 -0
- modsmith/net/__init__.py +7 -0
- modsmith/net/_download.py +243 -0
- modsmith/net/_resource.py +118 -0
- modsmith/net/_session.py +241 -0
- modsmith/packs/__init__.py +18 -0
- modsmith/packs/_formats.py +262 -0
- modsmith/packs/_model.py +247 -0
- modsmith/platforms/__init__.py +25 -0
- modsmith/platforms/_base.py +312 -0
- modsmith/platforms/_curseforge.py +390 -0
- modsmith/platforms/_modrinth.py +261 -0
- modsmith/progress.py +78 -0
- modsmith/py.typed +0 -0
- modsmith-0.1.0.dist-info/METADATA +285 -0
- modsmith-0.1.0.dist-info/RECORD +33 -0
- modsmith-0.1.0.dist-info/WHEEL +4 -0
- modsmith-0.1.0.dist-info/licenses/LICENSE +21 -0
modsmith/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Modsmith: a Python toolkit for Minecraft modpacks and game files."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from . import (
|
|
6
|
+
errors,
|
|
7
|
+
instances,
|
|
8
|
+
java,
|
|
9
|
+
launcher,
|
|
10
|
+
loaders,
|
|
11
|
+
minecraft,
|
|
12
|
+
net,
|
|
13
|
+
packs,
|
|
14
|
+
platforms,
|
|
15
|
+
progress,
|
|
16
|
+
)
|
|
17
|
+
from .net._session import __version__
|
|
18
|
+
from .progress import CancelToken, Progress
|
|
19
|
+
|
|
20
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"CancelToken",
|
|
24
|
+
"Progress",
|
|
25
|
+
"__version__",
|
|
26
|
+
"errors",
|
|
27
|
+
"instances",
|
|
28
|
+
"java",
|
|
29
|
+
"launcher",
|
|
30
|
+
"loaders",
|
|
31
|
+
"minecraft",
|
|
32
|
+
"net",
|
|
33
|
+
"packs",
|
|
34
|
+
"platforms",
|
|
35
|
+
"progress",
|
|
36
|
+
]
|
modsmith/_support.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Internal helpers for files and names."""
|
|
2
|
+
|
|
3
|
+
import errno
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import IO, Any
|
|
10
|
+
|
|
11
|
+
from platformdirs import user_cache_path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def default_cache_dir() -> Path:
|
|
15
|
+
"""Return the per-user directory used for caches by default."""
|
|
16
|
+
return user_cache_path("modsmith", appauthor=False)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def slugify(name: str, fallback: str = "instance") -> str:
|
|
20
|
+
"""Turn a display name into a lowercase, hyphenated slug.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
name: The display name.
|
|
24
|
+
fallback: Returned when ``name`` has no ASCII letters or digits.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The slug.
|
|
28
|
+
"""
|
|
29
|
+
return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") or fallback
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def copy_replace(source: Path, dest: Path) -> None:
|
|
33
|
+
"""Copy ``source`` over ``dest``, then delete ``source``."""
|
|
34
|
+
shutil.copy2(source, dest)
|
|
35
|
+
source.unlink()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def replace_file(source: Path, dest: Path) -> None:
|
|
39
|
+
"""Rename over ``dest``, copying when the filesystem refuses the rename.
|
|
40
|
+
|
|
41
|
+
Containers such as MSIX can reject renames inside redirected folders with
|
|
42
|
+
``EXDEV`` even though both paths share a directory.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
source: The file to move.
|
|
46
|
+
dest: The file to replace.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
OSError: The rename failed for another reason, or the copy failed.
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
os.replace(source, dest)
|
|
53
|
+
except OSError as e:
|
|
54
|
+
if e.errno != errno.EXDEV:
|
|
55
|
+
raise
|
|
56
|
+
copy_replace(source, dest)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def write_atomic(path: Path, data: bytes | IO[bytes]) -> None:
|
|
60
|
+
"""Write a file through a temporary sibling, creating parent directories.
|
|
61
|
+
|
|
62
|
+
Readers see either the old contents or the new ones, never a partial file.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
path: The file to write.
|
|
66
|
+
data: The contents, or an open file to copy them from.
|
|
67
|
+
"""
|
|
68
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
temp = path.with_name(path.name + ".tmp")
|
|
70
|
+
with open(temp, "wb") as file:
|
|
71
|
+
if isinstance(data, bytes):
|
|
72
|
+
file.write(data)
|
|
73
|
+
else:
|
|
74
|
+
shutil.copyfileobj(data, file)
|
|
75
|
+
replace_file(temp, path)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def write_json(path: Path, data: Any) -> None:
|
|
79
|
+
"""Atomically write indented JSON as UTF-8."""
|
|
80
|
+
text = json.dumps(data, indent=2, ensure_ascii=False)
|
|
81
|
+
write_atomic(path, text.encode("utf-8"))
|
modsmith/errors.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Exceptions raised by modsmith. All of them derive from `ModsmithError`."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .packs import PackEntry
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Cancelled",
|
|
11
|
+
"DownloadError",
|
|
12
|
+
"InstanceError",
|
|
13
|
+
"IntegrityError",
|
|
14
|
+
"JavaError",
|
|
15
|
+
"LauncherError",
|
|
16
|
+
"LoaderError",
|
|
17
|
+
"ModsmithError",
|
|
18
|
+
"NetworkError",
|
|
19
|
+
"PackError",
|
|
20
|
+
"PlatformError",
|
|
21
|
+
"ProcessorError",
|
|
22
|
+
"UnresolvedEntriesError",
|
|
23
|
+
"UnsupportedError",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ModsmithError(Exception):
|
|
28
|
+
"""Base class for every error raised by this package."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Cancelled(ModsmithError):
|
|
32
|
+
"""A `CancelToken` stopped the operation. It can be run again later."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class NetworkError(ModsmithError):
|
|
36
|
+
"""An HTTP request failed.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
message: What went wrong.
|
|
40
|
+
status: The HTTP status code.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
status: The HTTP status code, or None if no response arrived.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, message: str, status: int | None = None) -> None:
|
|
47
|
+
super().__init__(message)
|
|
48
|
+
self.status = status
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DownloadError(ModsmithError):
|
|
52
|
+
"""One or more downloads failed.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
message: A summary of the failure.
|
|
56
|
+
failures: A reason for each file that failed.
|
|
57
|
+
|
|
58
|
+
Attributes:
|
|
59
|
+
failures: A reason for each file that failed.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, message: str, failures: Sequence[str] = ()) -> None:
|
|
63
|
+
super().__init__(message)
|
|
64
|
+
self.failures = list(failures)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class IntegrityError(ModsmithError):
|
|
68
|
+
"""A file does not match its expected size or hash."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class UnsupportedError(ModsmithError):
|
|
72
|
+
"""The requested version, loader or format is not supported."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PackError(ModsmithError):
|
|
76
|
+
"""A modpack archive is invalid."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class PlatformError(ModsmithError):
|
|
80
|
+
"""A platform could not provide the requested data."""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class LoaderError(ModsmithError):
|
|
84
|
+
"""A mod loader could not be installed."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ProcessorError(LoaderError):
|
|
88
|
+
"""A Forge or NeoForge install processor failed."""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class JavaError(ModsmithError):
|
|
92
|
+
"""A Java runtime could not be found or installed."""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class LauncherError(ModsmithError):
|
|
96
|
+
"""The launcher profile data could not be used."""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class InstanceError(ModsmithError):
|
|
100
|
+
"""A game instance could not be read or changed."""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class UnresolvedEntriesError(InstanceError):
|
|
104
|
+
"""A pack still has entries with no download.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
entries: The entries that could not be found.
|
|
108
|
+
|
|
109
|
+
Attributes:
|
|
110
|
+
entries: The entries that could not be found.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(self, entries: "Sequence[PackEntry]") -> None:
|
|
114
|
+
super().__init__(f"{len(entries)} pack entries have no download")
|
|
115
|
+
self.entries = list(entries)
|