diff --git a/.github/spell-ignore-words.txt b/.github/spell-ignore-words.txt index bb7c1aa223..6a8e2d70f9 100644 --- a/.github/spell-ignore-words.txt +++ b/.github/spell-ignore-words.txt @@ -3,3 +3,5 @@ dpkg pipenv datasource lifecycle +feature_branch +pull_request diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6427467789..de9612b900 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -148,7 +148,12 @@ repos: - --pre-commit - github_app_geo_project/module/versions/schema.json - VERSIONS-CONFIG.md - + - id: jsonschema2md + files: ^github_app_geo_project/module/clean/schema\.json$ + args: + - --pre-commit + - github_app_geo_project/module/clean/schema.json + - CLEAN-CONFIG.md - repo: https://github.com/sbrunner/jsonschema-validator rev: 0.1.0 hooks: @@ -170,6 +175,7 @@ repos: github_app_geo_project/module/audit/schema\.json |github_app_geo_project/module/delete_old_workflow_runs/schema\.json |github_app_geo_project/module/versions/schema\.json + |github_app_geo_project/module/clean/schema\.json )$ args: - --fields=description,title diff --git a/.prospector.yaml b/.prospector.yaml index cc8e8e1223..cd816379fe 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -11,6 +11,7 @@ ignore-paths: - github_app_geo_project/application_configuration.py - github_app_geo_project/module/standard/changelog_configuration.py - github_app_geo_project/module/audit/configuration.py + - github_app_geo_project/module/clean/configuration.py mypy: options: diff --git a/CLEAN-CONFIG.md b/CLEAN-CONFIG.md new file mode 100644 index 0000000000..32dacc8203 --- /dev/null +++ b/CLEAN-CONFIG.md @@ -0,0 +1,11 @@ +# Clean modules configuration + +## Properties + +- **`clean`** _(object)_: Cannot contain additional properties. + - **`docker`** _(boolean)_: Clean the docker images made from feature branches and pull requests. Default: `true`. + - **`git`** _(array)_ + - **Items** _(object)_: Clean a folder from a branch. Cannot contain additional properties. + - **`on-type`** _(string)_: feature_branch, pull_request or all. Must be one of: `["feature_branch", "pull_request", "all"]`. Default: `"all"`. + - **`branch`** _(string)_: The branch on witch one the folder will be cleaned. Default: `"gh-pages"`. + - **`folder`** _(string)_: The folder to be cleaned, can contains {name}, that will be replaced with the branch name or pull request number. Default: `"{name}"`. diff --git a/github_app_geo_project/module/clean/__init__.py b/github_app_geo_project/module/clean/__init__.py new file mode 100644 index 0000000000..e40511e86a --- /dev/null +++ b/github_app_geo_project/module/clean/__init__.py @@ -0,0 +1,224 @@ +"""Module to display the status of the workflows in the transversal dashboard.""" + +import json +import logging +import os.path +import subprocess # nosec +import tempfile +from typing import Any, cast + +import github +import requests +import tag_publish.configuration +import yaml +from pydantic import BaseModel + +from github_app_geo_project import module +from github_app_geo_project.configuration import GithubProject +from github_app_geo_project.module import utils as module_utils + +from . import configuration + +_LOGGER = logging.getLogger(__name__) + + +class _ActionData(BaseModel): + type: str + name: str + + +class Clean(module.Module[configuration.CleanConfiguration, _ActionData, None]): + """Module to display the status of the workflows in the transversal dashboard.""" + + def title(self) -> str: + """Get the title of the module.""" + return "Clean feature artifacts" + + def description(self) -> str: + """Get the description of the module.""" + return "Clean feature branches or pull requests artifacts" + + def documentation_url(self) -> str: + """Get the URL to the documentation page of the module.""" + return "https://github.com/camptocamp/github-app-geo-project/wiki/Module-%E2%80%90-Clean" + + def get_github_application_permissions(self) -> module.GitHubApplicationPermissions: + """Get the GitHub application permissions needed by the module.""" + return module.GitHubApplicationPermissions( + { + "contents": "write", + }, + {"pull_request", "delete"}, + ) + + def get_json_schema(self) -> dict[str, Any]: + """Get the JSON schema for the module.""" + with open(os.path.join(os.path.dirname(__file__), "schema.json"), encoding="utf-8") as schema_file: + return json.loads(schema_file.read()).get("properties", {}).get("audit") # type: ignore[no-any-return] + + def get_actions(self, context: module.GetActionContext) -> list[module.Action[_ActionData]]: + """Get the action related to the module and the event.""" + if context.event_data.get("action") == "delete": + return [ + module.Action(_ActionData(type="pull_request", name="123"), priority=module.PRIORITY_CRON) + ] + return [] + + async def process( + self, context: module.ProcessContext[configuration.CleanConfiguration, _ActionData, None] + ) -> module.ProcessOutput[_ActionData, None]: + """Process the action.""" + if context.module_config.get("docker", True): + await self._clean_docker(context) + for git in context.module_config.get("git", []): + await self._clean_git(context, git) + + return module.ProcessOutput() + + async def _clean_docker( + self, context: module.ProcessContext[configuration.CleanConfiguration, _ActionData, None] + ) -> None: + """Clean the Docker images on Docker Hub for the branch we delete.""" + # get the .github/publish.yaml + + try: + publish_configuration_content = context.github_project.repo.get_contents(".github/publish.yaml") + assert not isinstance(publish_configuration_content, list) + publish_config = cast( + tag_publish.configuration.Configuration, + yaml.load(publish_configuration_content.decoded_content, Loader=yaml.SafeLoader), + ) + except github.UnknownObjectException as exception: + if exception.status != 404: + raise + return + + name = context.module_event_data.name + if context.module_event_data.type == "pull_request": + transformers = publish_config.get( + "transformers", + cast(tag_publish.configuration.Transformers, tag_publish.configuration.TRANSFORMERS_DEFAULT), + ) + pull_match = tag_publish.match( + name.split("/", 2)[2], + tag_publish.compile_re( + transformers.get( + "pull_request_to_version", cast(tag_publish.configuration.Transform, [{}]) + ) + ), + ) + name = tag_publish.get_value(*pull_match) + + for repo in ( + publish_config.get("docker", {}) + .get( + "repository", + cast( + dict[str, tag_publish.configuration.DockerRepository], + tag_publish.configuration.DOCKER_REPOSITORY_DEFAULT, + ), + ) + .values() + ): + if context.module_event_data.type not in repo.get( + "versions_type", + tag_publish.configuration.DOCKER_REPOSITORY_VERSIONS_DEFAULT, + ): + continue + host = repo.get("host", "docker.io") + if host not in ["docker.io", "ghcr.io"]: + _LOGGER.warning("Unsupported host %s", host) + continue + for image in publish_config.get("docker", {}).get("images", []): + for tag in image.get("tags", []): + tag = tag.format(version=name) + _LOGGER.info("Cleaning %s/%s:%s", host, image["name"], tag) + + if host == "docker.io": + self._clean_docker_hub_image(image["name"], tag) + else: + self._clean_ghcr_image(image["name"], tag, context.github_project) + + def _clean_docker_hub_image(self, image: str, tag: str) -> None: + username = os.environ["DOCKERHUB_USERNAME"] + password = os.environ["DOCKERHUB_PASSWORD"] + token = requests.post( + "https://hub.docker.com/v2/users/login/", + headers={"Content-Type": "application/json"}, + data=json.dumps( + { + "username": username, + "password": password, + } + ), + timeout=int(os.environ.get("GHCI_REQUESTS_TIMEOUT", "30")), + ).json()["token"] + + response = requests.head( + f"https://hub.docker.com/v2/repositories/{image}/tags/{tag}/", + headers={"Authorization": "JWT " + token}, + timeout=int(os.environ.get("C2CCIUTILS_TIMEOUT", "30")), + ) + if response.status_code == 404: + return + if not response.ok: + _LOGGER.error("Error checking image: docker.io/%s:%s", image, tag) + + response = requests.delete( + f"https://hub.docker.com/v2/repositories/{image}/tags/{tag}/", + headers={"Authorization": "JWT " + token}, + timeout=int(os.environ.get("C2CCIUTILS_TIMEOUT", "30")), + ) + if not response.ok: + _LOGGER.error("Error on deleting image: docker.io/%s:%s", image, tag) + + def _clean_ghcr_image(self, image: str, tag: str, github_project: GithubProject) -> None: + image_split = image.split("/", 1) + response = requests.delete( + f"https://api.github.com/orgs/{image_split[0]}/packages/container/{image_split[1]}/versions/{tag}", + headers={ + "Authorization": "Bearer " + github_project.token, + "Accept": "application/vnd.github.v3+json", + "X-GitHub-Api-Version": "2022-11-28", + }, + ) + if not response.ok: + _LOGGER.error("Error on deleting image: ghcr.io/%s:%s", image, tag) + + async def _clean_git( + self, + context: module.ProcessContext[configuration.CleanConfiguration, _ActionData, None], + git: configuration.Git, + ) -> None: + """Clean the Git repository for the branch we delete.""" + if git.get("on-type", configuration.ON_TYPE_DEFAULT) not in (context.module_event_data.type, "all"): + return + + branch = git.get("branch", configuration.BRANCH_DEFAULT) + folder = git.get("folder", configuration.FOLDER_DEFAULT).format(name=context.module_event_data.name) + + async with module_utils.WORKING_DIRECTORY_LOCK: + # Checkout the right branch on a temporary directory + with tempfile.TemporaryDirectory() as tmpdirname: + os.chdir(tmpdirname) + _LOGGER.debug("Clone the repository in the temporary directory: %s", tmpdirname) + success = module_utils.git_clone(context.github_project, branch) + if not success: + _LOGGER.error( + "Error on cloning the repository %s/%s", + context.github_project.owner, + context.github_project.repository, + ) + + os.chdir(context.github_project.repository) + subprocess.run(["git", "rm", folder], check=True) + subprocess.run( + [ + "git", + "commit", + "-m", + f"Delete {folder} to clean {context.module_event_data.type} {context.module_event_data.name}", + ], + check=True, + ) + subprocess.run(["git", "push", "origin", branch], check=True) diff --git a/github_app_geo_project/module/clean/configuration.py b/github_app_geo_project/module/clean/configuration.py new file mode 100644 index 0000000000..47cac1e62e --- /dev/null +++ b/github_app_geo_project/module/clean/configuration.py @@ -0,0 +1,85 @@ +"""Automatically generated file from a JSON schema.""" + +from typing import Literal, TypedDict, Union + +BRANCH_DEFAULT = "gh-pages" +""" Default value of the field path 'git branch' """ + + +class CleanConfiguration(TypedDict, total=False): + """Clean configuration.""" + + docker: bool + """ + docker. + + Clean the docker images made from feature branches and pull requests + + default: True + """ + + git: list["Git"] + + +class CleanModulesConfiguration(TypedDict, total=False): + """Clean modules configuration.""" + + clean: "CleanConfiguration" + """ Clean configuration. """ + + +DOCKER_DEFAULT = True +""" Default value of the field path 'Clean configuration docker' """ + + +FOLDER_DEFAULT = "{name}" +""" Default value of the field path 'git folder' """ + + +# | git. +# | +# | Clean a folder from a branch +Git = TypedDict( + "Git", + { + # | on-type. + # | + # | feature_branch, pull_request or all + # | + # | default: all + "on-type": "OnType", + # | branch. + # | + # | The branch on witch one the folder will be cleaned + # | + # | default: gh-pages + "branch": str, + # | folder. + # | + # | The folder to be cleaned, can contains {name}, that will be replaced with the branch name or pull request number + # | + # | default: {name} + "folder": str, + }, + total=False, +) + + +ON_TYPE_DEFAULT = "all" +""" Default value of the field path 'git on-type' """ + + +OnType = Union[Literal["feature_branch"], Literal["pull_request"], Literal["all"]] +""" +on-type. + +feature_branch, pull_request or all + +default: all +""" +ONTYPE_FEATURE_BRANCH: Literal["feature_branch"] = "feature_branch" +"""The values for the 'on-type' enum""" +ONTYPE_PULL_REQUEST: Literal["pull_request"] = "pull_request" +"""The values for the 'on-type' enum""" +ONTYPE_ALL: Literal["all"] = "all" +"""The values for the 'on-type' enum""" diff --git a/github_app_geo_project/module/clean/schema.json b/github_app_geo_project/module/clean/schema.json new file mode 100644 index 0000000000..982c654b6e --- /dev/null +++ b/github_app_geo_project/module/clean/schema.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/camptocamp/github-app-geo-project/github_app_geo_project/module/audit/schema.json", + "type": "object", + "title": "Clean modules configuration", + "additionalProperties": false, + "properties": { + "clean": { + "type": "object", + "title": "Clean configuration", + "additionalProperties": false, + "properties": { + "docker": { + "title": "docker", + "type": "boolean", + "description": "Clean the docker images made from feature branches and pull requests", + "default": true + }, + "git": { + "type": "array", + "items": { + "type": "object", + "title": "git", + "description": "Clean a folder from a branch", + "additionalProperties": false, + "properties": { + "on-type": { + "title": "on-type", + "type": "string", + "description": "feature_branch, pull_request or all", + "default": "all", + "enum": ["feature_branch", "pull_request", "all"] + }, + "branch": { + "title": "branch", + "type": "string", + "description": "The branch on witch one the folder will be cleaned", + "default": "gh-pages" + }, + "folder": { + "title": "folder", + "type": "string", + "description": "The folder to be cleaned, can contains {name}, that will be replaced with the branch name or pull request number", + "default": "{name}" + } + } + } + } + } + } + } +} diff --git a/jsonschema-gentypes.yaml b/jsonschema-gentypes.yaml index 316161debc..e55636e7f0 100644 --- a/jsonschema-gentypes.yaml +++ b/jsonschema-gentypes.yaml @@ -33,3 +33,5 @@ generate: destination: github_app_geo_project/module/delete_old_workflow_runs/configuration.py - source: github_app_geo_project/module/versions/schema.json destination: github_app_geo_project/module/versions/configuration.py + - source: github_app_geo_project/module/clean/schema.json + destination: github_app_geo_project/module/clean/configuration.py diff --git a/poetry.lock b/poetry.lock index ba9634219a..04ae4ab7de 100644 --- a/poetry.lock +++ b/poetry.lock @@ -45,6 +45,23 @@ files = [ docs = ["mkdocs", "mkdocs-material", "mkdocs-material-extensions", "mkdocstrings", "mkdocstrings-python", "pymdown-extensions"] test = ["pytest", "pytest-cov"] +[[package]] +name = "applications-download" +version = "0.7.1" +description = "Tools used to publish Python packages, Docker images and Helm charts for GitHub tag and branch" +optional = false +python-versions = ">=3.9" +files = [ + {file = "applications_download-0.7.1-py3-none-any.whl", hash = "sha256:cb4c1a686d8b2522b3b3fe4632a8305cef59f31a02dfd7e5588292d58dc8f917"}, + {file = "applications_download-0.7.1.tar.gz", hash = "sha256:b385e3c88fb41fd50121ae67e3c69f58613dc252e501e6aff6aea4c4698b1f98"}, +] + +[package.dependencies] +jsonschema-validator-new = ">=0.0.0,<1.0.0" +PyYAML = ">=6.0.0,<7.0.0" +requests = ">=2.0.0,<3.0.0" +"ruamel.yaml" = ">=0.0.0,<1.0.0" + [[package]] name = "apt-repo" version = "0.5" @@ -96,6 +113,21 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +[[package]] +name = "backports-tarfile" +version = "1.2.0" +description = "Backport of CPython tarfile module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, + {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] + [[package]] name = "bandit" version = "1.8.0" @@ -490,6 +522,20 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "configupdater" +version = "3.2" +description = "Parser like ConfigParser but for updating configuration files" +optional = false +python-versions = ">=3.6" +files = [ + {file = "ConfigUpdater-3.2-py2.py3-none-any.whl", hash = "sha256:0f65a041627d7693840b4dd743581db4c441c97195298a29d075f91b79539df2"}, + {file = "ConfigUpdater-3.2.tar.gz", hash = "sha256:9fdac53831c1b062929bf398b649b87ca30e7f1a735f3fbf482072804106306b"}, +] + +[package.extras] +testing = ["flake8", "pytest", "pytest-cov", "pytest-randomly", "pytest-xdist", "sphinx"] + [[package]] name = "cornice" version = "6.1.0" @@ -840,6 +886,39 @@ files = [ docs = ["Sphinx", "pylons-sphinx-themes", "setuptools", "watchdog"] testing = ["mock", "pytest", "pytest-cov", "watchdog"] +[[package]] +name = "id" +version = "1.5.0" +description = "A tool for generating OIDC identities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658"}, + {file = "id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d"}, +] + +[package.dependencies] +requests = "*" + +[package.extras] +dev = ["build", "bump (>=1.3.2)", "id[lint,test]"] +lint = ["bandit", "interrogate", "mypy", "ruff (<0.8.2)", "types-requests"] +test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"] + +[[package]] +name = "identify" +version = "2.6.3" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, + {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "idna" version = "3.10" @@ -887,6 +966,29 @@ rawpy = ["numpy (>2)", "rawpy"] test = ["fsspec[github]", "pytest", "pytest-cov"] tifffile = ["tifffile"] +[[package]] +name = "importlib-metadata" +version = "8.5.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -912,6 +1014,93 @@ files = [ [package.extras] colors = ["colorama (>=0.4.6)"] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"}, + {file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"}, +] + +[package.dependencies] +"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-functools" +version = "4.1.0" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649"}, + {file = "jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] +type = ["pytest-mypy"] + +[[package]] +name = "jeepney" +version = "0.8.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, +] + +[package.extras] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["async_generator", "trio"] + +[[package]] +name = "json5" +version = "0.10.0" +description = "A Python implementation of the JSON5 data format." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa"}, + {file = "json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559"}, +] + +[package.extras] +dev = ["build (==1.2.2.post1)", "coverage (==7.5.3)", "mypy (==1.13.0)", "pip (==24.3.1)", "pylint (==3.2.3)", "ruff (==0.7.3)", "twine (==5.1.1)", "uv (==0.5.1)"] + [[package]] name = "jsonmerge" version = "1.9.2" @@ -961,6 +1150,51 @@ files = [ [package.dependencies] referencing = ">=0.31.0" +[[package]] +name = "jsonschema-validator-new" +version = "0.3.2" +description = "Tool to validate files against a JSON Schema" +optional = false +python-versions = ">=3.9" +files = [ + {file = "jsonschema_validator_new-0.3.2-py3-none-any.whl", hash = "sha256:1b18a2b2a6115c00ec879d2dc6f99fc657f1086f62b0abef1484165ff19b1b8d"}, + {file = "jsonschema_validator_new-0.3.2.tar.gz", hash = "sha256:622a93406d7ebc14ab40f54f83688ccbf0ee37acf81b9543c82914c5077555ba"}, +] + +[package.dependencies] +jsonschema = "*" +requests = "*" +"ruamel.yaml" = "*" + +[[package]] +name = "keyring" +version = "25.5.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "keyring-25.5.0-py3-none-any.whl", hash = "sha256:e67f8ac32b04be4714b42fe84ce7dad9c40985b9ca827c592cc303e7c26d9741"}, + {file = "keyring-25.5.0.tar.gz", hash = "sha256:4c753b3ec91717fe713c4edd522d625889d8973a349b0e582622f49766de58e6"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +"jaraco.classes" = "*" +"jaraco.context" = "*" +"jaraco.functools" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +completion = ["shtab (>=1.1.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] +type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"] + [[package]] name = "lazy-loader" version = "0.4" @@ -1323,6 +1557,39 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "more-itertools" +version = "10.5.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"}, + {file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"}, +] + +[[package]] +name = "multi-repo-automation" +version = "1.5.0" +description = "Library for automation updates on multiple repositories." +optional = false +python-versions = ">=3.9" +files = [ + {file = "multi_repo_automation-1.5.0-py3-none-any.whl", hash = "sha256:60590e3a0fd5c67bb1525340d0f25f02d4e7304576cd7cfc596f182ec343220c"}, + {file = "multi_repo_automation-1.5.0.tar.gz", hash = "sha256:a64fa7a7134eaa42c3bf5cfc7cac3d143cfa0607cb06c3da9c77edc23e067f2b"}, +] + +[package.dependencies] +configupdater = "*" +identify = "*" +json5 = "*" +PyYAML = "*" +requests = "*" +"ruamel.yaml" = "*" +security_md = "*" +tomlkit = "*" +typing-extensions = "*" + [[package]] name = "mypy" version = "1.13.0" @@ -1405,6 +1672,39 @@ example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] +[[package]] +name = "nh3" +version = "0.2.19" +description = "Python bindings to the ammonia HTML sanitization library." +optional = false +python-versions = "*" +files = [ + {file = "nh3-0.2.19-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ec9c8bf86e397cb88c560361f60fdce478b5edb8b93f04ead419b72fbe937ea6"}, + {file = "nh3-0.2.19-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0adf00e2b2026fa10a42537b60d161e516f206781c7515e4e97e09f72a8c5d0"}, + {file = "nh3-0.2.19-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3805161c4e12088bd74752ba69630e915bc30fe666034f47217a2f16b16efc37"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3dedd7858a21312f7675841529941035a2ac91057db13402c8fe907aa19205a"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:0b6820fc64f2ff7ef3e7253a093c946a87865c877b3889149a6d21d322ed8dbd"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:833b3b5f1783ce95834a13030300cea00cbdfd64ea29260d01af9c4821da0aa9"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5d4f5e2189861b352b73acb803b5f4bb409c2f36275d22717e27d4e0c217ae55"}, + {file = "nh3-0.2.19-cp313-cp313t-win32.whl", hash = "sha256:2b926f179eb4bce72b651bfdf76f8aa05d167b2b72bc2f3657fd319f40232adc"}, + {file = "nh3-0.2.19-cp313-cp313t-win_amd64.whl", hash = "sha256:ac536a4b5c073fdadd8f5f4889adabe1cbdae55305366fb870723c96ca7f49c3"}, + {file = "nh3-0.2.19-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c2e3f0d18cc101132fe10ab7ef5c4f41411297e639e23b64b5e888ccaad63f41"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11270b16c1b012677e3e2dd166c1aa273388776bf99a3e3677179db5097ee16a"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc483dd8d20f8f8c010783a25a84db3bebeadced92d24d34b40d687f8043ac69"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d53a4577b6123ca1d7e8483fad3e13cb7eda28913d516bd0a648c1a473aa21a9"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdb20740d24ab9f2a1341458a00a11205294e97e905de060eeab1ceca020c09c"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8325d51e47cb5b11f649d55e626d56c76041ba508cd59e0cb1cf687cc7612f1"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8eb7affc590e542fa7981ef508cd1644f62176bcd10d4429890fc629b47f0bc"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2eb021804e9df1761abeb844bb86648d77aa118a663c82f50ea04110d87ed707"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a7b928862daddb29805a1010a0282f77f4b8b238a37b5f76bc6c0d16d930fd22"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed06ed78f6b69d57463b46a04f68f270605301e69d80756a8adf7519002de57d"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:df8eac98fec80bd6f5fd0ae27a65de14f1e1a65a76d8e2237eb695f9cd1121d9"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00810cd5275f5c3f44b9eb0e521d1a841ee2f8023622de39ffc7d88bd533d8e0"}, + {file = "nh3-0.2.19-cp38-abi3-win32.whl", hash = "sha256:7e98621856b0a911c21faa5eef8f8ea3e691526c2433f9afc2be713cb6fbdb48"}, + {file = "nh3-0.2.19-cp38-abi3-win_amd64.whl", hash = "sha256:75c7cafb840f24430b009f7368945cb5ca88b2b54bb384ebfba495f16bc9c121"}, + {file = "nh3-0.2.19.tar.gz", hash = "sha256:790056b54c068ff8dceb443eaefb696b84beff58cca6c07afd754d17692a4804"}, +] + [[package]] name = "numpy" version = "2.1.3" @@ -1643,6 +1943,20 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa typing = ["typing-extensions"] xmp = ["defusedxml"] +[[package]] +name = "pkginfo" +version = "1.10.0" +description = "Query metadata from sdists / bdists / installed packages." +optional = false +python-versions = ">=3.6" +files = [ + {file = "pkginfo-1.10.0-py3-none-any.whl", hash = "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097"}, + {file = "pkginfo-1.10.0.tar.gz", hash = "sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297"}, +] + +[package.extras] +testing = ["pytest", "pytest-cov", "wheel"] + [[package]] name = "plaster" version = "1.1.2" @@ -2290,6 +2604,17 @@ pytest = ">=8.2,<9" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -2352,6 +2677,25 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "readme-renderer" +version = "44.0" +description = "readme_renderer is a library for rendering readme descriptions for Warehouse" +optional = false +python-versions = ">=3.9" +files = [ + {file = "readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151"}, + {file = "readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1"}, +] + +[package.dependencies] +docutils = ">=0.21.2" +nh3 = ">=0.2.14" +Pygments = ">=2.5.1" + +[package.extras] +md = ["cmarkgfm (>=0.8.0)"] + [[package]] name = "redis" version = "5.2.0" @@ -2424,6 +2768,20 @@ requests = ">=2.0.0" [package.extras] rsa = ["oauthlib[signedtoken] (>=3.0.0)"] +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + [[package]] name = "requirements-detector" version = "1.3.2" @@ -2459,6 +2817,20 @@ urllib3 = ">=1.25.10,<3.0" [package.extras] tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"] +[[package]] +name = "rfc3986" +version = "2.0.0" +description = "Validating URI References per RFC 3986" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, + {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, +] + +[package.extras] +idna2008 = ["idna"] + [[package]] name = "rich" version = "13.9.4" @@ -2769,6 +3141,21 @@ dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodest doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<=7.3.7)", "sphinx-design (>=0.4.0)"] test = ["Cython", "array-api-strict (>=2.0)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +[[package]] +name = "secretstorage" +version = "3.3.3" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + [[package]] name = "security-md" version = "0.2.3" @@ -3065,6 +3452,29 @@ files = [ [package.dependencies] pbr = ">=2.0.0" +[[package]] +name = "tag-publish" +version = "0.13.3" +description = "Tools used to publish Python packages, Docker images and Helm charts for GitHub tag and branch" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tag_publish-0.13.3-py3-none-any.whl", hash = "sha256:02d041174d3bb0fd2e9f6f65f3efed391f8f73a12834a6901e2fdfa16149c985"}, + {file = "tag_publish-0.13.3.tar.gz", hash = "sha256:9fda0869a46d0faef65f0b13e8fe4e3ab69d91255dcf83c9fa8324bc8bc588f8"}, +] + +[package.dependencies] +applications-download = ">=0.0.0,<1.0.0" +debian-inspector = ">=31.0.0,<32.0.0" +id = ">=1.0.0,<2.0.0" +jsonschema-validator-new = ">=0.0.0,<1.0.0" +multi-repo-automation = ">=1.0.0,<2.0.0" +PyGithub = ">=2.0.0,<3.0.0" +PyYAML = ">=6.0.0,<7.0.0" +requests = ">=2.0.0,<3.0.0" +security-md = ">=0.0.0,<1.0.0" +twine = ">=5.0.0,<6.0.0" + [[package]] name = "tifffile" version = "2024.9.20" @@ -3152,6 +3562,28 @@ files = [ {file = "trove_classifiers-2024.10.21.16.tar.gz", hash = "sha256:17cbd055d67d5e9d9de63293a8732943fabc21574e4c7b74edf112b4928cf5f3"}, ] +[[package]] +name = "twine" +version = "5.1.1" +description = "Collection of utilities for publishing packages on PyPI" +optional = false +python-versions = ">=3.8" +files = [ + {file = "twine-5.1.1-py3-none-any.whl", hash = "sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997"}, + {file = "twine-5.1.1.tar.gz", hash = "sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db"}, +] + +[package.dependencies] +importlib-metadata = ">=3.6" +keyring = ">=15.1" +pkginfo = ">=1.8.1,<1.11" +readme-renderer = ">=35.0" +requests = ">=2.20" +requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +rfc3986 = ">=1.4.0" +rich = ">=12.0.0" +urllib3 = ">=1.26.0" + [[package]] name = "types-markdown" version = "3.7.0.20240822" @@ -3453,6 +3885,25 @@ files = [ {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, ] +[[package]] +name = "zipp" +version = "3.21.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +files = [ + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + [[package]] name = "zope-deprecation" version = "5.0" @@ -3549,4 +4000,4 @@ test = ["zope.testing"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "8e4defa637622a204ec3071cf0ff3f6ed83800ecf1d4c01d36fbda67c93c8007" +content-hash = "ad834683761ae85c99d2cb43d8f6b26b55bc335796730f91dbd0b5793553bd78" diff --git a/pyproject.toml b/pyproject.toml index 7e96a1b8d2..1442786060 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,7 @@ sentry-sdk = "2.19.0" webob = "1.8.9" waitress = "3.0.2" lxml-html-clean = "0.4.1" +tag-publish = "0.13.3" [tool.poetry.group.dev.dependencies] c2cwsgiutils = { version = "6.1.5", extras = ["test-images"] }