Update files.

This commit is contained in:
2026-05-17 17:16:58 +08:00
parent 686edaa51f
commit d1663c96e9
5 changed files with 1662 additions and 1 deletions
+129
View File
@@ -0,0 +1,129 @@
from pathlib import Path
import requests
import click
import os
PAPER_VERSION_API = "https://api.papermc.io/v2/projects/paper/versions/{}"
PAPER_SERVER_JAR_API = "https://api.papermc.io/v2/projects/paper/versions/{}/builds/{}/downloads/paper-{}-{}.jar"
MOJANG_VERSION_MANIFEST_V2 = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"
def download_file(url: str, destination: Path, chunk_size: int = 1024 * 512):
destination.mkdir(parents=True, exist_ok=True)
with requests.get(url, stream=True, timeout=30) as r:
if r.status_code != 200:
raise Exception(f"Download failed: {r.status_code}\nResponse: {r.text}")
total = int(r.headers.get("content-length", 0))
with destination.open(mode="wb", buffering=chunk_size).write(r.content) as f:
if total > 0:
with click.progressbar(length=total, label=f"Downloading {os.path.basename(destination)}") as bar:
for chunk in r.iter_content(chunk_size=chunk_size):
if chunk:
f.write(chunk)
bar.update(len(chunk))
else:
click.echo(f"Downloading {os.path.basename(destination)} (unknown size)")
for chunk in r.iter_content(chunk_size=chunk_size):
if chunk:
f.write(chunk)
def get_specific_version_paper_builds(minecraft_version: str) -> list[dict[str, str]]:
"""
Get specific version of Paper builds
:param minecraft_version:
:return:
"""
url = PAPER_VERSION_API.format(minecraft_version)
try:
r = requests.get(url)
if r.status_code == 200:
return r.json().get("builds", [])
else:
raise Exception("Unable to fetch build version for {}\n"
"Response: {}".format(minecraft_version, r.text))
except requests.exceptions.RequestException as e:
raise Exception("Unable to get paper version from server.\n"
"URL: {}\n"
"Error: {}".format(url, e))
def get_version_list(release=True):
try:
r = requests.get(MOJANG_VERSION_MANIFEST_V2)
if r.status_code == 200:
if release:
return [version.get('id') for version in r.json().get("versions", [])
if version.get("type") == "release" if version.get('id') is not None]
return r.json()["versions"]
else:
raise Exception("Unable to fetch version list.\n"
"Response: {}".format(r.text))
except requests.exceptions.RequestException as e:
raise Exception("Unable to get version list from server.\n"
"URL: {}\n"
"Error: {}".format(MOJANG_VERSION_MANIFEST_V2, e))
def get_latest_version_minecraft(release=True):
version_list = get_version_list(release=release)
ver = version_list[0].get("id") if version_list else None
if ver is None:
raise Exception("Unable to find latest version in version list.\n")
return ver
def download_server_jar(minecraft_version: str, build_version: str, destination: Path, filename: str | None = None):
"""
Download server jar (paper server only)
"""
url = PAPER_SERVER_JAR_API.format(minecraft_version, build_version, minecraft_version, build_version)
if filename:
jar_name = filename
if not jar_name.endswith(".jar"):
jar_name += ".jar"
else:
jar_name = os.path.basename(url)
destination.parent.mkdir(parents=True, exist_ok=True)
destination = Path(destination, jar_name)
try:
download_file(url, destination)
return destination
except Exception as e:
raise Exception("Unable to download server jar for version {}\nURL: {}\nError: {}".format(minecraft_version, url, e))
def get_latest_build_of_version(minecraft_version: str) -> str:
builds = get_specific_version_paper_builds(minecraft_version)
if not builds:
raise Exception(f"No builds found for Paper {minecraft_version}")
# Paper API usually lists builds ascending; latest is the last one
return str(builds[-1])
def download_latest_build_paper_jar(minecraft_version: str, destination_dir: Path, filename: str | None = None):
build = get_latest_build_of_version(minecraft_version)
return download_server_jar(minecraft_version, build, destination_dir, filename=filename)
def download_latest_paper_jar(destination_dir: Path, filename: str | None = None, release: bool = True):
"""
Download latest Minecraft version (release) paper jar
"""
vers = get_version_list(release=release)
if len(vers) == 0:
raise Exception("No versions available for Minecraft (Did the server return wrong response ?)")
latest_mc = vers[0]
return download_latest_build_paper_jar(latest_mc, destination_dir, filename=filename)
+360
View File
@@ -0,0 +1,360 @@
"""
FileSettings
Original author: Kitee Contributors
"""
import contextlib
import copy
import json
from pathlib import Path
def validation_rule(
default=None,
*,
children=None,
write_back_if_not_exist=False,
recover_missing_items=False,
use_same_form=False
):
rule = {
"writeBackIfNotExist": write_back_if_not_exist,
"recoverMissingItems": recover_missing_items,
"useSameForm": use_same_form,
}
if children is not None:
rule["children"] = children
else:
rule["default"] = default
return rule
def required_value(default, *, recover_missing_items=False):
return validation_rule(
default,
write_back_if_not_exist=True,
recover_missing_items=recover_missing_items,
)
def required_section(children):
return validation_rule(children=children, write_back_if_not_exist=True)
def required_list(children, use_same_form=False):
return validation_rule(children=children,
write_back_if_not_exist=True,
use_same_form=use_same_form)
class FileSettings:
"""
Simple Settings Object
IMPORTANT: Settings always use self.data as the main operate data, NOT FROM disk settings file!!!
Usage:
FileSettings.create() -> Create settings file (path is self.path)
FileSettings.load() - > Load settings file
FileSettings.save() -> Save settings file
FileSettings.edit() -> Edit settings file (Auto save when with block completes)
Example:
with FileSettings.edit() as settings:
settings["hello"] = "world"
"""
def __init__(self, path, default, validation_rules=None,
dumps_func=json.dumps, load_func=json.load,
settings_change_callback=None, loader=None):
self.data = copy.deepcopy(default)
self.default = copy.deepcopy(default)
self.path: Path = Path(path)
self.validation_rules = validation_rules
self.dumps_func = dumps_func
self.load_func = load_func
self.loader = loader
self.settings_change_callback = settings_change_callback
def reset(self):
"""Replace self.data with self.default and save"""
self.data = copy.deepcopy(self.default)
self.save()
if callable(self.settings_change_callback):
self.settings_change_callback(self.data)
def __repr__(self):
return f"<FileSettings At {self.path.as_posix()}>"
def create(self, exist_ok=False):
"""
Create settings file (path is self.path, data use default value)
:param exist_ok: If not True, raise exception if file already exists
:return:
"""
self.path.parent.mkdir(parents=True, exist_ok=True)
if not exist_ok and self.path.exists():
raise FileExistsError(f'{self.path} already exists')
self.path.write_text(self._dumps(self.default), encoding="utf-8")
def read_from_exist(self):
self.data = self.load()
def mload(self):
"""Get data from memory"""
return copy.deepcopy(self.data)
def load(self):
"""
Load settings file data into self.data (path is self.path)
:return:
"""
if not self.path.exists():
raise FileNotFoundError(f'{self.path} does not exist')
with self.path.open("r", encoding="utf-8") as settings_file:
if self.loader:
data = self.load_func(settings_file, Loader=self.loader)
else:
data = self.load_func(settings_file)
if isinstance(self.validation_rules, dict):
data = self.validate_data(data, self.default, self.validation_rules)
self.data = copy.deepcopy(data)
return data
def save(self):
"""
Save self.data (data in memory) into settings file
:return:
"""
if not self.path.exists():
raise FileNotFoundError(f'{self.path} does not exist. Create it before saving.')
self.path.write_text(self._dumps(self.data), encoding="utf-8")
def get(self, key, default=None):
return self.data.get(key, default)
def dget(self, key, default=None): # dget -> get_default
return self.default.get(key, default)
def exists(self):
return self.path.exists()
@contextlib.contextmanager
def edit(self):
"""
Edit settings (With auto save)
:return:
"""
if not self.exists():
self.create()
yield self
self.save()
if callable(self.settings_change_callback):
self.settings_change_callback(self)
def validate_data(self, data, default, rules):
"""
Validates data by validating rules.
:param data: dict data
:param default: default sample
:param rules: rules of all keys
:return:
"""
if not isinstance(data, dict):
data = {}
if not isinstance(default, dict):
default = {}
if not isinstance(rules, dict):
return copy.deepcopy(data)
validated = copy.deepcopy(data)
for key, rule in rules.items():
rule_default, options = self._parse_validation_rule(rule)
default_value = copy.deepcopy(default.get(key, rule_default))
if key not in validated:
if options.get("writeBackIfNotExist"):
validated[key] = self._validate_value(
default_value,
default_value,
rule_default,
options,
)
continue
validated[key] = self._validate_value(
validated[key],
default_value,
rule_default,
options,
)
return validated
def update(self, settings):
"""Update self.data with new settings values."""
if not isinstance(settings, dict):
raise TypeError("settings must be a dict")
def update_inner(new, old):
if not isinstance(old, dict):
return
for n_k, n_v in new.items():
if isinstance(n_v, dict) and (n_k in old and isinstance(old[n_k], dict)):
update_inner(n_v, old[n_k])
else:
old[n_k] = copy.deepcopy(n_v)
update_inner(settings, self.data)
if callable(self.settings_change_callback):
self.settings_change_callback(self)
def update_new_settings(self, new_default_settings):
"""Add missing settings to self.default and self.data."""
if not isinstance(new_default_settings, dict):
raise TypeError("new_default_settings must be a dict")
def add_missing(new, old):
if not isinstance(old, dict):
return
for n_k, n_v in new.items():
if isinstance(n_v, dict) and (n_k in old and isinstance(old[n_k], dict)):
add_missing(n_v, old[n_k])
if n_k not in old:
old[n_k] = copy.deepcopy(n_v)
add_missing(new_default_settings, self.default)
add_missing(new_default_settings, self.data)
if callable(self.settings_change_callback):
self.settings_change_callback(self)
@staticmethod
def _parse_validation_rule(rule):
if isinstance(rule, dict) and ("default" in rule or "children" in rule):
options = {
"writeBackIfNotExist": rule.get("writeBackIfNotExist", False),
"recoverMissingItems": rule.get("recoverMissingItems", False), # Recover missing item (only for list)
"useSameForm": rule.get("useSameForm", False), # For list that contains dict item (all keys are same)
}
if "children" in rule:
return rule["children"], options
return rule.get("default"), options
if (
isinstance(rule, (list, tuple))
and len(rule) == 2
and isinstance(rule[1], dict)
):
return rule[0], rule[1]
return rule, {}
def _validate_dict_form(self, sample, target):
for k, v in sample.items():
if target.get(k, None) is None:
target[k] = v
elif isinstance(v, dict) and isinstance(target.get(k, None), dict):
target[k] = self._validate_dict_form(v, target.get(k, {}))
return target
def _validate_value(self, value, default_value, rule_default, options):
if options.get("useSameForm"):
if not isinstance(value, list):
if isinstance(default_value, list):
return copy.deepcopy(default_value)
return []
validated = copy.deepcopy(value)
if options.get("recoverMissingItems") and isinstance(default_value, list):
for item in default_value:
if item not in validated:
validated.append(copy.deepcopy(item))
form = rule_default
if isinstance(form, list):
form = next((item for item in form if isinstance(item, dict)), None)
if not isinstance(form, dict):
return validated
for index, item in enumerate(validated):
if not isinstance(item, dict):
continue
validated[index] = self._validate_dict_form(copy.deepcopy(form), item)
return validated
if isinstance(rule_default, dict):
if not isinstance(value, dict):
value = {}
if not isinstance(default_value, dict):
default_value = {}
return self.validate_data(value, default_value, rule_default)
if isinstance(default_value, list):
if not isinstance(value, list):
return copy.deepcopy(default_value)
validated = copy.deepcopy(value)
if options.get("recoverMissingItems"):
for item in default_value:
if item not in validated:
validated.append(copy.deepcopy(item))
return validated
if default_value is None:
return value
if type(value) is not type(default_value):
return copy.deepcopy(default_value)
return value
def _dumps(self, data):
if not callable(self.dumps_func):
raise TypeError(f"dumps_func must be callable")
return self.dumps_func(data)
def __getitem__(self, key):
return self.data[key]
def __setitem__(self, key, value):
self.data[key] = value
if callable(self.settings_change_callback):
self.settings_change_callback(self)
def __eq__(self, other):
if isinstance(other, FileSettings):
return self.path == other.path and self.data == other.data
if isinstance(other, dict):
return self.data == other
return NotImplemented