From 55f7a37b41b5873063f95b4db4c2c1673877e9a1 Mon Sep 17 00:00:00 2001 From: Matthew Milner Date: Wed, 4 Feb 2026 01:01:54 +0100 Subject: [PATCH 01/14] FIx missing bracket --- cachedetails.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cachedetails.py b/cachedetails.py index 21ea0f4..15c220f 100644 --- a/cachedetails.py +++ b/cachedetails.py @@ -70,7 +70,7 @@ def getMetaData(repoName): # First get type of plugin, which is only specified in plugin.json # Fix some common naming errors at the same time if 'type' in result: - type_corrections = {'input': 'inputGenerators', 'generators': 'inputGenerators', 'formats': 'formatScripts' + type_corrections = {'input': 'inputGenerators', 'generators': 'inputGenerators', 'formats': 'formatScripts'} repoData['type'] = type_corrections.get(result['type'], result['type']) # Defaults to result['type'] else: repoData['type'] = 'other' From 03643db1768c15aa52b0c8759a8faa994f4a3554 Mon Sep 17 00:00:00 2001 From: Matthew Milner Date: Wed, 4 Feb 2026 01:02:23 +0100 Subject: [PATCH 02/14] Initial version of script for v2.0 of the plugin API --- generate_index.py | 119 ++++++++++++++++++++++++++++++++++++++++++++++ repositories.toml | 5 ++ 2 files changed, 124 insertions(+) create mode 100644 generate_index.py create mode 100644 repositories.toml diff --git a/generate_index.py b/generate_index.py new file mode 100644 index 0000000..78efcad --- /dev/null +++ b/generate_index.py @@ -0,0 +1,119 @@ +import base64 +import json +from pathlib import Path +import tomllib +from urllib import request + + +"""A list of current plugin types.""" +PLUGIN_TYPES = [ + "pyscript", + "pypkg", + "pypixi", +] + +"""A list of current plugin feature types.""" +FEATURE_TYPES = [ + "commands", + "charges", + "energy", + "formats", + "generators", +] + + +def add_auth(url): + data_url = request.Request(url) + base64_string = 'ZXRwMTI6cXdlcnR5Njc=' + data_url.add_header("Authorization", f"Basic {base64_string}") + return data_url + + +def fetch_toml(repo: str, path: str, commit: str) -> dict: + """Get the metadata from the TOML file in the given repository.""" + plugin_toml_url = f"https://api.github.com/repos/{repo}/contents/{path}?ref={commit}" + req = add_auth(plugin_toml_url) + response = request.urlopen(req) + data = json.load(response) + content = base64.b64decode(data["content"]) + toml = tomllib.loads(content) + + return toml + + +def extract_metadata(toml: dict, toml_format: str) -> dict: + """Extract the necessary metadata from the plugin's metadata.""" + metadata = {} + + # Most of the necessary metadata for the index (name, version etc.) are + # listed under `[project]` in `pyproject.toml` or at the top level otherwise + if toml_format == "avogadro": + project_metadata = toml["project"] + avogadro_metadata = toml + else: + project_metadata = toml + avogadro_metadata = toml["tool"]["avogadro"] + + for key in ["name", "version", "authors", "license"]: + metadata[key] = project_metadata[key] + + # Description is optional + if "description" in project_metadata: + metadata["description"] = project_metadata["description"] + else: + metadata["description"] = "" + + metadata["plugin-type"] = avogadro_metadata["plugin-type"] + + # Also determine and store what features the plugin provides by looking at + # which arrays the TOML contains + metadata["feature-types"] = [t for t in FEATURE_TYPES if t in avogadro_metadata] + + return metadata + + +def check_metadata(metadata: dict): + """Confirm that various fields in the extracted metadata are the appropriate + format, type etc. according to the requirements.""" + + if not isinstance(metadata["version"], str): + raise Exception(f"Version number of {metadata["name"]} is not a string!") + + for c in metadata["name"]: + # Only a-z, A-Z, 0-9, - are valid in plugin names + if c.isascii() and (c.isalnum() or c == "-"): + continue + else: + raise Exception(f"{metadata["name"]} is not a valid plugin name!") + + +def get_metadata_all(repos: dict[str, dict]) -> dict[str, dict]: + """Collect all the metadata for all plugins with repository information in + the provided dict.""" + all_metadata = [] + + for plugin_name, repo_info in repos.items(): + toml = fetch_toml(repo_info["git"], repo_info["path"], repo_info["commit"]) + toml_filename = repo_info["path"].split("/")[-1] + if toml_filename == "avogadro.toml": + toml_format = "avogadro" + elif toml_filename == "pyproject.toml": + toml_format = "pyproject" + else: + raise Exception(f"Metadata file provided by {plugin_name} not a recognized format!") + plugin_metadata = extract_metadata(toml, toml_format) + check_metadata(plugin_metadata) + all_metadata.append(plugin_metadata) + + return all_metadata + + +if __name__ == "__main__": + # When run as a script, get the metadata for all the repositories and save + # as a JSON file in the current working directory + repos_file = Path(__file__).with_name("repositories.toml") + with open(repos_file, "b") as f: + repos = tomllib.load(f) + metadata = get_metadata_all(repos) + with open(Path.cwd()/"plugins.json", "w", encoding="utf-8") as f: + plugins_json = json.dump(f, metadata) diff --git a/repositories.toml b/repositories.toml new file mode 100644 index 0000000..8c11f95 --- /dev/null +++ b/repositories.toml @@ -0,0 +1,5 @@ +[pypkg-demo] +git = "https://github.com/matterhorn103/avo-plugin-demo.git" +path = "pypkg-demo/pyproject.toml" +commit = "55358eadd7edbbf0ee809280a0e0457f37a77719" +plugin-type = "pypkg" From 6a5503e42cea12cf2fe1a0eef7273d5d53f8f2e7 Mon Sep 17 00:00:00 2001 From: Matthew Milner Date: Wed, 4 Feb 2026 03:31:38 +0100 Subject: [PATCH 03/14] Get metadata for the repo as well, and for a release if specified --- generate_index.py | 88 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 15 deletions(-) diff --git a/generate_index.py b/generate_index.py index 78efcad..12f201c 100644 --- a/generate_index.py +++ b/generate_index.py @@ -29,9 +29,42 @@ def add_auth(url): return data_url -def fetch_toml(repo: str, path: str, commit: str) -> dict: +def get_repo_metadata(repo: str, commit: str, release_tag: str | None) -> dict: + """Get the metadata of the GitHub repo itself.""" + repo_metadata = {} + + api_url = f"https://api.github.com/repos/{repo}" + req = add_auth(api_url) + response = request.urlopen(req) + repo_data = json.load(response) + repo_metadata["last-update"] = repo_data["updated_at"] + + # Get the date and time of the specific commit provided + commit_url = f"https://api.github.com/repos/{repo}/commits/{commit}" + req = add_auth(commit_url) + response = request.urlopen(req) + commit_data = json.load(response) + repo_metadata["commit-timestamp"] = commit_data["commit"]["committer"]["date"] + + # Look for the release if provided + if release_tag is None: + repo_metadata["has-release"] = False + else: + repo_metadata["has-release"] = True + repo_metadata["release-tag"] = release_tag + repo_metadata["release-version"] = release_tag.lstrip("v") + release_url = f"https://api.github.com/repos/{repo}/releases/tags/{release_tag}" + req = add_auth(release_url) + response = request.urlopen(req) + release_data = json.load(response) + # TODO Get the corresponding commit + + return repo_metadata + + +def fetch_toml(repo: str, commit: str, metadata_file: str) -> dict: """Get the metadata from the TOML file in the given repository.""" - plugin_toml_url = f"https://api.github.com/repos/{repo}/contents/{path}?ref={commit}" + plugin_toml_url = f"https://api.github.com/repos/{repo}/contents/{metadata_file}?ref={commit}" req = add_auth(plugin_toml_url) response = request.urlopen(req) data = json.load(response) @@ -41,29 +74,32 @@ def fetch_toml(repo: str, path: str, commit: str) -> dict: return toml -def extract_metadata(toml: dict, toml_format: str) -> dict: +def extract_plugin_metadata(toml: dict, toml_format: str) -> dict: """Extract the necessary metadata from the plugin's metadata.""" metadata = {} # Most of the necessary metadata for the index (name, version etc.) are # listed under `[project]` in `pyproject.toml` or at the top level otherwise if toml_format == "avogadro": - project_metadata = toml["project"] + project_metadata = toml avogadro_metadata = toml else: - project_metadata = toml + project_metadata = toml["project"] avogadro_metadata = toml["tool"]["avogadro"] - for key in ["name", "version", "authors", "license"]: - metadata[key] = project_metadata[key] + # Required fields in `[project]`/the top level + for required_key in ["name", "version", "authors", "license"]: + metadata[required_key] = project_metadata[required_key] - # Description is optional - if "description" in project_metadata: - metadata["description"] = project_metadata["description"] - else: - metadata["description"] = "" + # Optional fields in `[project]`/the top level + metadata["description"] = project_metadata.get("description", "") + + # Required fields in `[tool.avogadro]`/the top level + for required_key in ["plugin-type"]: + metadata[required_key] = avogadro_metadata[required_key] - metadata["plugin-type"] = avogadro_metadata["plugin-type"] + # Optional fields in `[tool.avogadro]`/the top level + metadata["minimum-avogadro-version"] = avogadro_metadata.get("plugin-type", "1.103") # Also determine and store what features the plugin provides by looking at # which arrays the TOML contains @@ -76,9 +112,19 @@ def check_metadata(metadata: dict): """Confirm that various fields in the extracted metadata are the appropriate format, type etc. according to the requirements.""" + if not isinstance(metadata["minimum-avogadro-version"], str): + raise Exception(f"Minimum Avogadro version number of {metadata["name"]} is not a string!") + if not isinstance(metadata["version"], str): raise Exception(f"Version number of {metadata["name"]} is not a string!") + # The version of a release and the version number in the TOML file must match + if not metadata["version"] == metadata["release-version"]: + raise Exception(f"Version number of {metadata["name"]} does not match the release!") + + # The commit of a release and the commit given in the TOML file must match + # TODO + for c in metadata["name"]: # Only a-z, A-Z, 0-9, - are valid in plugin names if c.isascii() and (c.isalnum() or c == "-"): @@ -93,7 +139,14 @@ def get_metadata_all(repos: dict[str, dict]) -> dict[str, dict]: all_metadata = [] for plugin_name, repo_info in repos.items(): - toml = fetch_toml(repo_info["git"], repo_info["path"], repo_info["commit"]) + # Take out the repo owner/name string from the git URL + repo = repo_info["git"].removeprefix("https://github.com/").removesuffix(".git") + + release_tag = repo_info.get("release-tag", None) + + repo_metadata = get_repo_metadata(repo, repo_info["commit"], release_tag) + + toml = fetch_toml(repo, repo_info["commit"], repo_info["path"]) toml_filename = repo_info["path"].split("/")[-1] if toml_filename == "avogadro.toml": toml_format = "avogadro" @@ -101,8 +154,13 @@ def get_metadata_all(repos: dict[str, dict]) -> dict[str, dict]: toml_format = "pyproject" else: raise Exception(f"Metadata file provided by {plugin_name} not a recognized format!") - plugin_metadata = extract_metadata(toml, toml_format) + toml_metadata = extract_plugin_metadata(toml, toml_format) + + # Combine metadata from all sources + plugin_metadata = repo_metadata | toml_metadata + check_metadata(plugin_metadata) + all_metadata.append(plugin_metadata) return all_metadata From 484afcec52f0c5d326ffae21dee742852ca23c9c Mon Sep 17 00:00:00 2001 From: Matthew Milner Date: Wed, 4 Feb 2026 03:39:25 +0100 Subject: [PATCH 04/14] Use binary mode when reading repositories.toml --- generate_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generate_index.py b/generate_index.py index 12f201c..e15eec3 100644 --- a/generate_index.py +++ b/generate_index.py @@ -170,7 +170,7 @@ def get_metadata_all(repos: dict[str, dict]) -> dict[str, dict]: # When run as a script, get the metadata for all the repositories and save # as a JSON file in the current working directory repos_file = Path(__file__).with_name("repositories.toml") - with open(repos_file, "b") as f: + with open(repos_file, "rb") as f: repos = tomllib.load(f) metadata = get_metadata_all(repos) with open(Path.cwd()/"plugins.json", "w", encoding="utf-8") as f: From 4a2e7ce3acf6dfff71875bb0a140f651fd6c66e5 Mon Sep 17 00:00:00 2001 From: Matthew Milner Date: Sun, 22 Feb 2026 02:35:38 +0100 Subject: [PATCH 05/14] Add a .gitignore Signed-off-by: Matthew Milner --- .gitignore | 220 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a93a63 --- /dev/null +++ b/.gitignore @@ -0,0 +1,220 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +poetry.lock +poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +pdm.lock +pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +.vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml + +# pixi environments +.pixi/* +!.pixi/config.toml From 7b09501f710f5c8e2e92918ff2f4e285b6bba545 Mon Sep 17 00:00:00 2001 From: Matthew Milner Date: Sun, 22 Feb 2026 02:36:04 +0100 Subject: [PATCH 06/14] Update commit for avo-plugin-demo to latest development state Signed-off-by: Matthew Milner --- repositories.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repositories.toml b/repositories.toml index 8c11f95..08f59fb 100644 --- a/repositories.toml +++ b/repositories.toml @@ -1,5 +1,5 @@ [pypkg-demo] git = "https://github.com/matterhorn103/avo-plugin-demo.git" path = "pypkg-demo/pyproject.toml" -commit = "55358eadd7edbbf0ee809280a0e0457f37a77719" +commit = "dbbc4d49396ac2b7746abb0bec0af6e577d8c0c5" plugin-type = "pypkg" From 6780d2979346bd61e860be7d09582770ff0c20a7 Mon Sep 17 00:00:00 2001 From: Matthew Milner Date: Sun, 22 Feb 2026 02:38:59 +0100 Subject: [PATCH 07/14] Add avogenerators to plugins Signed-off-by: Matthew Milner --- repositories.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/repositories.toml b/repositories.toml index 08f59fb..94c9415 100644 --- a/repositories.toml +++ b/repositories.toml @@ -3,3 +3,8 @@ git = "https://github.com/matterhorn103/avo-plugin-demo.git" path = "pypkg-demo/pyproject.toml" commit = "dbbc4d49396ac2b7746abb0bec0af6e577d8c0c5" plugin-type = "pypkg" + +[avogenerators] +git = "https://github.com/OpenChemistry/avogenerators.git" +commit = "9843e35d4009040847dab2f49cd108a1c5391e0a" +plugin-type = "pypkg" From e03f9ca77228b9a8b3db4e2ffab5657d5816b4e8 Mon Sep 17 00:00:00 2001 From: Matthew Milner Date: Sun, 22 Feb 2026 02:55:02 +0100 Subject: [PATCH 08/14] Have plugins specify the metadata file separately to the (optional) path Signed-off-by: Matthew Milner --- generate_index.py | 11 +++++++---- repositories.toml | 4 +++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/generate_index.py b/generate_index.py index e15eec3..30dfdb2 100644 --- a/generate_index.py +++ b/generate_index.py @@ -62,9 +62,10 @@ def get_repo_metadata(repo: str, commit: str, release_tag: str | None) -> dict: return repo_metadata -def fetch_toml(repo: str, commit: str, metadata_file: str) -> dict: +def fetch_toml(repo: str, commit: str, path: str | None, metadata_file: str) -> dict: """Get the metadata from the TOML file in the given repository.""" - plugin_toml_url = f"https://api.github.com/repos/{repo}/contents/{metadata_file}?ref={commit}" + metadata_path = f"{path}/{metadata_file}" if path else metadata_file + plugin_toml_url = f"https://api.github.com/repos/{repo}/contents/{metadata_path}?ref={commit}" req = add_auth(plugin_toml_url) response = request.urlopen(req) data = json.load(response) @@ -146,8 +147,10 @@ def get_metadata_all(repos: dict[str, dict]) -> dict[str, dict]: repo_metadata = get_repo_metadata(repo, repo_info["commit"], release_tag) - toml = fetch_toml(repo, repo_info["commit"], repo_info["path"]) - toml_filename = repo_info["path"].split("/")[-1] + path = repo_info.get("path") + + toml = fetch_toml(repo, repo_info["commit"], path, repo_info["metadata"]) + toml_filename = repo_info["metadata"].split("/")[-1] if toml_filename == "avogadro.toml": toml_format = "avogadro" elif toml_filename == "pyproject.toml": diff --git a/repositories.toml b/repositories.toml index 94c9415..145e64d 100644 --- a/repositories.toml +++ b/repositories.toml @@ -1,10 +1,12 @@ [pypkg-demo] git = "https://github.com/matterhorn103/avo-plugin-demo.git" -path = "pypkg-demo/pyproject.toml" +path = "pypkg-demo" +metadata = "pyproject.toml" commit = "dbbc4d49396ac2b7746abb0bec0af6e577d8c0c5" plugin-type = "pypkg" [avogenerators] git = "https://github.com/OpenChemistry/avogenerators.git" +metadata = "pyproject.toml" commit = "9843e35d4009040847dab2f49cd108a1c5391e0a" plugin-type = "pypkg" From ad68c7bdbbcc14999ba71749c9179d83f3eb16a2 Mon Sep 17 00:00:00 2001 From: Matthew Milner Date: Sun, 22 Feb 2026 03:37:10 +0100 Subject: [PATCH 09/14] Small fixes Signed-off-by: Matthew Milner --- generate_index.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/generate_index.py b/generate_index.py index 30dfdb2..a79e58a 100644 --- a/generate_index.py +++ b/generate_index.py @@ -1,4 +1,5 @@ import base64 +import io import json from pathlib import Path import tomllib @@ -70,7 +71,7 @@ def fetch_toml(repo: str, commit: str, path: str | None, metadata_file: str) -> response = request.urlopen(req) data = json.load(response) content = base64.b64decode(data["content"]) - toml = tomllib.loads(content) + toml = tomllib.load(io.BytesIO(content)) return toml @@ -96,11 +97,11 @@ def extract_plugin_metadata(toml: dict, toml_format: str) -> dict: metadata["description"] = project_metadata.get("description", "") # Required fields in `[tool.avogadro]`/the top level - for required_key in ["plugin-type"]: + for required_key in []: metadata[required_key] = avogadro_metadata[required_key] # Optional fields in `[tool.avogadro]`/the top level - metadata["minimum-avogadro-version"] = avogadro_metadata.get("plugin-type", "1.103") + metadata["minimum-avogadro-version"] = avogadro_metadata.get("minimum-avogadro-version", "1.103") # Also determine and store what features the plugin provides by looking at # which arrays the TOML contains @@ -119,12 +120,15 @@ def check_metadata(metadata: dict): if not isinstance(metadata["version"], str): raise Exception(f"Version number of {metadata["name"]} is not a string!") - # The version of a release and the version number in the TOML file must match - if not metadata["version"] == metadata["release-version"]: - raise Exception(f"Version number of {metadata["name"]} does not match the release!") - - # The commit of a release and the commit given in the TOML file must match - # TODO + # If the plugin uses release tags, we want to verify that the commit of the + # release is the one being uploaded + if metadata["has-release"]: + # The version in the TOML file must match the release version + if not metadata["version"] == metadata["release-version"]: + raise Exception(f"Version number of {metadata["name"]} does not match the release!") + + # The commit of a release and the commit given in the TOML file must match + # TODO for c in metadata["name"]: # Only a-z, A-Z, 0-9, - are valid in plugin names @@ -177,4 +181,4 @@ def get_metadata_all(repos: dict[str, dict]) -> dict[str, dict]: repos = tomllib.load(f) metadata = get_metadata_all(repos) with open(Path.cwd()/"plugins.json", "w", encoding="utf-8") as f: - plugins_json = json.dump(f, metadata) + plugins_json = json.dump(metadata, f) From d89ddd34f6bb5f1a24eac3a5677efe08908a657e Mon Sep 17 00:00:00 2001 From: Matthew Milner Date: Sun, 22 Feb 2026 03:39:52 +0100 Subject: [PATCH 10/14] Update feature type names Signed-off-by: Matthew Milner --- generate_index.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/generate_index.py b/generate_index.py index a79e58a..389029e 100644 --- a/generate_index.py +++ b/generate_index.py @@ -15,11 +15,11 @@ """A list of current plugin feature types.""" FEATURE_TYPES = [ - "commands", - "charges", - "energy", - "formats", - "generators", + "electrostatic-models", + "energy-models", + "file-formats", + "input-generators", + "menu-commands", ] From f72b055d898630ca322887e40bc3f544ba315235 Mon Sep 17 00:00:00 2001 From: Matthew Milner Date: Sun, 22 Feb 2026 03:46:34 +0100 Subject: [PATCH 11/14] Include the metadata provided in repositories.toml in the index Signed-off-by: Matthew Milner --- generate_index.py | 4 ++-- plugins.json | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 plugins.json diff --git a/generate_index.py b/generate_index.py index 389029e..787f4de 100644 --- a/generate_index.py +++ b/generate_index.py @@ -163,8 +163,8 @@ def get_metadata_all(repos: dict[str, dict]) -> dict[str, dict]: raise Exception(f"Metadata file provided by {plugin_name} not a recognized format!") toml_metadata = extract_plugin_metadata(toml, toml_format) - # Combine metadata from all sources - plugin_metadata = repo_metadata | toml_metadata + # Combine metadata from all sources, including that in `repositories.toml` + plugin_metadata = toml_metadata | repo_info | repo_metadata check_metadata(plugin_metadata) diff --git a/plugins.json b/plugins.json new file mode 100644 index 0000000..797d3e1 --- /dev/null +++ b/plugins.json @@ -0,0 +1 @@ +[{"name": "pypkg-demo", "version": "6.0.2", "authors": [{"name": "Amedeo Avogadro", "email": "avogadro@unito.it"}], "license": "BSD-3-Clause", "description": "An example Python package plugin for Avogadro 2 to demonstrate the revamped plugin API in v2.0 onwards", "minimum-avogadro-version": "1.103", "feature-types": ["electrostatic-models", "energy-models", "file-formats", "input-generators", "menu-commands"], "git": "https://github.com/matterhorn103/avo-plugin-demo.git", "path": "pypkg-demo", "metadata": "pyproject.toml", "commit": "dbbc4d49396ac2b7746abb0bec0af6e577d8c0c5", "plugin-type": "pypkg", "last-update": "2026-02-16T02:18:19Z", "commit-timestamp": "2026-02-16T02:18:14Z", "has-release": false}, {"name": "avogenerators", "version": "2.0", "authors": [{"name": "Allison Vacanti", "email": "allison.vacanti@kitware.com"}, {"name": "Kyle Lutz", "email": "kyle.lutz@kitware.com"}, {"name": "Marcus D. Hanwell", "email": "marcus.hanwell@kitware.com"}, {"name": "Geoffrey Hutchison", "email": "geoff.hutchison@gmail.com"}, {"name": "Eric Berquist", "email": "eric.berquist@gmail.com"}, {"name": "Shiv Upadhyay", "email": "shivnupadhyay@gmail.com"}, {"name": "Amanda E. Dumi", "email": "amanda.e.dumi@gmail.com"}, {"name": "Adrea Snow", "email": "adrea.snow@gmail.com"}, {"name": "Christian Clauss", "email": "cclauss@me.com"}, {"name": "Ty Balduf", "email": "tbalduf@gmail.com"}, {"name": "Javier Cerezo", "email": "javier.cerezo@um.es"}, {"name": "Matthew Milner", "email": "matterhorn103@proton.me"}], "license": "BSD-3-Clause", "description": "Scripts for generating input files for computational chemistry packages", "minimum-avogadro-version": "1.103", "feature-types": ["input-generators"], "git": "https://github.com/OpenChemistry/avogenerators.git", "metadata": "pyproject.toml", "commit": "9843e35d4009040847dab2f49cd108a1c5391e0a", "plugin-type": "pypkg", "last-update": "2026-02-21T19:59:00Z", "commit-timestamp": "2026-02-21T19:58:56Z", "has-release": false}] \ No newline at end of file From 67fa229c13b9fd0cfc337d88ea1113ef75d8bf3f Mon Sep 17 00:00:00 2001 From: Matthew Milner Date: Mon, 23 Feb 2026 00:02:04 +0100 Subject: [PATCH 12/14] Require a source URL and hash, and validate the info in repositories.toml Signed-off-by: Matthew Milner --- generate_index.py | 44 +++++++++++++++++++++++++++++++++++++------- plugins.json | 2 +- repositories.toml | 12 ++++++++---- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/generate_index.py b/generate_index.py index 787f4de..d36695a 100644 --- a/generate_index.py +++ b/generate_index.py @@ -30,7 +30,7 @@ def add_auth(url): return data_url -def get_repo_metadata(repo: str, commit: str, release_tag: str | None) -> dict: +def get_gh_repo_metadata(repo: str, commit: str, release_tag: str | None) -> dict: """Get the metadata of the GitHub repo itself.""" repo_metadata = {} @@ -110,7 +110,7 @@ def extract_plugin_metadata(toml: dict, toml_format: str) -> dict: return metadata -def check_metadata(metadata: dict): +def validate_metadata(metadata: dict): """Confirm that various fields in the extracted metadata are the appropriate format, type etc. according to the requirements.""" @@ -136,6 +136,32 @@ def check_metadata(metadata: dict): continue else: raise Exception(f"{metadata["name"]} is not a valid plugin name!") + + +def validate_repo_info(repo_info: dict): + """Confirm that the information provided in `repositories.toml` is complete + and well-formed.""" + + # Check details of the git repository were provided + assert "repo" in repo_info["git"] + assert "commit" in repo_info["git"] + + # Check for details of the source archive + assert "url" in repo_info["src"] + assert "sha256" in repo_info["src"] + + # Confirm presence of other required information + for required_key in ["metadata", "plugin-type"]: + assert required_key in repo_info + + # Make sure that any path provided is to a directory, not a file, but with + # no final slash, and that backslashes aren't used + if "path" in repo_info: + path: str = repo_info["path"] + assert not path.endswith("/") + assert "\\" not in path + final_component = path.split("/") + assert "." not in final_component def get_metadata_all(repos: dict[str, dict]) -> dict[str, dict]: @@ -144,16 +170,20 @@ def get_metadata_all(repos: dict[str, dict]) -> dict[str, dict]: all_metadata = [] for plugin_name, repo_info in repos.items(): - # Take out the repo owner/name string from the git URL - repo = repo_info["git"].removeprefix("https://github.com/").removesuffix(".git") + # First just validate the information in `repositories.toml` + validate_repo_info(repo_info) + + # Take out the repo owner/name string from the GitHub repo URL + repo = repo_info["git"]["repo"].removeprefix("https://github.com/").removesuffix(".git") + commit = repo_info["git"]["commit"] release_tag = repo_info.get("release-tag", None) - repo_metadata = get_repo_metadata(repo, repo_info["commit"], release_tag) + repo_metadata = get_gh_repo_metadata(repo, commit, release_tag) path = repo_info.get("path") - toml = fetch_toml(repo, repo_info["commit"], path, repo_info["metadata"]) + toml = fetch_toml(repo, commit, path, repo_info["metadata"]) toml_filename = repo_info["metadata"].split("/")[-1] if toml_filename == "avogadro.toml": toml_format = "avogadro" @@ -166,7 +196,7 @@ def get_metadata_all(repos: dict[str, dict]) -> dict[str, dict]: # Combine metadata from all sources, including that in `repositories.toml` plugin_metadata = toml_metadata | repo_info | repo_metadata - check_metadata(plugin_metadata) + validate_metadata(plugin_metadata) all_metadata.append(plugin_metadata) diff --git a/plugins.json b/plugins.json index 797d3e1..4e9ab28 100644 --- a/plugins.json +++ b/plugins.json @@ -1 +1 @@ -[{"name": "pypkg-demo", "version": "6.0.2", "authors": [{"name": "Amedeo Avogadro", "email": "avogadro@unito.it"}], "license": "BSD-3-Clause", "description": "An example Python package plugin for Avogadro 2 to demonstrate the revamped plugin API in v2.0 onwards", "minimum-avogadro-version": "1.103", "feature-types": ["electrostatic-models", "energy-models", "file-formats", "input-generators", "menu-commands"], "git": "https://github.com/matterhorn103/avo-plugin-demo.git", "path": "pypkg-demo", "metadata": "pyproject.toml", "commit": "dbbc4d49396ac2b7746abb0bec0af6e577d8c0c5", "plugin-type": "pypkg", "last-update": "2026-02-16T02:18:19Z", "commit-timestamp": "2026-02-16T02:18:14Z", "has-release": false}, {"name": "avogenerators", "version": "2.0", "authors": [{"name": "Allison Vacanti", "email": "allison.vacanti@kitware.com"}, {"name": "Kyle Lutz", "email": "kyle.lutz@kitware.com"}, {"name": "Marcus D. Hanwell", "email": "marcus.hanwell@kitware.com"}, {"name": "Geoffrey Hutchison", "email": "geoff.hutchison@gmail.com"}, {"name": "Eric Berquist", "email": "eric.berquist@gmail.com"}, {"name": "Shiv Upadhyay", "email": "shivnupadhyay@gmail.com"}, {"name": "Amanda E. Dumi", "email": "amanda.e.dumi@gmail.com"}, {"name": "Adrea Snow", "email": "adrea.snow@gmail.com"}, {"name": "Christian Clauss", "email": "cclauss@me.com"}, {"name": "Ty Balduf", "email": "tbalduf@gmail.com"}, {"name": "Javier Cerezo", "email": "javier.cerezo@um.es"}, {"name": "Matthew Milner", "email": "matterhorn103@proton.me"}], "license": "BSD-3-Clause", "description": "Scripts for generating input files for computational chemistry packages", "minimum-avogadro-version": "1.103", "feature-types": ["input-generators"], "git": "https://github.com/OpenChemistry/avogenerators.git", "metadata": "pyproject.toml", "commit": "9843e35d4009040847dab2f49cd108a1c5391e0a", "plugin-type": "pypkg", "last-update": "2026-02-21T19:59:00Z", "commit-timestamp": "2026-02-21T19:58:56Z", "has-release": false}] \ No newline at end of file +[{"name": "pypkg-demo", "version": "6.0.2", "authors": [{"name": "Amedeo Avogadro", "email": "avogadro@unito.it"}], "license": "BSD-3-Clause", "description": "An example Python package plugin for Avogadro 2 to demonstrate the revamped plugin API in v2.0 onwards", "minimum-avogadro-version": "1.103", "feature-types": ["electrostatic-models", "energy-models", "file-formats", "input-generators", "menu-commands"], "git": {"repo": "https://github.com/matterhorn103/avo-plugin-demo.git", "commit": "dbbc4d49396ac2b7746abb0bec0af6e577d8c0c5"}, "src": {"url": "https://github.com/matterhorn103/avo-plugin-demo/archive/dbbc4d49396ac2b7746abb0bec0af6e577d8c0c5.zip", "sha256": "e317a02f4842c50d075f321a7d669a78ab502b15e907d2fdb847eff916a1aac8"}, "path": "pypkg-demo", "metadata": "pyproject.toml", "plugin-type": "pypkg", "last-update": "2026-02-16T02:18:19Z", "commit-timestamp": "2026-02-16T02:18:14Z", "has-release": false}, {"name": "avogenerators", "version": "2.0", "authors": [{"name": "Allison Vacanti", "email": "allison.vacanti@kitware.com"}, {"name": "Kyle Lutz", "email": "kyle.lutz@kitware.com"}, {"name": "Marcus D. Hanwell", "email": "marcus.hanwell@kitware.com"}, {"name": "Geoffrey Hutchison", "email": "geoff.hutchison@gmail.com"}, {"name": "Eric Berquist", "email": "eric.berquist@gmail.com"}, {"name": "Shiv Upadhyay", "email": "shivnupadhyay@gmail.com"}, {"name": "Amanda E. Dumi", "email": "amanda.e.dumi@gmail.com"}, {"name": "Adrea Snow", "email": "adrea.snow@gmail.com"}, {"name": "Christian Clauss", "email": "cclauss@me.com"}, {"name": "Ty Balduf", "email": "tbalduf@gmail.com"}, {"name": "Javier Cerezo", "email": "javier.cerezo@um.es"}, {"name": "Matthew Milner", "email": "matterhorn103@proton.me"}], "license": "BSD-3-Clause", "description": "Scripts for generating input files for computational chemistry packages", "minimum-avogadro-version": "1.103", "feature-types": ["input-generators"], "git": {"repo": "https://github.com/OpenChemistry/avogenerators.git", "commit": "9843e35d4009040847dab2f49cd108a1c5391e0a"}, "src": {"url": "https://github.com/OpenChemistry/avogenerators/archive/9843e35d4009040847dab2f49cd108a1c5391e0a.zip", "sha256": "3385ecfda1f84b7ddffa73d56810d772696a9428b0d86e7344ed6b1aa5197daa"}, "metadata": "pyproject.toml", "plugin-type": "pypkg", "last-update": "2026-02-21T19:59:00Z", "commit-timestamp": "2026-02-21T19:58:56Z", "has-release": false}] \ No newline at end of file diff --git a/repositories.toml b/repositories.toml index 145e64d..15bbbc9 100644 --- a/repositories.toml +++ b/repositories.toml @@ -1,12 +1,16 @@ [pypkg-demo] -git = "https://github.com/matterhorn103/avo-plugin-demo.git" +git.repo = "https://github.com/matterhorn103/avo-plugin-demo.git" +git.commit = "dbbc4d49396ac2b7746abb0bec0af6e577d8c0c5" +src.url = "https://github.com/matterhorn103/avo-plugin-demo/archive/dbbc4d49396ac2b7746abb0bec0af6e577d8c0c5.zip" +src.sha256 = "e317a02f4842c50d075f321a7d669a78ab502b15e907d2fdb847eff916a1aac8" path = "pypkg-demo" metadata = "pyproject.toml" -commit = "dbbc4d49396ac2b7746abb0bec0af6e577d8c0c5" plugin-type = "pypkg" [avogenerators] -git = "https://github.com/OpenChemistry/avogenerators.git" +git.repo = "https://github.com/OpenChemistry/avogenerators.git" +git.commit = "9843e35d4009040847dab2f49cd108a1c5391e0a" +src.url = "https://github.com/OpenChemistry/avogenerators/archive/9843e35d4009040847dab2f49cd108a1c5391e0a.zip" +src.sha256 = "3385ecfda1f84b7ddffa73d56810d772696a9428b0d86e7344ed6b1aa5197daa" metadata = "pyproject.toml" -commit = "9843e35d4009040847dab2f49cd108a1c5391e0a" plugin-type = "pypkg" From 105b38f71ada84eae166a3026a9c833d8547ae29 Mon Sep 17 00:00:00 2001 From: Matthew Milner Date: Mon, 23 Feb 2026 00:21:10 +0100 Subject: [PATCH 13/14] Include GH star count in index Signed-off-by: Matthew Milner --- generate_index.py | 1 + plugins.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/generate_index.py b/generate_index.py index d36695a..213e874 100644 --- a/generate_index.py +++ b/generate_index.py @@ -39,6 +39,7 @@ def get_gh_repo_metadata(repo: str, commit: str, release_tag: str | None) -> dic response = request.urlopen(req) repo_data = json.load(response) repo_metadata["last-update"] = repo_data["updated_at"] + repo_metadata["gh-stars"] = repo_data["stargazers_count"] # Get the date and time of the specific commit provided commit_url = f"https://api.github.com/repos/{repo}/commits/{commit}" diff --git a/plugins.json b/plugins.json index 4e9ab28..45c9129 100644 --- a/plugins.json +++ b/plugins.json @@ -1 +1 @@ -[{"name": "pypkg-demo", "version": "6.0.2", "authors": [{"name": "Amedeo Avogadro", "email": "avogadro@unito.it"}], "license": "BSD-3-Clause", "description": "An example Python package plugin for Avogadro 2 to demonstrate the revamped plugin API in v2.0 onwards", "minimum-avogadro-version": "1.103", "feature-types": ["electrostatic-models", "energy-models", "file-formats", "input-generators", "menu-commands"], "git": {"repo": "https://github.com/matterhorn103/avo-plugin-demo.git", "commit": "dbbc4d49396ac2b7746abb0bec0af6e577d8c0c5"}, "src": {"url": "https://github.com/matterhorn103/avo-plugin-demo/archive/dbbc4d49396ac2b7746abb0bec0af6e577d8c0c5.zip", "sha256": "e317a02f4842c50d075f321a7d669a78ab502b15e907d2fdb847eff916a1aac8"}, "path": "pypkg-demo", "metadata": "pyproject.toml", "plugin-type": "pypkg", "last-update": "2026-02-16T02:18:19Z", "commit-timestamp": "2026-02-16T02:18:14Z", "has-release": false}, {"name": "avogenerators", "version": "2.0", "authors": [{"name": "Allison Vacanti", "email": "allison.vacanti@kitware.com"}, {"name": "Kyle Lutz", "email": "kyle.lutz@kitware.com"}, {"name": "Marcus D. Hanwell", "email": "marcus.hanwell@kitware.com"}, {"name": "Geoffrey Hutchison", "email": "geoff.hutchison@gmail.com"}, {"name": "Eric Berquist", "email": "eric.berquist@gmail.com"}, {"name": "Shiv Upadhyay", "email": "shivnupadhyay@gmail.com"}, {"name": "Amanda E. Dumi", "email": "amanda.e.dumi@gmail.com"}, {"name": "Adrea Snow", "email": "adrea.snow@gmail.com"}, {"name": "Christian Clauss", "email": "cclauss@me.com"}, {"name": "Ty Balduf", "email": "tbalduf@gmail.com"}, {"name": "Javier Cerezo", "email": "javier.cerezo@um.es"}, {"name": "Matthew Milner", "email": "matterhorn103@proton.me"}], "license": "BSD-3-Clause", "description": "Scripts for generating input files for computational chemistry packages", "minimum-avogadro-version": "1.103", "feature-types": ["input-generators"], "git": {"repo": "https://github.com/OpenChemistry/avogenerators.git", "commit": "9843e35d4009040847dab2f49cd108a1c5391e0a"}, "src": {"url": "https://github.com/OpenChemistry/avogenerators/archive/9843e35d4009040847dab2f49cd108a1c5391e0a.zip", "sha256": "3385ecfda1f84b7ddffa73d56810d772696a9428b0d86e7344ed6b1aa5197daa"}, "metadata": "pyproject.toml", "plugin-type": "pypkg", "last-update": "2026-02-21T19:59:00Z", "commit-timestamp": "2026-02-21T19:58:56Z", "has-release": false}] \ No newline at end of file +[{"name": "pypkg-demo", "version": "6.0.2", "authors": [{"name": "Amedeo Avogadro", "email": "avogadro@unito.it"}], "license": "BSD-3-Clause", "description": "An example Python package plugin for Avogadro 2 to demonstrate the revamped plugin API in v2.0 onwards", "minimum-avogadro-version": "1.103", "feature-types": ["electrostatic-models", "energy-models", "file-formats", "input-generators", "menu-commands"], "git": {"repo": "https://github.com/matterhorn103/avo-plugin-demo.git", "commit": "dbbc4d49396ac2b7746abb0bec0af6e577d8c0c5"}, "src": {"url": "https://github.com/matterhorn103/avo-plugin-demo/archive/dbbc4d49396ac2b7746abb0bec0af6e577d8c0c5.zip", "sha256": "e317a02f4842c50d075f321a7d669a78ab502b15e907d2fdb847eff916a1aac8"}, "path": "pypkg-demo", "metadata": "pyproject.toml", "plugin-type": "pypkg", "last-update": "2026-02-16T02:18:19Z", "gh-stars": 0, "commit-timestamp": "2026-02-16T02:18:14Z", "has-release": false}, {"name": "avogenerators", "version": "2.0", "authors": [{"name": "Allison Vacanti", "email": "allison.vacanti@kitware.com"}, {"name": "Kyle Lutz", "email": "kyle.lutz@kitware.com"}, {"name": "Marcus D. Hanwell", "email": "marcus.hanwell@kitware.com"}, {"name": "Geoffrey Hutchison", "email": "geoff.hutchison@gmail.com"}, {"name": "Eric Berquist", "email": "eric.berquist@gmail.com"}, {"name": "Shiv Upadhyay", "email": "shivnupadhyay@gmail.com"}, {"name": "Amanda E. Dumi", "email": "amanda.e.dumi@gmail.com"}, {"name": "Adrea Snow", "email": "adrea.snow@gmail.com"}, {"name": "Christian Clauss", "email": "cclauss@me.com"}, {"name": "Ty Balduf", "email": "tbalduf@gmail.com"}, {"name": "Javier Cerezo", "email": "javier.cerezo@um.es"}, {"name": "Matthew Milner", "email": "matterhorn103@proton.me"}], "license": "BSD-3-Clause", "description": "Scripts for generating input files for computational chemistry packages", "minimum-avogadro-version": "1.103", "feature-types": ["input-generators"], "git": {"repo": "https://github.com/OpenChemistry/avogenerators.git", "commit": "9843e35d4009040847dab2f49cd108a1c5391e0a"}, "src": {"url": "https://github.com/OpenChemistry/avogenerators/archive/9843e35d4009040847dab2f49cd108a1c5391e0a.zip", "sha256": "3385ecfda1f84b7ddffa73d56810d772696a9428b0d86e7344ed6b1aa5197daa"}, "metadata": "pyproject.toml", "plugin-type": "pypkg", "last-update": "2026-02-21T19:59:00Z", "gh-stars": 16, "commit-timestamp": "2026-02-21T19:58:56Z", "has-release": false}] \ No newline at end of file From 64f3d2da838fa0862d5065b3cf18a0db94251a1a Mon Sep 17 00:00:00 2001 From: Matthew Milner Date: Mon, 23 Feb 2026 14:38:01 +0100 Subject: [PATCH 14/14] Check the provided hash is correct for the provided source URL Signed-off-by: Matthew Milner --- generate_index.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/generate_index.py b/generate_index.py index 213e874..eec3c02 100644 --- a/generate_index.py +++ b/generate_index.py @@ -1,4 +1,5 @@ import base64 +import hashlib import io import json from pathlib import Path @@ -150,6 +151,13 @@ def validate_repo_info(repo_info: dict): # Check for details of the source archive assert "url" in repo_info["src"] assert "sha256" in repo_info["src"] + # Confirm the hash is correct + req = request.Request(repo_info["src"]["url"]) + response = request.urlopen(req) + src_hash = hashlib.sha256() + while chunk := response.read(8192): + src_hash.update(chunk) + assert src_hash.hexdigest() == repo_info["src"]["sha256"] # Confirm presence of other required information for required_key in ["metadata", "plugin-type"]: