🚨 Fix most ruff warnings
All checks were successful
CI Pipeline / Check Templates (PR) (push) Has been skipped
pre-commit / build (push) Successful in 51s
CI Pipeline / Package and Sign Mod (push) Has been skipped
CI Pipeline / Check Templates (All) (push) Successful in 1m23s

This commit is contained in:
Stanislas Daniel Claude Dolcini 2024-09-11 15:26:41 +03:00
parent 6c752be00e
commit f99ad88998
Signed by: Stan
GPG Key ID: 244943DFF8370D60
7 changed files with 208 additions and 168 deletions

View File

@ -7,6 +7,7 @@ line-ending = "lf"
select = ["ALL"]
ignore = [
"ANN",
"BLE001",
"C90",
"COM812",
"D10",
@ -40,6 +41,7 @@ ignore = [
"TRY002",
"TRY003",
"TRY004",
"TRY301",
"UP038",
"W505"
]

View File

@ -1,16 +1,17 @@
import json
from pathlib import Path
MOD_PATH = Path("community-mod")
def check_cwd_is_correct():
cwd = Path().resolve()
try:
if not Path(".git").exists():
raise Exception("No .git in current folder.")
with open(MOD_PATH / "mod.json", "r") as f:
mod = json.load(f)
if mod['name'] != "community-mod":
raise Exception("mod.json has incorrect name")
except:
raise Exception("scripts.py must be called from the community mod repository")
try:
if not Path(".git").exists():
raise Exception("No .git in current folder.")
with open(MOD_PATH / "mod.json") as f:
mod = json.load(f)
if mod["name"] != "community-mod":
raise Exception("mod.json has incorrect name")
except Exception as e:
raise Exception("scripts.py must be called from the community mod repository") from e

View File

@ -1,43 +1,42 @@
import argparse
import shutil
import json
import shutil
from pathlib import Path
from . import MOD_PATH, check_cwd_is_correct
PUBLIC_PATH = Path("binaries/data/mods/public/")
DEFAULT_COPY = [
"simulation/data",
"simulation/templates"
]
DEFAULT_COPY = ["simulation/data", "simulation/templates"]
def validate_path(path: str):
mod_path = Path(path) / PUBLIC_PATH
try:
with open(mod_path / "mod.json", "r", encoding="utf-8") as f:
mod = json.load(f)
if mod['name'] != "0ad":
raise Exception("mod.json has incorrect name")
except:
raise Exception(f"path '{path}' does not point to a 0 A.D. SVN folder.")
return mod_path
mod_path = Path(path) / PUBLIC_PATH
try:
with open(mod_path / "mod.json", encoding="utf-8") as f:
mod = json.load(f)
if mod["name"] != "0ad":
raise Exception("mod.json has incorrect name")
except Exception as e:
raise Exception(f"path '{path}' does not point to a 0 A.D. SVN folder.") from e
return mod_path
def copy_0ad_files(path_0ad: Path, to_copy = DEFAULT_COPY):
for path in to_copy:
shutil.copytree(path_0ad / path, MOD_PATH / path, dirs_exist_ok=False)
def copy_0ad_files(path_0ad: Path, to_copy=DEFAULT_COPY):
for path in to_copy:
shutil.copytree(path_0ad / path, MOD_PATH / path, dirs_exist_ok=False)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Copy files from 0 A.D. to the community mod.')
parser.add_argument('-0ad', help='Path to the 0 A.D. folder')
parser.add_argument('-p','--path', nargs='*', help='Optionally, a list of paths to copy.')
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Copy files from 0 A.D. to the community mod.")
parser.add_argument("-0ad", help="Path to the 0 A.D. folder")
parser.add_argument("-p", "--path", nargs="*", help="Optionally, a list of paths to copy.")
args = parser.parse_args()
args = parser.parse_args()
check_cwd_is_correct()
path = validate_path(getattr(args, '0ad'))
copy_0ad_files(path, args.folder or DEFAULT_COPY)
check_cwd_is_correct()
path = validate_path(getattr(args, "0ad"))
copy_0ad_files(path, args.folder or DEFAULT_COPY)
else:
raise Exception("Must be called directly")
raise Exception("Must be called directly")

View File

@ -1,12 +1,13 @@
#!/usr/bin/env python3
import argparse
from os import chdir
from pathlib import Path
from subprocess import run, CalledProcessError
from sys import exit
from xml.etree import ElementTree
from .scriptlib import SimulTemplateEntity, find_files
import logging
from pathlib import Path
from subprocess import CalledProcessError, run
from sys import exit
from xml.etree import ElementTree as ET
from .scriptlib import SimulTemplateEntity, find_files
logger = logging.getLogger(__name__)
@ -15,37 +16,54 @@ relaxng_schema = root / "scripts" / "entity.rng"
vfs_root = root
mod = "community-mod"
def main():
if not relaxng_schema.exists():
print(f"""Relax NG schema non existant.
Please create the file {relaxng_schema.relative_to(root)}
You can do that by running 'pyrogenesis -dumpSchema' in the 'system' directory""")
exit(1)
if run(['xmllint', '--version'], capture_output=True).returncode != 0:
if run(["xmllint", "--version"], capture_output=True, check=False).returncode != 0:
print("xmllint not found in your PATH, please install it (usually in libxml2 package)")
exit(2)
parser = argparse.ArgumentParser(description='Validate templates')
parser.add_argument('-p','--path', nargs='*', help='Optionally, a list of templates to validate.')
parser = argparse.ArgumentParser(description="Validate templates")
parser.add_argument(
"-p", "--path", nargs="*", help="Optionally, a list of templates to validate."
)
args = parser.parse_args()
simul_templates_path = Path('simulation/templates')
simul_templates_path = Path("simulation/templates")
simul_template_entity = SimulTemplateEntity(vfs_root, logger)
count = 0
failed = 0
templates = sorted([(Path(p), None) for p in args.path]) if args.path else sorted(find_files(vfs_root, [mod], 'simulation/templates', 'xml'))
templates = []
if args.path is not None:
templates = sorted([(Path(p), None) for p in args.path])
else:
templates = sorted(find_files(vfs_root, [mod], "simulation/templates", "xml"))
for fp, _ in templates:
if fp.stem.startswith('template_'):
if fp.stem.startswith("template_"):
continue
path = fp.as_posix()
if path.startswith('simulation/templates/mixins/') or path.startswith("simulation/templates/special/"):
if path.startswith(("simulation/templates/mixins/", "simulation/templates/special/")):
continue
print(f"# {fp}...")
count += 1
entity = simul_template_entity.load_inherited(simul_templates_path, str(fp.relative_to(simul_templates_path)), [mod])
xmlcontent = ElementTree.tostring(entity, encoding='unicode')
entity = simul_template_entity.load_inherited(
simul_templates_path, str(fp.relative_to(simul_templates_path)), [mod]
)
xmlcontent = ET.tostring(entity, encoding="unicode")
try:
run(['xmllint', '--relaxng', str(relaxng_schema.resolve()), '-'], input=xmlcontent, capture_output=True, text=True, check=True)
run(
["xmllint", "--relaxng", str(relaxng_schema.resolve()), "-"],
input=xmlcontent,
capture_output=True,
text=True,
check=True,
)
except CalledProcessError as e:
failed += 1
print(e.stderr)
@ -53,5 +71,5 @@ You can do that by running 'pyrogenesis -dumpSchema' in the 'system' directory""
print(f"\nTotal: {count}; failed: {failed}")
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -1,13 +1,16 @@
import json
from . import MOD_PATH, check_cwd_is_correct
def get_version():
with open(MOD_PATH / "mod.json", "r") as f:
mod = json.load(f)
print(mod['version'])
if __name__ == '__main__':
check_cwd_is_correct()
get_version()
def get_version():
with open(MOD_PATH / "mod.json") as f:
mod = json.load(f)
print(mod["version"])
if __name__ == "__main__":
check_cwd_is_correct()
get_version()
else:
raise Exception("Must be called directly")
raise Exception("Must be called directly")

View File

@ -1,36 +1,42 @@
import json
import os
import requests
from . import MOD_PATH
import subprocess
import requests
from . import MOD_PATH
def run_git_command(command):
""" Run a git command and return its output. """
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
"""Run a git command and return its output."""
result = subprocess.run(command, capture_output=True, text=True, check=False)
if result.returncode != 0:
raise Exception(f"Git command failed: {result.stderr}")
return result.stdout.strip()
def get_current_commit():
""" Get the current commit hash. """
return run_git_command(['git', 'rev-parse', 'HEAD'])
"""Get the current commit hash."""
return run_git_command(["git", "rev-parse", "HEAD"])
def get_commit_for_tag(tag):
""" Get the commit hash for a tag, or return None if the tag doesn't exist. """
"""Get the commit hash for a tag, or return None if the tag doesn't exist."""
try:
commit_hash = run_git_command(['git', 'rev-list', '-n', '1', tag])
return commit_hash
except Exception:
return run_git_command(["git", "rev-list", "-n", "1", tag])
except Exception as e:
print(f"Failed to get commit for tag {tag}: {e}")
return None
def get_previous_tag_in_series(tag):
""" Get the previous tag in the same minor version series (e.g., 0.26.X-1). """
"""Get the previous tag in the same minor version series (e.g., 0.26.X-1)."""
try:
version_array = tag.split('.')
version_array = tag.split(".")
# Extract the major.minor part of the tag (e.g., '0.26' from '0.26.11')
major_minor_version = '.'.join(version_array[:2])
major_minor_version = ".".join(version_array[:2])
# List all tags, reverse sorted by version number
tags = run_git_command(['git', 'tag', '--sort=-v:refname']).splitlines()
tags = run_git_command(["git", "tag", "--sort=-v:refname"]).splitlines()
# Filter tags that match the major.minor version
filtered_tags = [t for t in tags if t.startswith(major_minor_version)]
if tag in filtered_tags:
@ -39,28 +45,30 @@ def get_previous_tag_in_series(tag):
return filtered_tags[current_index + 1]
version_array[2] = str(int(version_array[2]) - 1)
manual_tag = '.'.join(version_array)
if manual_tag in filtered_tags:
current_index = filtered_tags.index(manual_tag)
return filtered_tags[current_index]
return None
manual_tag = ".".join(version_array)
if manual_tag not in filtered_tags:
return None
current_index = filtered_tags.index(manual_tag)
return filtered_tags[current_index]
except Exception as e:
raise Exception(f"Failed to get previous tag in series: {e}")
raise Exception(f"Failed to get previous tag in series: {e}") from e
def get_changelog(current_commit, previous_tag):
""" Get the changelog between the current commit and the previous tag. """
"""Get the changelog between the current commit and the previous tag."""
try:
if previous_tag is not None:
changelog = run_git_command(['git', 'log', f'{previous_tag}..{current_commit}', '--oneline'])
else:
# No previous tag, so get the log from the start of the repository
changelog = run_git_command(['git', 'log', current_commit, '--oneline'])
return changelog
return run_git_command(
["git", "log", f"{previous_tag}..{current_commit}", "--oneline"]
)
# No previous tag, so get the log from the start of the repository
return run_git_command(["git", "log", current_commit, "--oneline"])
except Exception as e:
raise Exception(f"Failed to generate changelog: {e}")
raise Exception(f"Failed to generate changelog: {e}") from e
api_key = os.getenv('MODIO_API_KEY')
api_key = os.getenv("MODIO_API_KEY")
# This must be generated manually from the website's interface.
oauth2_token = os.getenv("MODIO_OAUTH2_TOKEN")
@ -72,9 +80,11 @@ mod_name = os.getenv("MOD_NAME")
community_mod_id = 2144305
zeroad_id = 5
mod_json = json.load(open(MOD_PATH / 'mod.json', 'r'))
mod_json = None
with open(MOD_PATH / "mod.json") as f:
mod_json = json.load(f)
tag = mod_json['version']
tag = mod_json["version"]
commit = get_commit_for_tag(tag)
if not commit:
print(f"Tag {tag} does not exist. Using current commit as fallback.")
@ -83,31 +93,35 @@ if not commit:
previous_tag = get_previous_tag_in_series(tag)
changelog = get_changelog(commit, previous_tag)
print (changelog)
print(changelog)
headers = {
'Authorization': f'Bearer {oauth2_token}',
'Accept': 'application/json'
}
headers = {"Authorization": f"Bearer {oauth2_token}", "Accept": "application/json"}
r = requests.get(f'https://api.mod.io/v1/me/mods', params={
'api_key': api_key
}, headers = headers)
r = requests.get(
"https://api.mod.io/v1/me/mods", timeout=5, params={"api_key": api_key}, headers=headers
)
print(r.json())
files = {'filedata': open(mod_file_path, 'rb')}
with open(mod_file_path, "rb") as f:
files = {"filedata": f}
signature = ""
with open(f"{mod_name}-{mod_version}.zip.minisign", encoding="utf-8") as f:
signature = f.read()
signature = open(f"{mod_name}-{mod_version}.zip.minisign", 'r', encoding="utf-8").read()
rq = requests.post(
f"https://api.mod.io/v1/games/{zeroad_id}/mods/{community_mod_id}/files",
files=files,
headers=headers,
timeout=500,
data={
"version": mod_version,
"active": True,
"changelog": changelog,
"metadata_blob": json.dumps(
{"dependencies": mod_json["dependencies"], "minisigs": [signature]}
),
},
)
rq = requests.post(f'https://api.mod.io/v1/games/{zeroad_id}/mods/{community_mod_id}/files', files=files, headers=headers, data={
'version': mod_version,
'active': True,
'changelog': changelog,
'metadata_blob' : json.dumps({
'dependencies': mod_json['dependencies'],
'minisigs': [signature]
})
})
print(rq.json())
print(rq.json())

View File

@ -1,9 +1,9 @@
from collections import Counter
from decimal import Decimal
from re import split
from sys import stderr
from xml.etree import ElementTree
from os.path import exists
from re import split
from xml.etree import ElementTree as ET
class SimulTemplateEntity:
def __init__(self, vfs_root, logger):
@ -12,11 +12,11 @@ class SimulTemplateEntity:
def get_file(self, base_path, vfs_path, mod):
default_path = self.vfs_root / mod / base_path
file = (default_path/ "special" / "filter" / vfs_path).with_suffix('.xml')
file = (default_path / "special" / "filter" / vfs_path).with_suffix(".xml")
if not exists(file):
file = (default_path / "mixins" / vfs_path).with_suffix('.xml')
file = (default_path / "mixins" / vfs_path).with_suffix(".xml")
if not exists(file):
file = (default_path / vfs_path).with_suffix('.xml')
file = (default_path / vfs_path).with_suffix(".xml")
return file
def get_main_mod(self, base_path, vfs_path, mods):
@ -33,107 +33,110 @@ class SimulTemplateEntity:
return main_mod
def apply_layer(self, base_tag, tag):
"""
apply tag layer to base_tag
"""
if tag.get('datatype') == 'tokens':
base_tokens = split(r'\s+', base_tag.text or '')
tokens = split(r'\s+', tag.text or '')
"""Apply tag layer to base_tag."""
if tag.get("datatype") == "tokens":
base_tokens = split(r"\s+", base_tag.text or "")
tokens = split(r"\s+", tag.text or "")
final_tokens = base_tokens.copy()
for token in tokens:
if token.startswith('-'):
if token.startswith("-"):
token_to_remove = token[1:]
if token_to_remove in final_tokens:
final_tokens.remove(token_to_remove)
elif token not in final_tokens:
final_tokens.append(token)
base_tag.text = ' '.join(final_tokens)
base_tag.text = " ".join(final_tokens)
base_tag.set("datatype", "tokens")
elif tag.get('op'):
op = tag.get('op')
op1 = Decimal(base_tag.text or '0')
op2 = Decimal(tag.text or '0')
elif tag.get("op"):
op = tag.get("op")
op1 = Decimal(base_tag.text or "0")
op2 = Decimal(tag.text or "0")
# Try converting to integers if possible, to pass validation.
if op == 'add':
if op == "add":
base_tag.text = str(int(op1 + op2) if int(op1 + op2) == op1 + op2 else op1 + op2)
elif op == 'mul':
elif op == "mul":
base_tag.text = str(int(op1 * op2) if int(op1 * op2) == op1 * op2 else op1 * op2)
elif op == 'mul_round':
elif op == "mul_round":
base_tag.text = str(round(op1 * op2))
else:
raise ValueError(f"Invalid operator '{op}'")
else:
base_tag.text = tag.text
for prop in tag.attrib:
if prop not in ('disable', 'replace', 'parent', 'merge'):
if prop not in ("disable", "replace", "parent", "merge"):
base_tag.set(prop, tag.get(prop))
for child in tag:
base_child = base_tag.find(child.tag)
if 'disable' in child.attrib:
if "disable" in child.attrib:
if base_child is not None:
base_tag.remove(base_child)
elif ('merge' not in child.attrib) or (base_child is not None):
if 'replace' in child.attrib and base_child is not None:
elif ("merge" not in child.attrib) or (base_child is not None):
if "replace" in child.attrib and base_child is not None:
base_tag.remove(base_child)
base_child = None
if base_child is None:
base_child = ElementTree.Element(child.tag)
base_child = ET.Element(child.tag)
base_tag.append(base_child)
self.apply_layer(base_child, child)
if 'replace' in base_child.attrib:
del base_child.attrib['replace']
if "replace" in base_child.attrib:
del base_child.attrib["replace"]
def load_inherited(self, base_path, vfs_path, mods):
entity = self._load_inherited(base_path, vfs_path, mods)
entity[:] = sorted(entity[:], key=lambda x: x.tag)
return entity
def _load_inherited(self, base_path, vfs_path, mods, base = None):
"""
vfs_path should be relative to base_path in a mod
"""
if '|' in vfs_path:
def _load_inherited(self, base_path, vfs_path, mods, base=None):
"""vfs_path should be relative to base_path in a mod."""
if "|" in vfs_path:
paths = vfs_path.split("|", 1)
base = self._load_inherited(base_path, paths[1], mods, base);
base = self._load_inherited(base_path, paths[0], mods, base);
return base
self._load_inherited(base_path, paths[1], mods, base)
return self._load_inherited(base_path, paths[0], mods, base)
main_mod = self.get_main_mod(base_path, vfs_path, mods)
fp = self.get_file(base_path, vfs_path, main_mod)
layer = ElementTree.parse(fp).getroot()
layer = ET.parse(fp).getroot()
for el in layer.iter():
children = [x.tag for x in el]
duplicates = [x for x, c in Counter(children).items() if c > 1]
if duplicates:
for dup in duplicates:
self.logger.warning(f"Duplicate child node '{dup}' in tag {el.tag} of {fp}")
if layer.get('parent'):
parent = self._load_inherited(base_path, layer.get('parent'), mods, base)
self.logger.warning(
"Duplicate child node '%s' in tag %s of %s", dup, el.tag, fp
)
if layer.get("parent"):
parent = self._load_inherited(base_path, layer.get("parent"), mods, base)
self.apply_layer(parent, layer)
return parent
else:
if not base:
return layer
else:
self.apply_layer(base, layer)
return base
if not base:
return layer
self.apply_layer(base, layer)
return base
def find_files(vfs_root, mods, vfs_path, *ext_list):
"""Return a list of 2-size tuples.
Each tuple contains:
- Path relative to the mod base
- full Path
"""
returns a list of 2-size tuple with:
- Path relative to the mod base
- full Path
"""
full_exts = ['.' + ext for ext in ext_list]
full_exts = ["." + ext for ext in ext_list]
def find_recursive(dp, base):
"""(relative Path, full Path) generator"""
"""(relative Path, full Path) generator."""
if dp.is_dir():
if dp.name != '.svn' and dp.name != '.git' and not dp.name.endswith('~'):
if dp.name not in (".svn", ".git") and not dp.name.endswith("~"):
for fp in dp.iterdir():
yield from find_recursive(fp, base)
elif dp.suffix in full_exts:
relative_file_path = dp.relative_to(base)
yield (relative_file_path, dp.resolve())
return [(rp, fp) for mod in mods for (rp, fp) in find_recursive(vfs_root / mod / vfs_path, vfs_root / mod)]
return [
(rp, fp)
for mod in mods
for (rp, fp) in find_recursive(vfs_root / mod / vfs_path, vfs_root / mod)
]