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 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' diff --git a/generate_index.py b/generate_index.py new file mode 100644 index 0000000..eec3c02 --- /dev/null +++ b/generate_index.py @@ -0,0 +1,223 @@ +import base64 +import hashlib +import io +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 = [ + "electrostatic-models", + "energy-models", + "file-formats", + "input-generators", + "menu-commands", +] + + +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 get_gh_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"] + 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}" + 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, path: str | None, metadata_file: str) -> dict: + """Get the metadata from the TOML file in the given repository.""" + 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) + content = base64.b64decode(data["content"]) + toml = tomllib.load(io.BytesIO(content)) + + return toml + + +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 + avogadro_metadata = toml + else: + project_metadata = toml["project"] + avogadro_metadata = toml["tool"]["avogadro"] + + # Required fields in `[project]`/the top level + for required_key in ["name", "version", "authors", "license"]: + metadata[required_key] = project_metadata[required_key] + + # 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 []: + metadata[required_key] = avogadro_metadata[required_key] + + # Optional fields in `[tool.avogadro]`/the top level + 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 + metadata["feature-types"] = [t for t in FEATURE_TYPES if t in avogadro_metadata] + + return metadata + + +def validate_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!") + + # 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 + if c.isascii() and (c.isalnum() or c == "-"): + 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 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"]: + 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]: + """Collect all the metadata for all plugins with repository information in + the provided dict.""" + all_metadata = [] + + for plugin_name, repo_info in repos.items(): + # 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_gh_repo_metadata(repo, commit, release_tag) + + path = repo_info.get("path") + + toml = fetch_toml(repo, 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": + toml_format = "pyproject" + else: + 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, including that in `repositories.toml` + plugin_metadata = toml_metadata | repo_info | repo_metadata + + validate_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, "rb") 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(metadata, f) diff --git a/plugins.json b/plugins.json new file mode 100644 index 0000000..45c9129 --- /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": {"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 diff --git a/repositories.toml b/repositories.toml new file mode 100644 index 0000000..15bbbc9 --- /dev/null +++ b/repositories.toml @@ -0,0 +1,16 @@ +[pypkg-demo] +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" +plugin-type = "pypkg" + +[avogenerators] +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" +plugin-type = "pypkg"