diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 10cb7fc8..3e9874a1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,19 +34,11 @@ jobs: --username ${{ secrets.PYPI_USERNAME }} \ --password ${{ secrets.PYPI_PASSWORD }} - - name: Docker meta server - id: meta_server + - name: Docker meta + id: meta uses: docker/metadata-action@v3 with: - images: aparcar/asu-server - tags: | - type=semver,pattern={{version}} - - - name: Docker meta worker - id: meta_worker - uses: docker/metadata-action@v3 - with: - images: aparcar/asu-worker + images: aparcar/asu tags: | type=semver,pattern={{version}} @@ -56,18 +48,10 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and push ASU server to Docker Hub - uses: docker/build-push-action@v2 - with: - file: Dockerfile.server - push: true - tags: ${{ steps.meta_server.outputs.tags }} - labels: ${{ steps.meta_server.outputs.labels }} - - - name: Build and push ASU worker to Docker Hub + - name: Build and push ASU to Docker Hub uses: docker/build-push-action@v2 with: - file: Dockerfile.worker + file: Containerfile push: true - tags: ${{ steps.meta_worker.outputs.tags }} - labels: ${{ steps.meta_worker.outputs.labels }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1e348396..67bc8a22 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,6 +48,8 @@ jobs: - name: Test with pytest run: | + podman system service --time=0 unix://tmp/podman.sock & + export CONTAINER_HOST="unix:///tmp/podman.sock" poetry run coverage run -m pytest --runslow poetry run coverage xml diff --git a/.gitignore b/.gitignore index e01c0d67..04fd1af3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ instance/ json/ poetry.lock public/ +redis/ site/ store/ var/ diff --git a/Containerfile b/Containerfile new file mode 100644 index 00000000..1a8a6321 --- /dev/null +++ b/Containerfile @@ -0,0 +1,16 @@ +FROM python:3.10-slim + +WORKDIR /app/ + +RUN pip install poetry + +COPY poetry.lock pyproject.toml ./ + +RUN poetry config virtualenvs.create false \ + && poetry install --only main --no-interaction --no-ansi + +COPY ./asu/ ./asu/ + +COPY ./misc/config.py /etc/asu/config.py + +CMD gunicorn 'asu.asu:create_app()' --bind 0.0.0.0:8000 diff --git a/Dockerfile.server b/Dockerfile.server deleted file mode 100644 index ff028e5c..00000000 --- a/Dockerfile.server +++ /dev/null @@ -1,19 +0,0 @@ -FROM python:3.10-slim - -RUN useradd -c "OpenWrt Builder" -m -d /home/build -s /bin/bash build - -USER build - -RUN mkdir /home/build/asu/ - -WORKDIR /home/build/asu/ - -COPY ./misc/config.py /etc/asu/config.py - -RUN pip install --no-cache-dir gunicorn asu - -ENV PATH="/home/build/.local/bin:${PATH}" - -CMD /bin/sh -c 'gunicorn "asu.asu:create_app()" --bind 0.0.0.0:8000' - -EXPOSE 8000 diff --git a/Dockerfile.worker b/Dockerfile.worker deleted file mode 100644 index b94b5d85..00000000 --- a/Dockerfile.worker +++ /dev/null @@ -1,19 +0,0 @@ -FROM openwrt/imagebuilder - -ENV REDIS_HOST="redis://redis" - -RUN mkdir /home/build/asu/ - -WORKDIR /home/build/asu/ - -RUN sudo apt-get -q update \ - && sudo apt-get install -y python3-pip \ - && sudo apt-get clean autoclean \ - && sudo apt-get autoremove --yes \ - && sudo rm -rf /var/lib/{apt,dpkg,cache,log}/ - -RUN pip3 install --no-cache-dir rq asu - -ENV PATH="/home/build/.local/bin:${PATH}" - -CMD /bin/sh -c 'rqworker --url "$REDIS_HOST"' diff --git a/README.md b/README.md index ef75bf77..6e48835b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Attendedsysupgrade Server for OpenWrt (GSoC 2017) +# Attendedsysupgrade Server (GSoC 2017) [![codecov](https://codecov.io/gh/aparcar/asu/branch/master/graph/badge.svg)](https://codecov.io/gh/aparcar/asu) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) @@ -9,11 +9,8 @@ devices running OpenWrt or distributions based on it. These tools offer an easy way to reflash the router with a new firmware version (including all packages) without the need to use `opkg`. -It's called Attended SysUpgrade (ASU) because the upgrade process is not started -automatically, but is initiated by a user who waits until it's done. - -ASU is based on an API (described below) to request custom firmware images with -any selection of packages pre-installed. This avoids the need to set up a build +ASU is based on an [API](#api) to request custom firmware images with any +selection of packages pre-installed. This avoids the need to set up a build environment, and makes it possible to create a custom firmware image even using a mobile device. @@ -59,81 +56,56 @@ immediately without rebuilding. ### Active server - [sysupgrade.openwrt.org](https://sysupgrade.openwrt.org) -- [asu.aparcar.org](https://asu.aparcar.org) -- ~~[chef.libremesh.org](https://chef.libremesh.org)~~ (`CNAME` to - asu.aparcar.org) +- [asu.aparcar.org](http://asu.aparcar.org:8000) +- [asu.hauke-m.de](http://asu.hauke-m.de:8000) ## Run your own server -Redis is required to store image requests: - - sudo apt install redis-server tar - -Install _asu_: - - pip install asu - -Create a `config.py`. -You can use `misc/config.py` as an example. - -Start the server via the following commands: +For security reasons each build happens inside a container so that one build +can't affect another build. For this to work a Podman container runs an API +service so workers can themselfs execute builds inside containers. - export FLASK_APP=asu.asu # set Flask app to asu - flask janitor update # download upstream profiles/packages - this runs forever - flask run # run development server - this runs forever +Please install Podman and test if it works: -Start the worker via the following comand: + podman run --rm -it docker.io/library/alpine:latest - rq worker # this runs forever +Once Podman works, install `podman-compose`: -### Docker + pip install podman-compose -Run the service inside multiple Docker containers. The services include the _ -ASU_ server itself, a _janitor_ service which fills the Redis database with -known packages and profiles as well as a `rqworker` which actually builds -images. - -Currently all services share the same folder and therefore a very "open" access -is required. Suggestions on how to improve this setup are welcome. +Now it's possible to run all services via `podman-compose`: mkdir -p ./asu-service/public/ - chmod -R 777 ./asu-service/ - cp ./misc/config.py ./asu-service/ - docker-compose up + podman-compose up -d -A webserver should proxy API calls to port 8000 of the `server` service while -the `asu/` folder should be file hosted as-is. +This will start the server, the Podman API container and two workers. The first +run needs a few minutes since available packages are parsed from teh upstream +server. Once the server is running, it's possible to request images via the API +on `http://localhost:8000`. Modify `podman-compose.yml` to change the port. ### Production -It is recommended to run _ASU_ via `gunicorn` proxied by `nginx` or -`caddyserver`. Find a possible server configurations in the `misc/` folder. - -The _ASU_ server will try `$PWD/config.py` and `/etc/asu/config.py` to find a -configuration. Find an example configuration in the `misc/` folder. - - pip install gunicorn - gunicorn "asu.asu:create_app()" +For production it's recommended to use a reverse proxy like `nginx` or `caddy`. -Ideally use the tool `squid` to cache package indexes, which are reloaded every -time an image is built. Find a basic configuration in at `misc/squid.conf` -which should be copied to `/etc/squid/squid.conf`. - -If you want to use `systemd` find the service files `asu.service` and -`worker@.service` in the `misc` folder as well. +#### System requirements +- 2 GB RAM (4 GB recommended) +- 2 CPU cores (4 cores recommended) +- 50 GB disk space (200 GB recommended) + ### Development After cloning this repository, create a Python virtual environment and install the dependencies: - python3 -m venv .direnv - source .direnv/bin/activate - pip install -r requirements.txt - export FLASK_APP=asu.asu # set Flask app to asu - export FLASK_APP=tests.conftest:mock_app FLASK_DEBUG=1 # run Flask in debug mode with mock data - flask run +#### Running the server + + poetry install + poetry run flask run + +#### Running a worker + poetry run rq worker ### API The API is documented via _OpenAPI_ and can be viewed interactively on the diff --git a/asu/api.py b/asu/api.py index 01dc04c0..24af6b8e 100644 --- a/asu/api.py +++ b/asu/api.py @@ -1,10 +1,8 @@ -from uuid import uuid4 - from flask import Blueprint, current_app, g, jsonify, redirect, request from rq import Connection, Queue -from .build import build -from .common import get_request_hash, remove_prefix +from asu.build import build +from asu.common import get_redis_client, get_request_hash, remove_prefix bp = Blueprint("api", __name__, url_prefix="/api") @@ -18,14 +16,14 @@ def get_distros() -> list: return ["openwrt"] -def get_redis(): +def redis_client(): """Return Redis connectio Returns: Redis: Configured used Redis connection """ if "redis" not in g: - g.redis = current_app.config["REDIS_CONN"] + g.redis = get_redis_client(current_app.config) return g.redis @@ -38,7 +36,7 @@ def get_queue() -> Queue: if "queue" not in g: with Connection(): g.queue = Queue( - connection=get_redis(), is_async=current_app.config["ASYNC_QUEUE"] + connection=redis_client(), is_async=current_app.config["ASYNC_QUEUE"] ) return g.queue @@ -46,7 +44,7 @@ def get_queue() -> Queue: def api_v1_revision(version, target, subtarget): return jsonify( { - "revision": get_redis() + "revision": redis_client() .get(f"revision:{version}:{target}/{subtarget}") .decode() } @@ -64,50 +62,6 @@ def api_v1_overview(): return redirect("/json/v1/overview.json") -def validate_packages(req): - if req.get("packages_versions") and not req.get("packages"): - req["packages"] = req["packages_versions"].keys() - - if not req.get("packages"): - return - - req["packages"] = set(req["packages"]) - {"kernel", "libc", "libgcc"} - - r = get_redis() - - # translate packages to remove their ABI version for 19.07.x compatibility - tr = set() - for p in req["packages"]: - p_tr = r.hget("mapping-abi", p) - if p_tr: - tr.add(p_tr.decode()) - else: - tr.add(p) - - req["packages"] = set(map(lambda x: remove_prefix(x, "+"), sorted(tr))) - - # store request packages temporary in Redis and create a diff - temp = str(uuid4()) - pipeline = r.pipeline(True) - pipeline.sadd(temp, *set(map(lambda p: p.strip("-"), req["packages"]))) - pipeline.expire(temp, 5) - pipeline.sdiff( - temp, - f"packages:{req['branch']}:{req['version']}:{req['target']}", - f"packages:{req['branch']}:{req['arch']}", - ) - unknown_packages = list(map(lambda p: p.decode(), pipeline.execute()[-1])) - - if unknown_packages: - return ( - { - "detail": f"Unsupported package(s): {', '.join(unknown_packages)}", - "status": 422, - }, - 422, - ) - - def validate_request(req): """Validate an image request and return found errors with status code @@ -154,7 +108,14 @@ def validate_request(req): 400, ) - r = get_redis() + req["packages"] = set( + map( + lambda x: remove_prefix(x, "+"), + sorted(req.get("packages_versions", {}).keys() or req.get("packages", [])), + ) + ) + + r = redis_client() current_app.logger.debug("Profile before mapping " + req["profile"]) @@ -194,10 +155,6 @@ def validate_request(req): 400, ) - package_problems = validate_packages(req) - if package_problems: - return package_problems - return ({}, None) @@ -208,7 +165,7 @@ def return_job_v1(job): response.update(job.meta) if job.is_failed: - response.update({"status": 500}) + response.update({"status": 500, "error": job.latest_result().exc_string}) elif job.is_queued: response.update( @@ -265,27 +222,25 @@ def api_v1_build_post(): failure_ttl = "12h" if "client" in req: - get_redis().hincrby("stats:clients", req["client"]) + redis_client().hincrby("stats:clients", req["client"]) else: if request.headers.get("user-agent").startswith("auc"): - get_redis().hincrby( + redis_client().hincrby( "stats:clients", request.headers.get("user-agent").replace(" (", "/").replace(")", ""), ) else: - get_redis().hincrby("stats:clients", "unknown/0") + redis_client().hincrby("stats:clients", "unknown/0") if job is None: - get_redis().incr("stats:cache-miss") + redis_client().incr("stats:cache-miss") response, status = validate_request(req) if response: return response, status req["store_path"] = current_app.config["STORE_PATH"] - if current_app.config.get("CACHE_PATH"): - req["cache_path"] = current_app.config.get("CACHE_PATH") - req["upstream_url"] = current_app.config["UPSTREAM_URL"] req["branch_data"] = current_app.config["BRANCHES"][req["branch"]] + req["repository_allow_list"] = current_app.config["REPOSITORY_ALLOW_LIST"] req["request_hash"] = request_hash job = get_queue().enqueue( @@ -298,7 +253,7 @@ def api_v1_build_post(): ) else: if job.is_finished: - get_redis().incr("stats:cache-hit") + redis_client().incr("stats:cache-hit") return return_job_v1(job) diff --git a/asu/asu.py b/asu/asu.py index 8c1ebbca..e05f325b 100644 --- a/asu/asu.py +++ b/asu/asu.py @@ -3,14 +3,16 @@ import connexion from flask import Flask, render_template, send_from_directory +from flask_cors import CORS from pkg_resources import resource_filename from prometheus_client import CollectorRegistry, make_wsgi_app -from redis import Redis +from rq import Queue from werkzeug.middleware.dispatcher import DispatcherMiddleware from yaml import safe_load -import asu.common from asu import __version__ +from asu.common import get_redis_client +from asu.janitor import update def create_app(test_config: dict = None) -> Flask: @@ -23,16 +25,13 @@ def create_app(test_config: dict = None) -> Flask: Flask: The application """ - redis_host = getenv("REDIS_HOST", "localhost") - redis_port = getenv("REDIS_PORT", 6379) - redis_password = getenv("REDIS_PASSWORD", "") - cnxn = connexion.FlaskApp(__name__) app = cnxn.app + CORS(app) app.config.from_mapping( JSON_PATH=Path.cwd() / "public/json/v1/", - REDIS_CONN=Redis(host=redis_host, port=redis_port, password=redis_password), + REDIS_URL=getenv("REDIS_URL"), TESTING=False, DEBUG=False, UPSTREAM_URL="https://downloads.openwrt.org", @@ -40,6 +39,7 @@ def create_app(test_config: dict = None) -> Flask: ASYNC_QUEUE=True, BRANCHES_FILE=getenv("BRANCHES_FILE"), MAX_CUSTOM_ROOTFS_SIZE_MB=100, + REPOSITORY_ALLOW_LIST=[], ) if not test_config: @@ -94,7 +94,9 @@ def store_path(path="index.html"): from . import metrics - app.config["REGISTRY"].register(metrics.BuildCollector(app.config["REDIS_CONN"])) + redis_client = get_redis_client(app.config["REDIS_URL"]) + + app.config["REGISTRY"].register(metrics.BuildCollector(redis_client)) branches = dict( map( @@ -112,22 +114,9 @@ def overview(): version=__version__, ) - @app.route("/stats") - def stats(): - branch_stats = {} - for branch, data in branches.items(): - branch_stats.setdefault(branch, {})["profiles"] = asu.common.stats_profiles( - branch - )[0:5] - return render_template( - "stats.html", - versions=asu.common.stats_versions(), - branch_stats=branch_stats, - ) - for package, source in app.config.get("MAPPING_ABI", {}).items(): - if not app.config["REDIS_CONN"].hexists("mapping-abi", package): - app.config["REDIS_CONN"].hset("mapping-abi", package, source) + if not redis_client.hexists("mapping-abi", package): + redis_client.hset("mapping-abi", package, source) cnxn.add_api( "openapi.yml", @@ -137,4 +126,19 @@ def stats(): validate_responses=app.config["TESTING"], ) + if not app.config["TESTING"]: + queue = Queue( + connection=redis_client, + is_async=app.config["ASYNC_QUEUE"], + ) + queue.enqueue( + update, + { + "JSON_PATH": app.config["JSON_PATH"], + "BRANCHES": app.config["BRANCHES"], + "UPSTREAM_URL": app.config["UPSTREAM_URL"], + }, + job_timeout="10m", + ) + return app diff --git a/asu/branches.yml b/asu/branches.yml index a0e05a23..ea21dfaf 100644 --- a/asu/branches.yml +++ b/asu/branches.yml @@ -19,9 +19,9 @@ branches: snapshot: false updates: features versions: + - 22.03.5 - 22.03.4 - 22.03.3 - - 22.03.2 - 22.03-SNAPSHOT package_changes: - source: kmod-nft-nat6 @@ -54,9 +54,9 @@ branches: snapshot: false updates: features versions: + - 21.02.7 - 21.02.6 - 21.02.5 - - 21.02.4 - 21.02-SNAPSHOT SNAPSHOT: diff --git a/asu/build.py b/asu/build.py index bebd23ca..d777f0de 100644 --- a/asu/build.py +++ b/asu/build.py @@ -1,26 +1,29 @@ import json import logging import re -import subprocess from datetime import datetime +from os import getenv from pathlib import Path -from shutil import copyfile, rmtree -import requests +from podman import PodmanClient from rq import get_current_job -from .common import ( +from asu.common import ( + check_manifest, + diff_packages, fingerprint_pubkey_usign, - get_file_hash, + get_container_version_tag, get_packages_hash, - verify_usign, + parse_manifest, + report_error, + run_container, ) log = logging.getLogger("rq.worker") log.setLevel(logging.DEBUG) -def build(req: dict): +def build(req: dict, job=None): """Build image request and setup ImageBuilders automatically The `request` dict contains properties of the requested image. @@ -28,202 +31,128 @@ def build(req: dict): Args: request (dict): Contains all properties of requested image """ + req["store_path"].mkdir(parents=True, exist_ok=True) + log.debug(f"Store path: {req['store_path']}") - def report_error(msg): - log.warning(f"Error: {msg}") - job.meta["detail"] = f"Error: {msg}" - job.save_meta() - raise - - if not req["store_path"].is_dir(): - report_error("Store path missing") - - job = get_current_job() + job = job or get_current_job() job.meta["detail"] = "init" job.save_meta() log.debug(f"Building {req}") - target, subtarget = req["target"].split("/") - cache = req.get("cache_path", Path.cwd()) / "cache" / req["version"] - cache_workdir = cache / target / subtarget - sums_file = Path(cache / target / f"{subtarget}.sha256sums") - sig_file = Path(cache / target / f"{subtarget}.sha256sums.sig") - - def setup_ib(): - """Setup ImageBuilder based on `req` - - This function downloads and verifies the ImageBuilder archive. Existing - setups are automatically updated if newer version are available - upstream. - """ - log.debug("Setting up ImageBuilder") - if (cache_workdir).is_dir(): - rmtree(cache_workdir) - - download_file("sha256sums.sig", sig_file) - download_file("sha256sums", sums_file) - - log.debug("Signatures downloaded" + sig_file.read_text()) - - if not verify_usign(sig_file, sums_file, req["branch_data"]["pubkey"]): - report_error("Bad signature of ImageBuilder archive") - - ib_search = re.search( - r"^(.{64}) \*([a-z]+-imagebuilder-.+?\.Linux-x86_64\.tar\.xz)$", - sums_file.read_text(), - re.MULTILINE, - ) - - if not ib_search: - report_error("Missing Checksum") - - ib_hash, ib_archive = ib_search.groups() - - job.meta["imagebuilder_status"] = "download_imagebuilder" - job.save_meta() - - download_file(ib_archive) - - if ib_hash != get_file_hash(cache / target / ib_archive): - report_error("Bad Checksum") - - (cache_workdir).mkdir(parents=True, exist_ok=True) - - job.meta["imagebuilder_status"] = "unpack_imagebuilder" - job.save_meta() - - extract_archive = subprocess.run( - ["tar", "--strip-components=1", "-xf", ib_archive, "-C", subtarget], - cwd=cache / target, - ) - - if extract_archive.returncode: - report_error("Failed to unpack ImageBuilder archive") - - log.debug(f"Extracted TAR {ib_archive}") - - (cache / target / ib_archive).unlink() - - for key in req["branch_data"].get("extra_keys", []): - fingerprint = fingerprint_pubkey_usign(key) - (cache_workdir / "keys" / fingerprint).write_text( - f"untrusted comment: ASU extra key {fingerprint}\n{key}" - ) - repos_path = cache_workdir / "repositories.conf" - repos = repos_path.read_text() - - extra_repos = req["branch_data"].get("extra_repos") - if extra_repos: - log.debug("Found extra repos") - for name, repo in extra_repos.items(): - repos += f"\nsrc/gz {name} {repo}" - - repos_path.write_text(repos) - log.debug(f"Repos:\n{repos}") - - if (Path.cwd() / "seckey").exists(): - # link key-build to imagebuilder - (cache_workdir / "key-build").symlink_to(Path.cwd() / "seckey") - if (Path.cwd() / "pubkey").exists(): - # link key-build.pub to imagebuilder - (cache_workdir / "key-build.pub").symlink_to(Path.cwd() / "pubkey") - if (Path.cwd() / "newcert").exists(): - # link key-build.ucert to imagebuilder - (cache_workdir / "key-build.ucert").symlink_to(Path.cwd() / "newcert") - - def download_file(filename: str, dest: str = None): - """Download file from upstream target path - - The URL points automatically to the targets folder upstream - - Args: - filename (str): File in upstream target folder - dest (str): Optional path to store the file, default to target - cache folder - """ - log.debug(f"Downloading {filename}") - r = requests.get( - req["upstream_url"] - + "/" - + req["branch_data"]["path"].format(version=req["version"]) - + "/targets/" - + req["target"] - + "/" - + filename + if getenv("CONTAINER_HOST"): + podman = PodmanClient().from_env() + else: + podman = PodmanClient( + base_url="unix:///Users/user/.lima/default/sock/podman.sock" ) - with open(dest or (cache / target / filename), "wb") as f: - f.write(r.content) - - (cache / target).mkdir(parents=True, exist_ok=True) - - stamp_file = cache / target / f"{subtarget}.stamp" + log.debug(f"Podman version: {podman.version()}") - sig_file_headers = requests.head( - req["upstream_url"] - + "/" - + req["branch_data"]["path"].format(version=req["version"]) - + "/targets/" - + req["target"] - + "/sha256sums.sig" - ).headers - log.debug(f"sig_file_headers: \n{sig_file_headers}") + container_version_tag = get_container_version_tag(req["version"]) + log.debug( + f"Container version: {container_version_tag} (requested {req['version']})" + ) - origin_modified = sig_file_headers.get("last-modified") - log.info("Origin %s", origin_modified) + BASE_CONTAINER = "ghcr.io/openwrt/imagebuilder" + image = ( + f"{BASE_CONTAINER}:{req['target'].replace('/', '-')}-{container_version_tag}" + ) - if stamp_file.is_file(): - local_modified = stamp_file.read_text() - log.info("Local %s", local_modified) - else: - local_modified = "" - - if origin_modified != local_modified: - log.debug("New ImageBuilder upstream available") - setup_ib() - stamp_file.write_text(origin_modified) - - if not (cache_workdir / ".config.orig").exists(): - # backup original configuration to keep default filesystems - copyfile( - cache_workdir / ".config", - cache_workdir / ".config.orig", - ) + log.info(f"Pulling {image}...") + podman.images.pull(image) + log.info(f"Pulling {image}... done") - info_run = subprocess.run( - ["make", "info"], text=True, capture_output=True, cwd=cache_workdir + returncode, job.meta["stdout"], job.meta["stderr"] = run_container( + podman, image, ["make", "info"] ) - version_code = re.search('Current Revision: "(r.+)"', info_run.stdout).group(1) + job.save_meta() + + version_code = re.search('Current Revision: "(r.+)"', job.meta["stdout"]).group(1) if "version_code" in req: if version_code != req.get("version_code"): report_error( - f"Received inncorrect version {version_code} (requested {req['version_code']})" + job, + f"Received inncorrect version {version_code} (requested {req['version_code']})", ) default_packages = set( - re.search(r"Default Packages: (.*)\n", info_run.stdout).group(1).split() + re.search(r"Default Packages: (.*)\n", job.meta["stdout"]).group(1).split() ) + log.debug(f"Default packages: {default_packages}") + profile_packages = set( re.search( r"{}:\n .+\n Packages: (.*?)\n".format(req["profile"]), - info_run.stdout, + job.meta["stdout"], re.MULTILINE, ) .group(1) .split() ) - if req.get("diff_packages", False): - remove_packages = (default_packages | profile_packages) - req["packages"] - req["packages"] = req["packages"] | set(map(lambda p: f"-{p}", remove_packages)) + if req.get("diff_packages"): + req["packages"] = diff_packages( + req["packages"], default_packages | profile_packages + ) + log.debug(f"Diffed packages: {req['packages']}") job.meta["imagebuilder_status"] = "calculate_packages_hash" job.save_meta() - manifest_run = subprocess.run( + mounts = [] + + bin_dir = req["request_hash"] + (req["store_path"] / bin_dir / "keys").mkdir(parents=True, exist_ok=True) + log.debug("Created store path: %s", req["store_path"] / bin_dir) + + if "repository_keys" in req: + log.debug("Found extra keys") + + for key in req.get("repository_keys"): + fingerprint = fingerprint_pubkey_usign(key) + log.debug(f"Found key {fingerprint}") + + (req["store_path"] / bin_dir / "keys" / fingerprint).write_text( + f"untrusted comment: {fingerprint}\n{key}" + ) + + mounts.append( + { + "type": "bind", + "source": str(req["store_path"] / bin_dir / "keys" / fingerprint), + "target": "/builder/keys/" + fingerprint, + "read_only": True, + }, + ) + + if "repositories" in req: + log.debug("Found extra repos") + repositories = "" + for name, repo in req.get("repositories").items(): + if repo.startswith(tuple(req["repository_allow_list"])): + repositories += f"src/gz {name} {repo}\n" + else: + report_error(job, f"Repository {repo} not allowed") + + repositories += "src imagebuilder file:packages\noption check_signature" + + (req["store_path"] / bin_dir / "repositories.conf").write_text(repositories) + + mounts.append( + { + "type": "bind", + "source": str(req["store_path"] / bin_dir / "repositories.conf"), + "target": "/builder/repositories.conf", + "read_only": True, + }, + ) + + returncode, job.meta["stdout"], job.meta["stderr"] = run_container( + podman, + image, [ "make", "manifest", @@ -231,148 +160,85 @@ def download_file(filename: str, dest: str = None): f"PACKAGES={' '.join(sorted(req.get('packages', [])))}", "STRIP_ABI=1", ], - text=True, - cwd=cache_workdir, - capture_output=True, + mounts=mounts, ) - job.meta["stdout"] = manifest_run.stdout - job.meta["stderr"] = manifest_run.stderr job.save_meta() - if manifest_run.returncode: - if "Package size mismatch" in manifest_run.stderr: - rmtree(cache_workdir) - return build(req) - else: - print(manifest_run.stdout) - print(manifest_run.stderr) - report_error("Impossible package selection") - - manifest = dict(map(lambda pv: pv.split(" - "), manifest_run.stdout.splitlines())) - - for package, version in req.get("packages_versions", {}).items(): - if package not in manifest: - report_error(f"Impossible package selection: {package} not in manifest") - if version != manifest[package]: - report_error( - f"Impossible package selection: {package} version not as requested: {version} vs. {manifest[package]}" - ) - - manifest_packages = manifest.keys() + if returncode: + report_error(job, "Impossible package selection") - log.debug(f"Manifest Packages: {manifest_packages}") + manifest = parse_manifest(job.meta["stdout"]) + log.debug(f"Manifest: {manifest}") - packages_hash = get_packages_hash(manifest_packages) - log.debug(f"Packages Hash {packages_hash}") + # Check if all requested packages are in the manifest + if err := check_manifest(manifest, req.get("packages_versions", {})): + report_error(job, err) - bin_dir = req["request_hash"] + packages_hash = get_packages_hash(manifest.keys()) + log.debug(f"Packages Hash: {packages_hash}") - (req["store_path"] / bin_dir).mkdir(parents=True, exist_ok=True) - - log.debug("Created store path: %s", req["store_path"] / bin_dir) - - if req.get("filesystem"): - config_path = cache_workdir / ".config" - config = config_path.read_text() - - for filesystem in ["squashfs", "ext4fs", "ubifs", "jffs2"]: - # this implementation uses `startswith` since a running device thinks - # it's running `ext4` while really there is `ext4fs` running - if not filesystem.startswith(req.get("filesystem", filesystem)): - log.debug(f"Disable {filesystem}") - config = config.replace( - f"CONFIG_TARGET_ROOTFS_{filesystem.upper()}=y", - f"# CONFIG_TARGET_ROOTFS_{filesystem.upper()} is not set", - ) - else: - log.debug(f"Enable {filesystem}") - config = config.replace( - f"# CONFIG_TARGET_ROOTFS_{filesystem.upper()} is not set", - f"CONFIG_TARGET_ROOTFS_{filesystem.upper()}=y", - ) - - config_path.write_text(config) - else: - log.debug("Enable default filesystems") - copyfile( - cache_workdir / ".config.orig", - cache_workdir / ".config", - ) - - build_cmd = [ + job.meta["build_cmd"] = [ "make", "image", f"PROFILE={req['profile']}", f"PACKAGES={' '.join(sorted(req.get('packages', [])))}", f"EXTRA_IMAGE_NAME={packages_hash}", - f"BIN_DIR={req['store_path'] / bin_dir}", + f"BIN_DIR=/builder/{bin_dir}", ] + + # Check if custom rootfs size is requested if rootfs_size_mb := req.get("rootfs_size_mb"): - build_cmd.append(f"ROOTFS_PARTSIZE={rootfs_size_mb}") + job.meta["build_cmd"].append(f"ROOTFS_PARTSIZE={rootfs_size_mb}") - log.debug("Build command: %s", build_cmd) + log.debug("Build command: %s", job.meta["build_cmd"]) job.meta["imagebuilder_status"] = "building_image" job.save_meta() if req.get("defaults"): + log.debug("Found defaults") defaults_file = ( - Path(req["store_path"]) / bin_dir / "files/etc/uci-defaults/99-asu-defaults" + req["store_path"] / bin_dir / "files/etc/uci-defaults/99-asu-defaults" ) defaults_file.parent.mkdir(parents=True) defaults_file.write_text(req["defaults"]) - build_cmd.append(f"FILES={req['store_path'] / bin_dir / 'files'}") - - log.debug(f"Running {' '.join(build_cmd)}") + job.meta["build_cmd"].append(f"FILES={req['store_path'] / bin_dir / 'files'}") + mounts.append( + { + "type": "bind", + "source": str(req["store_path"] / bin_dir / "files"), + "target": str(req["store_path"] / bin_dir / "files"), + "read_only": True, + }, + ) - image_build = subprocess.run( - build_cmd, - text=True, - cwd=cache_workdir, - capture_output=True, + returncode, job.meta["stdout"], job.meta["stderr"] = run_container( + podman, + image, + job.meta["build_cmd"], + mounts=mounts, + copy=["/builder/" + bin_dir, req["store_path"]], ) - job.meta["stdout"] = image_build.stdout - job.meta["stderr"] = image_build.stderr - job.meta["build_cmd"] = build_cmd job.save_meta() - if image_build.returncode: - report_error("Error while building firmware. See stdout/stderr") - - if "is too big" in image_build.stderr: - report_error("Selected packages exceed device storage") - - kernel_build_dir_run = subprocess.run( - ["make", "val.KERNEL_BUILD_DIR"], - text=True, - cwd=cache_workdir, - capture_output=True, - ) - - if kernel_build_dir_run.returncode: - report_error("Couldn't determine KERNEL_BUILD_DIR") + if returncode: + report_error(job, "Error while building firmware. See stdout/stderr") - kernel_build_dir_tmp = Path(kernel_build_dir_run.stdout.strip()) / "tmp" - - if kernel_build_dir_tmp.exists(): - log.info("Removing KDIR_TMP at %s", kernel_build_dir_tmp) - rmtree(kernel_build_dir_tmp) - else: - log.warning("KDIR_TMP missing at %s", kernel_build_dir_tmp) + if "is too big" in job.meta["stderr"]: + report_error(job, "Selected packages exceed device storage") json_file = Path(req["store_path"] / bin_dir / "profiles.json") if not json_file.is_file(): - report_error("No JSON file found") + report_error(job, "No JSON file found") json_content = json.loads(json_file.read_text()) + # Check if profile is in JSON file if req["profile"] not in json_content["profiles"]: - report_error("Profile not found in JSON file") - - now_timestamp = int(datetime.now().timestamp()) + report_error(job, "Profile not found in JSON file") json_content.update({"manifest": manifest}) json_content.update(json_content["profiles"][req["profile"]]) @@ -388,43 +254,10 @@ def download_file(filename: str, dest: str = None): job.connection.sadd(f"builds:{version_code}:{req['target']}", req["request_hash"]) + # Increment stats job.connection.hincrby( "stats:builds", - "#".join( - [req["branch_data"]["name"], req["version"], req["target"], req["profile"]] - ), - ) - - # Set last build timestamp for current target/subtarget to now - job.connection.hset( - f"worker:{job.worker_name}:last_build", req["target"], now_timestamp + "#".join([req["version"], req["target"], req["profile"]]), ) - # Iterate over all targets/subtargets of the worker and remove the once inactive for a week - for target_subtarget, last_build_timestamp in job.connection.hgetall( - f"worker:{job.worker_name}:last_build" - ).items(): - target_subtarget = target_subtarget.decode() - - log.debug("now_timestamp %s %s", target_subtarget, now_timestamp) - log.debug( - "last_build_timestamp %s %s", - target_subtarget, - last_build_timestamp.decode(), - ) - - if now_timestamp - int(last_build_timestamp.decode()) > 60 * 60 * 24: - log.info("Removing unused ImageBuilder for %s", target_subtarget) - job.connection.hdel( - f"worker:{job.worker_name}:last_build", target_subtarget - ) - if (cache / target_subtarget).exists(): - rmtree(cache / target_subtarget) - for suffix in [".stamp", ".sha256sums", ".sha256sums.sig"]: - (cache / target_subtarget).with_suffix(suffix).unlink( - missing_ok=True - ) - else: - log.debug("Keeping ImageBuilder for %s", target_subtarget) - return json_content diff --git a/asu/common.py b/asu/common.py index fd67f4f3..641f0586 100644 --- a/asu/common.py +++ b/asu/common.py @@ -1,20 +1,26 @@ import base64 import hashlib import json +import logging import struct from pathlib import Path +from re import match +from shutil import unpack_archive +from tempfile import NamedTemporaryFile import nacl.signing import requests -from flask import current_app +from podman import PodmanClient +import redis -def get_redis(): - return current_app.config["REDIS_CONN"] +def get_redis_client(config): + return redis.from_url(config["REDIS_URL"]) -def is_modified(url: str) -> bool: - r = get_redis() + +def is_modified(config, url: str) -> bool: + r = get_redis_client(config) modified_local = r.hget("last-modified", url) if modified_local: @@ -112,6 +118,8 @@ def get_request_hash(req: dict) -> str: req.get("filesystem", ""), get_str_hash(req.get("defaults", "")), str(req.get("rootfs_size_mb", "")), + str(req.get("repository_keys", "")), + str(req.get("repositories", "")), ] ), 32, @@ -189,3 +197,137 @@ def remove_prefix(text, prefix): str: text without prefix """ return text[text.startswith(prefix) and len(prefix) :] + + +def get_container_version_tag(version: str) -> str: + if match(r"^\d+\.\d+\.\d+$", version): + logging.debug("Version is a release version") + version: str = "v" + version + else: + logging.info(f"Version {version} is a branch") + if version == "SNAPSHOT": + version: str = "master" + else: + version: str = "openwrt-" + version.rstrip("-SNAPSHOT") + + return version + + +def diff_packages(requested_packages: set, default_packages: set): + """Return a list of packages to install and remove + + Args: + requested_packages (set): Set of requested packages + default_packages (set): Set of default packages + + Returns: + set: Set of packages to install and remove""" + remove_packages = default_packages - requested_packages + return requested_packages | set( + map(lambda p: f"-{p}".replace("--", "-"), remove_packages) + ) + + +def run_container(podman: PodmanClient, image, command, mounts=[], copy=[]): + """Run a container and return the returncode, stdout and stderr + + Args: + podman (PodmanClient): Podman client + image (str): Image to run + command (list): Command to run + mounts (list, optional): List of mounts. Defaults to []. + + Returns: + tuple: (returncode, stdout, stderr) + """ + logging.info(f"Running {image} {command} {mounts}") + container = podman.containers.run( + image=image, + command=command, + detach=True, + mounts=mounts, + userns_mode="keep-id", + cap_drop=["all"], + no_new_privileges=True, + privileged=False, + ) + + returncode = container.wait() + + # Podman 4.x changed the way logs are returned + if podman.version()["Version"].startswith("3"): + delimiter = b"\n" + else: + delimiter = b"" + + stdout = delimiter.join(container.logs(stdout=True, stderr=False)).decode("utf-8") + stderr = delimiter.join(container.logs(stdout=False, stderr=True)).decode("utf-8") + + logging.debug(f"returncode: {returncode}") + logging.debug(f"stdout: {stdout}") + logging.debug(f"stderr: {stderr}") + + if copy: + logging.debug(f"Copying {copy[0]} from container to {copy[1]}") + container_tar, _ = container.get_archive(copy[0]) + logging.debug(f"Container tar: {container_tar}") + + host_tar = NamedTemporaryFile(delete=True) + logging.debug(f"Host tar: {host_tar}") + + host_tar.write(b"".join(container_tar)) + + logging.debug(f"Copied {container_tar} to {host_tar}") + + unpack_archive( + host_tar.name, + copy[1], + "tar", + ) + logging.debug(f"Unpacked {host_tar} to {copy[1]}") + + host_tar.close() + logging.debug(f"Closed {host_tar}") + + container.remove(v=True) + + return returncode, stdout, stderr + + +def report_error(job, msg): + logging.warning(f"Error: {msg}") + job.meta["detail"] = f"Error: {msg}" + job.save_meta() + raise + + +def parse_manifest(manifest_content: str): + """Parse a manifest file and return a dictionary + + Args: + manifest (str): Manifest file content + + Returns: + dict: Dictionary of packages and versions + """ + return dict(map(lambda pv: pv.split(" - "), manifest_content.splitlines())) + + +def check_manifest(manifest, packages_versions): + """Validate a manifest file + + Args: + manifest (str): Manifest file content + packages_versions (dict): Dictionary of packages and versions + + Returns: + str: Error message or None + """ + for package, version in packages_versions.items(): + if package not in manifest: + return f"Impossible package selection: {package} not in manifest" + if version != manifest[package]: + return ( + f"Impossible package selection: {package} version not as requested: " + f"{version} vs. {manifest[package]}" + ) diff --git a/asu/janitor.py b/asu/janitor.py index 70f12817..6ec2197c 100644 --- a/asu/janitor.py +++ b/asu/janitor.py @@ -1,140 +1,61 @@ -import email import json -from datetime import datetime +import logging +from datetime import datetime, timedelta from shutil import rmtree -from time import sleep -import click import requests -from flask import Blueprint, current_app +from flask import Blueprint from rq import Queue from rq.exceptions import NoSuchJobError from rq.registry import FinishedJobRegistry from asu import __version__ -from asu.common import get_redis, is_modified +from asu.common import get_redis_client, is_modified bp = Blueprint("janitor", __name__) -def update_set(key: str, *data: list): - pipeline = get_redis().pipeline(True) +def update_set(config: dict, key: str, *data: list): + pipeline = get_redis_client(config).pipeline(True) pipeline.delete(key) pipeline.sadd(key, *data) pipeline.execute() -def parse_packages_file(url, repo): - r = get_redis() - req = requests.get(url) - - if req.status_code != 200: - current_app.logger.warning(f"No Packages found at {url}") - return {} - - packages = {} - mapping = {} - linebuffer = "" - for line in req.text.splitlines(): - if line == "": - parser = email.parser.Parser() - package = parser.parsestr(linebuffer) - source_name = package.get("SourceName") - if source_name: - packages[source_name] = dict( - (name.lower().replace("-", "_"), val) - for name, val in package.items() - ) - packages[source_name]["repository"] = repo - package_name = package.get("Package") - if source_name != package_name: - mapping[package_name] = source_name - else: - current_app.logger.warning(f"Something weird about {package}") - linebuffer = "" - else: - linebuffer += line + "\n" - - for package, source in mapping.items(): - if not r.hexists("mapping-abi", package): - current_app.logger.info(f"{repo}: Add ABI mapping {package} -> {source}") - r.hset("mapping-abi", package, source) - - return packages - - -def get_packages_target_base(branch, version, target): - version_path = branch["path"].format(version=version) - return parse_packages_file( - current_app.config["UPSTREAM_URL"] - + "/" - + version_path - + f"/targets/{target}/packages/Packages.manifest", - target, - ) - - -def get_packages_arch_repo(branch, arch, repo): - version_path = branch["path"].format(version=branch["versions"][0]) - # https://mirror-01.infra.openwrt.org/snapshots/packages/aarch64_cortex-a53/base/ - return parse_packages_file( - current_app.config["UPSTREAM_URL"] - + "/" - + version_path - + f"/packages/{arch}/{repo}/Packages.manifest", - repo, - ) - - -def update_branch(branch): +def update_branch(config, branch): version_path = branch["path"].format(version=branch["versions"][0]) targets = list( filter( lambda t: not t.startswith("."), requests.get( - current_app.config["UPSTREAM_URL"] - + f"/{version_path}/targets?json-targets" + config["UPSTREAM_URL"] + f"/{version_path}/targets?json-targets" ).json(), ) ) if not targets: - current_app.logger.warning("No targets found for {branch['name']}") + logging.warning("No targets found for {branch['name']}") return - update_set(f"targets:{branch['name']}", *list(targets)) - - packages_path = branch["path_packages"].format(branch=branch["name"]) - packages_path = branch["path_packages"].format(branch=branch["name"]) - output_path = current_app.config["JSON_PATH"] / packages_path - output_path.mkdir(exist_ok=True, parents=True) + update_set(config, f"targets:{branch['name']}", *list(targets)) architectures = set() for version in branch["versions"]: - current_app.logger.info(f"Update {branch['name']}/{version}") + logging.info(f"Update {branch['name']}/{version}") # TODO: ugly version_path = branch["path"].format(version=version) - version_path_abs = current_app.config["JSON_PATH"] / version_path - output_path = current_app.config["JSON_PATH"] / packages_path + version_path_abs = config["JSON_PATH"] / version_path version_path_abs.mkdir(exist_ok=True, parents=True) - packages_symlink = version_path_abs / "packages" - - if not packages_symlink.exists(): - packages_symlink.symlink_to(output_path) for target in targets: - update_target_packages(branch, version, target) - - for target in targets: - if target_arch := update_target_profiles(branch, version, target): + if target_arch := update_target_profiles(config, branch, version, target): architectures.add(target_arch) overview = { "branch": branch["name"], "release": version, - "image_url": current_app.config["UPSTREAM_URL"] - + f"/{version_path}/targets/{{target}}", + "image_url": config["UPSTREAM_URL"] + f"/{version_path}/targets/{{target}}", "profiles": [], } @@ -153,125 +74,8 @@ def update_branch(branch): json.dumps(overview, sort_keys=True, separators=(",", ":")) ) - for architecture in architectures: - update_arch_packages(branch, architecture) - - -def update_target_packages(branch: dict, version: str, target: str): - current_app.logger.info(f"{version}/{target}: Update packages") - - version_path = branch["path"].format(version=version) - r = get_redis() - - if not is_modified( - current_app.config["UPSTREAM_URL"] - + "/" - + version_path - + f"/targets/{target}/packages/Packages.manifest" - ): - current_app.logger.debug(f"{version}/{target}: Skip package update") - return - - packages = get_packages_target_base(branch, version, target) - - if len(packages) == 0: - current_app.logger.warning(f"No packages found for {target}") - return - - current_app.logger.debug(f"{version}/{target}: Found {len(packages)}") - - update_set(f"packages:{branch['name']}:{version}:{target}", *list(packages.keys())) - - virtual_packages = { - vpkg.split("=")[0] - for pkg in packages.values() - if (provides := pkg.get("provides")) - for vpkg in provides.split(", ") - } - r.sadd( - f"packages:{branch['name']}:{version}:{target}", - *(virtual_packages | packages.keys()), - ) - - output_path = current_app.config["JSON_PATH"] / version_path / "targets" / target - output_path.mkdir(exist_ok=True, parents=True) - - (output_path / "manifest.json").write_text( - json.dumps(packages, sort_keys=True, separators=(",", ":")) - ) - package_index = dict(map(lambda p: (p[0], p[1]["version"]), packages.items())) - - (output_path / "index.json").write_text( - json.dumps( - { - "architecture": packages["base-files"]["architecture"], - "packages": package_index, - }, - sort_keys=True, - separators=(",", ":"), - ) - ) - - current_app.logger.info(f"{version}: found {len(package_index.keys())} packages") - - -def update_arch_packages(branch: dict, arch: str): - current_app.logger.info(f"Update {branch['name']}/{arch}") - r = get_redis() - - packages_path = branch["path_packages"].format(branch=branch["name"]) - if not is_modified( - current_app.config["UPSTREAM_URL"] + f"/{packages_path}/{arch}/feeds.conf" - ): - current_app.logger.debug(f"{branch['name']}/{arch}: Skip package update") - return - - packages = {} - - # first update extra repos in case they contain redundant packages to core - for name, url in branch.get("extra_repos", {}).items(): - current_app.logger.debug(f"Update extra repo {name} at {url}") - packages.update(parse_packages_file(f"{url}/Packages.manifest", name)) - - # update default repositories afterwards so they overwrite redundancies - for repo in branch["repos"]: - repo_packages = get_packages_arch_repo(branch, arch, repo) - current_app.logger.debug( - f"{branch['name']}/{arch}/{repo}: Found {len(repo_packages)} packages" - ) - packages.update(repo_packages) - - if len(packages) == 0: - current_app.logger.warning(f"{branch['name']}/{arch}: No packages found") - return - - output_path = current_app.config["JSON_PATH"] / packages_path - output_path.mkdir(exist_ok=True, parents=True) - - (output_path / f"{arch}-manifest.json").write_text( - json.dumps(packages, sort_keys=True, separators=(",", ":")) - ) - - package_index = dict(map(lambda p: (p[0], p[1]["version"]), packages.items())) - - (output_path / f"{arch}-index.json").write_text( - json.dumps(package_index, sort_keys=True, separators=(",", ":")) - ) - - current_app.logger.info(f"{arch}: found {len(package_index.keys())} packages") - update_set(f"packages:{branch['name']}:{arch}", *package_index.keys()) - - virtual_packages = { - vpkg.split("=")[0] - for pkg in packages.values() - if (provides := pkg.get("provides")) - for vpkg in provides.split(", ") - } - r.sadd(f"packages:{branch['name']}:{arch}", *(virtual_packages | packages.keys())) - - -def update_target_profiles(branch: dict, version: str, target: str) -> str: +def update_target_profiles(config, branch: dict, version: str, target: str) -> str: """Update available profiles of a specific version Args: @@ -279,26 +83,25 @@ def update_target_profiles(branch: dict, version: str, target: str) -> str: version(str): Version within branch target(str): Target within version """ - current_app.logger.info(f"{version}/{target}: Update profiles") - r = get_redis() + logging.info(f"{version}/{target}: Update profiles") + r = get_redis_client(config) version_path = branch["path"].format(version=version) profiles_url = ( - current_app.config["UPSTREAM_URL"] - + f"/{version_path}/targets/{target}/profiles.json" + config["UPSTREAM_URL"] + f"/{version_path}/targets/{target}/profiles.json" ) req = requests.get(profiles_url) if req.status_code != 200: - current_app.logger.warning("Couldn't download %s", profiles_url) + logging.warning("Couldn't download %s", profiles_url) return False metadata = req.json() profiles = metadata.pop("profiles", {}) - if not is_modified(profiles_url): - current_app.logger.debug(f"{version}/{target}: Skip profiles update") + if not is_modified(config, profiles_url): + logging.debug(f"{version}/{target}: Skip profiles update") return metadata["arch_packages"] r.hset(f"architecture:{branch['name']}", target, metadata["arch_packages"]) @@ -309,16 +112,14 @@ def update_target_profiles(branch: dict, version: str, target: str) -> str: if version_code: version_code = version_code.decode() for request_hash in r.smembers(f"builds:{version_code}:{target}"): - current_app.logger.warning( - f"{version_code}/{target}: Delete outdated job build" - ) + logging.warning(f"{version_code}/{target}: Delete outdated job build") try: request_hash = request_hash.decode() registry.remove(request_hash, delete_job=True) - rmtree(current_app.config["STORE_PATH"] / request_hash) + rmtree(config["STORE_PATH"] / request_hash) except NoSuchJobError: - current_app.logger.warning("Job was already deleted") + logging.warning("Job was already deleted") r.delete(f"builds:{version_code}:{target}") r.set( @@ -326,7 +127,7 @@ def update_target_profiles(branch: dict, version: str, target: str) -> str: metadata["version_code"], ) - current_app.logger.info(f"{version}/{target}: Found {len(profiles)} profiles") + logging.info(f"{version}/{target}: Found {len(profiles)} profiles") pipeline = r.pipeline(True) pipeline.delete(f"profiles:{branch['name']}:{version}:{target}") @@ -334,7 +135,7 @@ def update_target_profiles(branch: dict, version: str, target: str) -> str: for profile, data in profiles.items(): for supported in data.get("supported_devices", []): if not r.hexists(f"mapping:{branch['name']}:{version}:{target}", supported): - current_app.logger.info( + logging.info( f"{version}/{target}: Add profile mapping {supported} -> {profile}" ) r.hset( @@ -344,11 +145,7 @@ def update_target_profiles(branch: dict, version: str, target: str) -> str: pipeline.sadd(f"profiles:{branch['name']}:{version}:{target}", profile) profile_path = ( - current_app.config["JSON_PATH"] - / version_path - / "targets" - / target - / profile + config["JSON_PATH"] / version_path / "targets" / target / profile ).with_suffix(".json") profile_path.parent.mkdir(exist_ok=True, parents=True) profile_path.write_text( @@ -373,13 +170,13 @@ def update_target_profiles(branch: dict, version: str, target: str) -> str: return metadata["arch_packages"] -def update_meta_json(): +def update_meta_json(config): latest = list( map( lambda b: b["versions"][0], filter( lambda b: b.get("enabled"), - current_app.config["BRANCHES"].values(), + config["BRANCHES"].values(), ), ) ) @@ -393,79 +190,65 @@ def update_meta_json(): "targets": dict( map( lambda a: (a[0].decode(), a[1].decode()), - get_redis().hgetall(f"architecture:{b['name']}").items(), + get_redis_client(config) + .hgetall(f"architecture:{b['name']}") + .items(), ) ), }, ), filter( lambda b: b.get("enabled"), - current_app.config["BRANCHES"].values(), + config["BRANCHES"].values(), ), ) ) - current_app.config["OVERVIEW"] = { + config["OVERVIEW"] = { "latest": latest, "branches": branches, "server": { "version": __version__, "contact": "mail@aparcar.org", - "allow_defaults": current_app.config["ALLOW_DEFAULTS"], + "allow_defaults": config["ALLOW_DEFAULTS"], + "repository_allow_list": config["REPOSITORY_ALLOW_LIST"], }, } - (current_app.config["JSON_PATH"] / "overview.json").write_text( - json.dumps( - current_app.config["OVERVIEW"], - indent=2, - sort_keys=False, - default=str - ) + (config["JSON_PATH"] / "overview.json").write_text( + json.dumps(config["OVERVIEW"], indent=2, sort_keys=False, default=str) ) - (current_app.config["JSON_PATH"] / "branches.json").write_text( - json.dumps( - list(branches.values()), - indent=2, - sort_keys=False, - default=str - ) + (config["JSON_PATH"] / "branches.json").write_text( + json.dumps(list(branches.values()), indent=2, sort_keys=False, default=str) ) - (current_app.config["JSON_PATH"] / "latest.json").write_text( - json.dumps({"latest": latest}) - ) + (config["JSON_PATH"] / "latest.json").write_text(json.dumps({"latest": latest})) -@bp.cli.command("update") -@click.option("-i", "--interval", default=10, type=int) -def update(interval): +def update(config): """Update the data required to run the server - For this all available packages and profiles for all enabled versions is + For this all available profiles for all enabled versions is downloaded and stored in the Redis database. """ - current_app.logger.info("Init ASU janitor") - while True: - if not current_app.config["BRANCHES"]: - current_app.logger.error( - "No BRANCHES defined in config, nothing to do, exiting" - ) - return - for branch in current_app.config["BRANCHES"].values(): - if not branch.get("enabled"): - current_app.logger.info(f"{branch['name']}: Skip disabled branch") - continue - current_app.logger.info(f"Update {branch['name']}") - update_branch(branch) + if not config["BRANCHES"]: + logging.error("No BRANCHES defined in config, nothing to do, exiting") + return + for branch in config["BRANCHES"].values(): + if not branch.get("enabled"): + logging.info(f"{branch['name']}: Skip disabled branch") + continue + + logging.info(f"Update {branch['name']}") + update_branch(config, branch) - update_meta_json() + update_meta_json(config) - if interval > 0: - current_app.logger.info(f"Next reload in { interval } minutes") - sleep(interval * 60) - else: - current_app.logger.info("Exiting ASU janitor") - break + Queue(connection=get_redis_client(config)).enqueue_in( + timedelta(minutes=10), + update, + config, + job_timeout="1m", + ) diff --git a/asu/metrics.py b/asu/metrics.py index 359cf5b3..643e3048 100644 --- a/asu/metrics.py +++ b/asu/metrics.py @@ -9,7 +9,7 @@ def collect(self): stats_builds = CounterMetricFamily( "builds", "Total number of built images", - labels=["branch", "version", "target", "profile"], + labels=["version", "target", "profile"], ) for build, count in self.connection.hgetall("stats:builds").items(): stats_builds.add_metric(build.decode().split("#"), count) diff --git a/asu/openapi.yml b/asu/openapi.yml index 44ec6d9d..3ca66506 100644 --- a/asu/openapi.yml +++ b/asu/openapi.yml @@ -13,8 +13,8 @@ externalDocs: description: README.md url: https://github.com/aparcar/asu/blob/master/README.md servers: -- url: https://asu.aparcar.org - description: Running instance of ASU + - url: https://asu.aparcar.org + description: Running instance of ASU paths: /api/v1/overview: get: @@ -30,7 +30,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/JsonSchemaOverview' + $ref: "#/components/schemas/JsonSchemaOverview" /api/v1/build: post: summary: Request a custom firmware image @@ -56,22 +56,22 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/BuildRequest' + $ref: "#/components/schemas/BuildRequest" responses: "200": - $ref: '#/components/responses/ResponseSuccessfull' + $ref: "#/components/responses/ResponseSuccessfull" "202": - $ref: '#/components/responses/ResponseActive' + $ref: "#/components/responses/ResponseActive" "400": - $ref: '#/components/responses/ResponseError' + $ref: "#/components/responses/ResponseError" "422": - $ref: '#/components/responses/ResponseBadPackage' + $ref: "#/components/responses/ResponseBadPackage" "500": - $ref: '#/components/responses/ResponseError' + $ref: "#/components/responses/ResponseError" /api/v1/build/{request_hash}: get: @@ -83,62 +83,62 @@ paths: Ideally clients requests status updates no more then every 5 seconds. operationId: asu.api.api_v1_build_get parameters: - - name: request_hash - in: path - description: | - The hashed request is responded after a successful build request at - `/api/v1/build`. - required: true - style: simple - explode: false - schema: - type: string + - name: request_hash + in: path + description: | + The hashed request is responded after a successful build request at + `/api/v1/build`. + required: true + style: simple + explode: false + schema: + type: string responses: "200": - $ref: '#/components/responses/ResponseSuccessfull' + $ref: "#/components/responses/ResponseSuccessfull" "202": - $ref: '#/components/responses/ResponseActive' + $ref: "#/components/responses/ResponseActive" "404": - $ref: '#/components/responses/ResponseError' + $ref: "#/components/responses/ResponseError" /api/v1/revision/{version}/{target}/{subtarget}: get: summary: receive revision of current target operationId: asu.api.api_v1_revision parameters: - - name: version - in: path - description: Version in question - required: true - style: simple - explode: false - schema: - type: string - - name: target - in: path - description: Target used on device - required: true - style: simple - explode: false - schema: - type: string - - name: subtarget - in: path - description: Target used on device - required: true - style: simple - explode: false - schema: - type: string + - name: version + in: path + description: Version in question + required: true + style: simple + explode: false + schema: + type: string + - name: target + in: path + description: Target used on device + required: true + style: simple + explode: false + schema: + type: string + - name: subtarget + in: path + description: Target used on device + required: true + style: simple + explode: false + schema: + type: string responses: "200": description: Revision found content: application/json: schema: - $ref: '#/components/schemas/JsonSchemaRevision' + $ref: "#/components/schemas/JsonSchemaRevision" components: responses: @@ -147,7 +147,7 @@ components: content: application/json: schema: - $ref: '#/components/schemas/BuildResponseSuccess' + $ref: "#/components/schemas/BuildResponseSuccess" ResponseActive: description: | @@ -159,30 +159,30 @@ components: type: integer description: Current position in build queue content: - application/json: - schema: - $ref: '#/components/schemas/BuildResponseActive' + application/json: + schema: + $ref: "#/components/schemas/BuildResponseActive" ResponseError: description: Invalid build request content: application/json: schema: - $ref: '#/components/schemas/BuildResponseError' + $ref: "#/components/schemas/BuildResponseError" ResponseBadPackage: description: Unknown package(s) in request content: application/json: schema: - $ref: '#/components/schemas/BuildResponseError' + $ref: "#/components/schemas/BuildResponseError" schemas: BuildRequest: required: - - profile - - target - - version + - profile + - target + - version type: object additionalProperties: false properties: @@ -229,8 +229,8 @@ components: packages: type: array example: - - vim - - tmux + - vim + - tmux items: type: string description: | @@ -281,6 +281,27 @@ components: boot. This feature might be dropped in the future. Size is limited to 10kB and can not be exceeded. maxLength: 20480 + repository_keys: + type: array + example: + - RWRNAX5vHtXWFmt+n5di7XX8rTu0w+c8X7Ihv4oCyD6tzsUwmH0A6kO0 + items: + type: string + description: | + List of signify/usign keys for repositories + repositories: + type: object + additionalProperties: + type: string + example: + openwrt_core: https://downloads.openwrt.org/snapshots/targets/x86/64/packages + openwrt_base: https://downloads.openwrt.org/snapshots/packages/x86_64/base + openwrt_luci: https://downloads.openwrt.org/snapshots/packages/x86_64/luci + openwrt_packages: https://downloads.openwrt.org/snapshots/packages/x86_64/packages + openwrt_routing: https://downloads.openwrt.org/snapshots/packages/x86_64/routing + openwrt_telephony: https://downloads.openwrt.org/snapshots/packages/x86_64/telephony + description: | + List of repositories to load packages from example: version: 19.07.8 @@ -308,14 +329,12 @@ components: status: type: integer example: 500 - description: - Always the same as the responding HTTP status code. + description: Always the same as the responding HTTP status code. enqueued_at: type: string format: date-time example: "2021-08-15T09:59:27.754430Z" - description: - Time and date of the build request. + description: Time and date of the build request. request_hash: type: string example: "5992c73895fb" @@ -342,8 +361,7 @@ components: type: string format: date-time example: "2021-08-15T09:59:27.754430Z" - description: - Time and date of the build request. + description: Time and date of the build request. request_hash: type: string example: "5992c73895fb" @@ -353,8 +371,7 @@ components: status: type: integer example: 202 - description: - Always the same as the responding HTTP status code. + description: Always the same as the responding HTTP status code. queue_position: type: integer example: 2 @@ -447,8 +464,8 @@ components: oneOf: - type: object required: - - model - - vendor + - model + - vendor properties: vendor: type: string @@ -530,8 +547,7 @@ components: type: string format: date example: "2021-08-15" - description: - Date of branch end of life + description: Date of branch end of life path: type: string example: "releases/{version}" @@ -580,7 +596,7 @@ components: branches: type: object additionalProperties: - $ref: '#/components/schemas/JsonSchemaBranch' + $ref: "#/components/schemas/JsonSchemaBranch" server: type: object properties: diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 8d14a2b0..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,43 +0,0 @@ -version: "2" - -services: - server: - image: "aparcar/asu-server:latest" - environment: - - REDIS_HOST=redis - volumes: - - "./asu-service:/home/build/asu/" - ports: - - "8000" - depends_on: - - redis - - janitor: - image: "aparcar/asu-server:latest" - environment: - - FLASK_APP=asu.asu - - FLASK_DEBUG=1 - - REDIS_HOST=redis - command: flask janitor update - volumes: - - "./asu-service:/home/build/asu/" - depends_on: - - redis - - worker: - image: "aparcar/asu-worker:latest" - volumes: - - "./asu-service/public/:/home/build/asu/public/" - depends_on: - - redis - - redis: - image: "redis:alpine" - - webserver: - image: caddy - volumes: - - "./misc/Caddyfile:/etc/caddy/Caddyfile" - - "./asu-service:/site/" - ports: - - "8000:80" diff --git a/misc/config.py b/misc/config.py index 794f1677..f1ce1fcb 100644 --- a/misc/config.py +++ b/misc/config.py @@ -10,9 +10,6 @@ # where to store created images STORE_PATH = Path.cwd() / "public/store/" -# where to store ImageBuilders. Do not set when multiple workers run -CACHE_PATH = None - # where to store JSON files JSON_PATH = Path.cwd() / "public/json/v1/" @@ -22,6 +19,14 @@ # manual mapping of package ABI changes MAPPING_ABI = {"libubus20191227": "libubus"} +# External repositories to allow +REPOSITORY_ALLOW_LIST = [ + "http://downloads.openwrt.org", + "https://downloads.openwrt.org", + "http://feed.libremesh.org", + "https://feed.libremesh.org", +] + # connection string for Redis # REDIS_CONN = Redis(host=redis_host, port=redis_port, password=redis_password) diff --git a/podman-compose.yml b/podman-compose.yml new file mode 100644 index 00000000..5f885709 --- /dev/null +++ b/podman-compose.yml @@ -0,0 +1,71 @@ +version: "2" +volumes: + podman-sock: + redis: +services: + server: + image: "aparcar/asu:latest" + build: + context: ./ + dockerfile: Containerfile + environment: + - REDIS_HOST=redis + volumes: + - "./asu-service/public/:/app/public/" + ports: + - "127.0.0.1:8000:8000" + depends_on: + - redis + + worker: + image: "aparcar/asu:latest" + build: + context: ./ + dockerfile: Containerfile + command: rqworker --url "redis://redis" + environment: + - CONTAINER_HOST=unix:///tmp/socket/podman.sock + volumes: + - ./asu-service/public/:/app/public/ + - podman-sock:/tmp/socket/ + depends_on: + - redis + - podman + + worker2: + image: "aparcar/asu:latest" + build: + context: ./ + dockerfile: Containerfile + command: rqworker --url "redis://redis" + environment: + - CONTAINER_HOST=unix:///tmp/socket/podman.sock + volumes: + - ./asu-service/public/:/app/public/ + - podman-sock:/tmp/socket/ + depends_on: + - redis + - podman + + podman: + image: quay.io/podman/stable + user: podman + volumes: + - podman-sock:/tmp/socket/ + command: sh -c "mkdir -p /tmp/socket && podman system service --time=0 unix:///tmp/socket/podman.sock" + + redis: + image: "redis:alpine" + volumes: + - redis:/data/ + +# Podman may not allow ports 1024, consider using an external web server +# webserver: +# image: caddy +# volumes: +# - "./misc/Caddyfile:/etc/caddy/Caddyfile" +# - "./asu-service:/site/" +# ports: +# - "8000:8081" +# depends_on: +# - server diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..5535d826 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1166 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + +[[package]] +name = "async-timeout" +version = "4.0.2" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, + {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, +] + +[[package]] +name = "atomicwrites" +version = "1.4.1" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "black" +version = "22.12.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2023.5.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, + {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, +] + +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "2.1.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] + +[package.extras] +unicode-backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "clickclick" +version = "20.10.2" +description = "Click utility functions" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "clickclick-20.10.2-py2.py3-none-any.whl", hash = "sha256:c8f33e6d9ec83f68416dd2136a7950125bd256ec39ccc9a85c6e280a16be2bb5"}, + {file = "clickclick-20.10.2.tar.gz", hash = "sha256:4efb13e62353e34c5eef7ed6582c4920b418d7dedc86d819e22ee089ba01802c"}, +] + +[package.dependencies] +click = ">=4.0" +PyYAML = ">=3.11" + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "connexion" +version = "2.14.2" +description = "Connexion - API first applications with OpenAPI/Swagger and Flask" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "connexion-2.14.2-py2.py3-none-any.whl", hash = "sha256:a73b96a0e07b16979a42cde7c7e26afe8548099e352cf350f80c57185e0e0b36"}, + {file = "connexion-2.14.2.tar.gz", hash = "sha256:dbc06f52ebeebcf045c9904d570f24377e8bbd5a6521caef15a06f634cf85646"}, +] + +[package.dependencies] +clickclick = ">=1.2,<21" +flask = ">=1.0.4,<2.3" +inflection = ">=0.3.1,<0.6" +itsdangerous = ">=0.24" +jsonschema = ">=2.5.1,<5" +packaging = ">=20" +PyYAML = ">=5.1,<7" +requests = ">=2.9.1,<3" +swagger-ui-bundle = {version = ">=0.0.2,<0.1", optional = true, markers = "extra == \"swagger-ui\""} +werkzeug = ">=1.0,<2.3" + +[package.extras] +aiohttp = ["MarkupSafe (>=0.23)", "aiohttp (>=2.3.10,<4)", "aiohttp-jinja2 (>=0.14.0,<2)"] +docs = ["sphinx-autoapi (==1.8.1)"] +flask = ["flask (>=1.0.4,<2.3)", "itsdangerous (>=0.24)"] +swagger-ui = ["swagger-ui-bundle (>=0.0.2,<0.1)"] +tests = ["MarkupSafe (>=0.23)", "aiohttp (>=2.3.10,<4)", "aiohttp-jinja2 (>=0.14.0,<2)", "aiohttp-remotes", "decorator (>=5,<6)", "flask (>=1.0.4,<2.3)", "itsdangerous (>=0.24)", "pytest (>=6,<7)", "pytest-aiohttp", "pytest-cov (>=2,<3)", "swagger-ui-bundle (>=0.0.2,<0.1)", "testfixtures (>=6,<7)"] + +[[package]] +name = "coverage" +version = "6.5.0" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, + {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, + {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, + {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, + {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, + {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, + {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, + {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, + {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, + {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, + {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, + {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, + {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, + {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, + {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "fakeredis" +version = "2.13.0" +description = "Fake implementation of redis API for testing purposes." +category = "dev" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "fakeredis-2.13.0-py3-none-any.whl", hash = "sha256:df7bb44fb9e593970c626325230e1c321f954ce7b204d4c4452eae5233d554ed"}, + {file = "fakeredis-2.13.0.tar.gz", hash = "sha256:53f00f44f771d2b794f1ea036fa07a33476ab7368f1b0e908daab3eff80336f6"}, +] + +[package.dependencies] +redis = ">=4" +sortedcontainers = ">=2.4,<3.0" + +[package.extras] +json = ["jsonpath-ng (>=1.5,<2.0)"] +lua = ["lupa (>=1.14,<2.0)"] + +[[package]] +name = "flake8" +version = "4.0.1" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, +] + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0" + +[[package]] +name = "flask" +version = "2.2.5" +description = "A simple framework for building complex web applications." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Flask-2.2.5-py3-none-any.whl", hash = "sha256:58107ed83443e86067e41eff4631b058178191a355886f8e479e347fa1285fdf"}, + {file = "Flask-2.2.5.tar.gz", hash = "sha256:edee9b0a7ff26621bd5a8c10ff484ae28737a2410d99b0bb9a6850c7fb977aa0"}, +] + +[package.dependencies] +click = ">=8.0" +importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} +itsdangerous = ">=2.0" +Jinja2 = ">=3.0" +Werkzeug = ">=2.2.2" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "flask-cors" +version = "3.0.10" +description = "A Flask extension adding a decorator for CORS support" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "Flask-Cors-3.0.10.tar.gz", hash = "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"}, + {file = "Flask_Cors-3.0.10-py2.py3-none-any.whl", hash = "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438"}, +] + +[package.dependencies] +Flask = ">=0.9" +Six = "*" + +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, + {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, +] + +[package.dependencies] +setuptools = ">=3.0" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.6.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "importlib_metadata-6.6.0-py3-none-any.whl", hash = "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed"}, + {file = "importlib_metadata-6.6.0.tar.gz", hash = "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] + +[[package]] +name = "importlib-resources" +version = "5.12.0" +description = "Read resources from Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, + {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[[package]] +name = "inflection" +version = "0.5.1" +description = "A port of Ruby on Rails inflector to Python" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, + {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "itsdangerous" +version = "2.1.2" +description = "Safely pass data to untrusted environments and back." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, + {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, +] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jsonschema" +version = "4.17.3" +description = "An implementation of JSON Schema validation for Python" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"}, + {file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"}, +] + +[package.dependencies] +attrs = ">=17.4.0" +importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} +pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} +pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "markupsafe" +version = "2.1.2" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, +] + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pathspec" +version = "0.11.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, + {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, +] + +[[package]] +name = "pkgutil-resolve-name" +version = "1.3.10" +description = "Resolve a name to an object." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, + {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, +] + +[[package]] +name = "platformdirs" +version = "3.5.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.5.1-py3-none-any.whl", hash = "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"}, + {file = "platformdirs-3.5.1.tar.gz", hash = "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f"}, +] + +[package.extras] +docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.2.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "podman" +version = "4.5.0" +description = "Bindings for Podman RESTful API" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "podman-4.5.0-py3-none-any.whl", hash = "sha256:307428e60df9704e2e3bca0ead93babe05fd397e62a3e3a02a2e05d3a779122c"}, + {file = "podman-4.5.0.tar.gz", hash = "sha256:62ff5f89168f348afc39b5efabe388789a4372305e9da67b09b4c4e0eac2bb06"}, +] + +[package.dependencies] +pyxdg = ">=0.26" +requests = ">=2.24" +tomli = {version = ">=1.2.3", markers = "python_version < \"3.11\""} +urllib3 = ">=1.26.5" + +[[package]] +name = "prometheus-client" +version = "0.13.1" +description = "Python client for the Prometheus monitoring system." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "prometheus_client-0.13.1-py3-none-any.whl", hash = "sha256:357a447fd2359b0a1d2e9b311a0c5778c330cfbe186d880ad5a6b39884652316"}, + {file = "prometheus_client-0.13.1.tar.gz", hash = "sha256:ada41b891b79fca5638bd5cfe149efa86512eaa55987893becd2c6d8d0a5dfc5"}, +] + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + +[[package]] +name = "pycodestyle" +version = "2.8.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +] + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "pyflakes" +version = "2.4.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, +] + +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] + +[[package]] +name = "pyrsistent" +version = "0.19.3" +description = "Persistent/Functional/Immutable data structures" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyrsistent-0.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a"}, + {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64"}, + {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf"}, + {file = "pyrsistent-0.19.3-cp310-cp310-win32.whl", hash = "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a"}, + {file = "pyrsistent-0.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da"}, + {file = "pyrsistent-0.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9"}, + {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393"}, + {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19"}, + {file = "pyrsistent-0.19.3-cp311-cp311-win32.whl", hash = "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3"}, + {file = "pyrsistent-0.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-win32.whl", hash = "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b"}, + {file = "pyrsistent-0.19.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8"}, + {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a"}, + {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c"}, + {file = "pyrsistent-0.19.3-cp38-cp38-win32.whl", hash = "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c"}, + {file = "pyrsistent-0.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7"}, + {file = "pyrsistent-0.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc"}, + {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2"}, + {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3"}, + {file = "pyrsistent-0.19.3-cp39-cp39-win32.whl", hash = "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2"}, + {file = "pyrsistent-0.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98"}, + {file = "pyrsistent-0.19.3-py3-none-any.whl", hash = "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64"}, + {file = "pyrsistent-0.19.3.tar.gz", hash = "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440"}, +] + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-httpserver" +version = "1.0.8" +description = "pytest-httpserver is a httpserver for pytest" +category = "dev" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "pytest_httpserver-1.0.8-py3-none-any.whl", hash = "sha256:24cd3d9f6a0b927c7bfc400d0b3fda7442721b8267ce29942bf307b190f0bb09"}, + {file = "pytest_httpserver-1.0.8.tar.gz", hash = "sha256:e052f69bc8a9073db02484681e8e47004dd1fb3763b0ae833bd899e5895c559a"}, +] + +[package.dependencies] +Werkzeug = ">=2.0.0" + +[[package]] +name = "pyxdg" +version = "0.28" +description = "PyXDG contains implementations of freedesktop.org standards in python." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pyxdg-0.28-py2.py3-none-any.whl", hash = "sha256:bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab"}, + {file = "pyxdg-0.28.tar.gz", hash = "sha256:3267bb3074e934df202af2ee0868575484108581e6f3cb006af1da35395e88b4"}, +] + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] + +[[package]] +name = "redis" +version = "4.5.5" +description = "Python client for Redis database and key-value store" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-4.5.5-py3-none-any.whl", hash = "sha256:77929bc7f5dab9adf3acba2d3bb7d7658f1e0c2f1cafe7eb36434e751c471119"}, + {file = "redis-4.5.5.tar.gz", hash = "sha256:dc87a0bdef6c8bfe1ef1e1c40be7034390c2ae02d92dcd0c7ca1729443899880"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""} + +[package.extras] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rq" +version = "1.15.0" +description = "RQ is a simple, lightweight, library for creating background jobs, and processing them." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "rq-1.15.0-py2.py3-none-any.whl", hash = "sha256:6bdc8885bf839a246c4b4e02f2ee31d8f840061fced200f13df9a58a582ec04a"}, + {file = "rq-1.15.0.tar.gz", hash = "sha256:9e89beb034bd253163ac4aa2a0c7d7c7c536aa488df8bbbbc9d354c96e81bb44"}, +] + +[package.dependencies] +click = ">=5.0.0" +redis = ">=4.0.0" + +[[package]] +name = "setuptools" +version = "67.8.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-67.8.0-py3-none-any.whl", hash = "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f"}, + {file = "setuptools-67.8.0.tar.gz", hash = "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "swagger-ui-bundle" +version = "0.0.9" +description = "swagger_ui_bundle - swagger-ui files in a pip package" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "swagger_ui_bundle-0.0.9-py3-none-any.whl", hash = "sha256:cea116ed81147c345001027325c1ddc9ca78c1ee7319935c3c75d3669279d575"}, + {file = "swagger_ui_bundle-0.0.9.tar.gz", hash = "sha256:b462aa1460261796ab78fd4663961a7f6f347ce01760f1303bbbdf630f11f516"}, +] + +[package.dependencies] +Jinja2 = ">=2.0" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.6.2" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.6.2-py3-none-any.whl", hash = "sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98"}, + {file = "typing_extensions-4.6.2.tar.gz", hash = "sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c"}, +] + +[[package]] +name = "urllib3" +version = "1.26.16" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"}, + {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "werkzeug" +version = "2.2.3" +description = "The comprehensive WSGI web application library." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Werkzeug-2.2.3-py3-none-any.whl", hash = "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612"}, + {file = "Werkzeug-2.2.3.tar.gz", hash = "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog"] + +[[package]] +name = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "b272f854837097973b462a1a6a0dc8589220c42d25df763c86f00f3f2a9c7dc7" diff --git a/pyproject.toml b/pyproject.toml index 7701034f..c79ec0c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,8 @@ rq = "^1.13.0" connexion = {extras = ["swagger-ui"], version = "^2.14.2"} prometheus-client = "^0.13.1" gunicorn = "^20.1.0" +podman = "^4.4.1" +flask-cors = "^3.0.10" [tool.poetry.dev-dependencies] pytest = "^6.2.5" diff --git a/tests/conftest.py b/tests/conftest.py index f1cc3689..9aa45bb1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,72 +5,82 @@ import prometheus_client import pytest from fakeredis import FakeStrictRedis +from pytest import MonkeyPatch from asu.asu import create_app -def pytest_addoption(parser): - parser.addoption( - "--runslow", action="store_true", default=False, help="run slow tests" - ) - - -def pytest_configure(config): - config.addinivalue_line("markers", "slow: mark test as slow to run") - - -def pytest_collection_modifyitems(config, items): - if config.getoption("--runslow"): - # --runslow given in cli: do not skip slow tests - return - skip_slow = pytest.mark.skip(reason="need --runslow option to run") - for item in items: - if "slow" in item.keywords: - item.add_marker(skip_slow) - - def redis_load_mock_data(redis): redis.sadd( - "packages:TESTVERSION:TESTVERSION:testtarget/testsubtarget", + "packages:1.2:1.2.3:testtarget/testsubtarget", "test1", "test2", "test3", "valid_new_package", ) - redis.sadd( - "profiles:TESTVERSION:TESTVERSION:testtarget/testsubtarget", "testprofile" - ) + redis.sadd("profiles:1.2:1.2.3:testtarget/testsubtarget", "testprofile") redis.sadd("profiles:SNAPSHOT:SNAPSHOT:ath79/generic", "tplink_tl-wdr4300-v1") redis.sadd("packages:SNAPSHOT:SNAPSHOT:ath79/generic", "vim", "tmux") redis.sadd("packages:SNAPSHOT:SNAPSHOT:x86/64", "vim", "tmux") redis.hset( - "mapping:TESTVERSION:TESTVERSION:testtarget/testsubtarget", + "mapping:1.2:1.2.3:testtarget/testsubtarget", mapping={"testvendor,testprofile": "testprofile"}, ) - redis.sadd("targets:TESTVERSION", "testtarget/testsubtarget") + redis.sadd("targets:1.2", "testtarget/testsubtarget") redis.sadd("targets:SNAPSHOT", "ath79/generic", "x86/64") redis.sadd("targets:21.02", "testtarget/testsubtarget") redis.hset("mapping-abi", mapping={"test1-1": "test1"}) -@pytest.fixture() +@pytest.fixture def redis_server(): r = FakeStrictRedis() + redis_load_mock_data(r) yield r r.flushall() +@pytest.fixture +def mocked_redis(monkeypatch, redis_server): + def mocked_redis_client(*args, **kwargs): + return redis_server + + monkeypatch.setattr("asu.common.get_redis_client", mocked_redis_client) + monkeypatch.setattr("asu.janitor.get_redis_client", mocked_redis_client) + monkeypatch.setattr("asu.api.get_redis_client", mocked_redis_client) + monkeypatch.setattr("asu.asu.get_redis_client", mocked_redis_client) + + +def pytest_addoption(parser): + parser.addoption( + "--runslow", action="store_true", default=False, help="run slow tests" + ) + + +def pytest_configure(config): + config.addinivalue_line("markers", "slow: mark test as slow to run") + + +def pytest_collection_modifyitems(config, items): + if config.getoption("--runslow"): + # --runslow given in cli: do not skip slow tests + return + skip_slow = pytest.mark.skip(reason="need --runslow option to run") + for item in items: + if "slow" in item.keywords: + item.add_marker(skip_slow) + + @pytest.fixture def test_path(): - test_path = tempfile.mkdtemp() + test_path = tempfile.mkdtemp(dir=Path.cwd() / "tests") yield test_path shutil.rmtree(test_path) @pytest.fixture -def app(test_path, redis_server): - redis_load_mock_data(redis_server) +def app(mocked_redis, test_path): registry = prometheus_client.CollectorRegistry(auto_describe=True) @@ -79,11 +89,12 @@ def app(test_path, redis_server): "REGISTRY": registry, "ASYNC_QUEUE": False, "JSON_PATH": test_path + "/json", - "REDIS_CONN": redis_server, + "REDIS_URL": "foobar", "STORE_PATH": test_path + "/store", "CACHE_PATH": test_path, "TESTING": True, "UPSTREAM_URL": "http://localhost:8001", + "REPOSITORY_ALLOW_LIST": [], "BRANCHES": { "SNAPSHOT": { "name": "SNAPSHOT", @@ -99,11 +110,11 @@ def app(test_path, redis_server): "extra_repos": {}, "extra_keys": [], }, - "TESTVERSION": { - "name": "TESTVERSION", + "1.2": { + "name": "1.2", "enabled": True, "snapshot": True, - "versions": ["TESTVERSION"], + "versions": ["1.2.3"], "git_branch": "master", "path": "snapshots", "path_packages": "snapshots/packages", @@ -152,8 +163,7 @@ def app(test_path, redis_server): @pytest.fixture -def app_using_branches_yml(test_path, redis_server): - redis_load_mock_data(redis_server) +def app_using_branches_yml(mocked_redis, test_path): registry = prometheus_client.CollectorRegistry(auto_describe=True) @@ -162,7 +172,6 @@ def app_using_branches_yml(test_path, redis_server): "REGISTRY": registry, "ASYNC_QUEUE": False, "JSON_PATH": test_path + "/json", - "REDIS_CONN": redis_server, "STORE_PATH": test_path + "/store", "CACHE_PATH": test_path, "TESTING": True, @@ -175,8 +184,7 @@ def app_using_branches_yml(test_path, redis_server): @pytest.fixture -def app_using_default_branches(test_path, redis_server): - redis_load_mock_data(redis_server) +def app_using_default_branches(mocked_redis, test_path): registry = prometheus_client.CollectorRegistry(auto_describe=True) @@ -185,7 +193,6 @@ def app_using_default_branches(test_path, redis_server): "REGISTRY": registry, "ASYNC_QUEUE": False, "JSON_PATH": test_path + "/json", - "REDIS_CONN": redis_server, "STORE_PATH": test_path + "/store", "CACHE_PATH": test_path, "TESTING": True, @@ -197,34 +204,10 @@ def app_using_default_branches(test_path, redis_server): @pytest.fixture -def client(app): +def client(mocked_redis, app): return app.test_client() -@pytest.fixture -def runner(app): - return app.test_cli_runner() - - @pytest.fixture(scope="session") def httpserver_listen_address(): return ("127.0.0.1", 8001) - - -@pytest.fixture -def upstream(httpserver): - base_url = "/snapshots/targets/testtarget/testsubtarget" - upstream_path = Path("./tests/upstream/snapshots/targets/testtarget/testsubtarget/") - expected_file_requests = [ - "sha256sums.sig", - "sha256sums", - "openwrt-imagebuilder-testtarget-testsubtarget.Linux-x86_64.tar.xz", - ] - - for f in expected_file_requests: - httpserver.expect_request(f"{base_url}/{f}").respond_with_data( - (upstream_path / f).read_bytes(), - headers={"Last-Modified": "Thu, 19 Mar 2020 20:27:41 GMT"}, - ) - - httpserver.check_assertions() diff --git a/tests/test_api.py b/tests/test_api.py index 5b0e00df..7f9beb4e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,179 +1,119 @@ import pytest -def test_api_build(client, upstream): +def test_api_build(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages=["test1", "test2"], ), ) assert response.status == "200 OK" - assert response.json.get("request_hash") == "df1dfbb6f6deca36b389e4b2917cb8f0" + assert response.json.get("manifest").get("test1") == "1.0" -def test_api_build_filesystem_ext4(app, upstream): - client = app.test_client() +def test_api_build_version_code(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", + version_code="r12647-cb44ab4f5d", target="testtarget/testsubtarget", profile="testprofile", packages=["test1", "test2"], - filesystem="ext4", ), ) assert response.status == "200 OK" - assert response.json.get("request_hash") == "34df61de58ef879888f91d75ccd381f2" - - config = ( - app.config["CACHE_PATH"] / "cache/TESTVERSION/testtarget/testsubtarget/.config" - ).read_text() - assert "# CONFIG_TARGET_ROOTFS_SQUASHFS is not set" in config - assert "CONFIG_TARGET_ROOTFS_EXT4FS=y" in config -def test_api_build_filesystem_squashfs(app, upstream): - client = app.test_client() +def test_api_build_rootfs_size(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages=["test1", "test2"], - filesystem="squashfs", + rootfs_size_mb=100, ), ) assert response.status == "200 OK" - assert response.json.get("request_hash") == "8f9718015c027664b0a8245e39f21d09" - config = ( - app.config["CACHE_PATH"] / "cache/TESTVERSION/testtarget/testsubtarget/.config" - ).read_text() - assert "# CONFIG_TARGET_ROOTFS_EXT4FS is not set" in config - assert "CONFIG_TARGET_ROOTFS_SQUASHFS=y" in config + assert response.json.get("build_cmd")[6] == "ROOTFS_PARTSIZE=100" -def test_api_build_filesystem_empty(app, upstream): - client = app.test_client() +def test_api_build_version_code_bad(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", + version_code="some-bad-version-code", target="testtarget/testsubtarget", profile="testprofile", packages=["test1", "test2"], - filesystem="", ), ) - assert response.status == "200 OK" - assert response.json.get("request_hash") == "df1dfbb6f6deca36b389e4b2917cb8f0" - config = ( - app.config["CACHE_PATH"] / "cache/TESTVERSION/testtarget/testsubtarget/.config" - ).read_text() - assert "CONFIG_TARGET_ROOTFS_EXT4FS=y" in config - assert "CONFIG_TARGET_ROOTFS_SQUASHFS=y" in config + assert response.status == "500 INTERNAL SERVER ERROR" -def test_api_build_filesystem_reset(app, upstream): - client = app.test_client() +def test_api_build_diff_packages(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages=["test1", "test2"], - filesystem="ext4", + diff_packages=True, ), ) assert response.status == "200 OK" - assert response.json.get("request_hash") == "34df61de58ef879888f91d75ccd381f2" - - assert ( - "# CONFIG_TARGET_ROOTFS_SQUASHFS is not set" - in ( - app.config["CACHE_PATH"] - / "cache/TESTVERSION/testtarget/testsubtarget/.config" - ).read_text() - ) - response = client.post( - "/api/v1/build", - json=dict( - version="TESTVERSION", - target="testtarget/testsubtarget", - profile="testprofile", - packages=["test1", "test2"], - ), - ) - assert response.status == "200 OK" - assert response.json.get("request_hash") == "df1dfbb6f6deca36b389e4b2917cb8f0" + # TODO shorten for testing assert ( - "# CONFIG_TARGET_ROOTFS_SQUASHFS is not set" - not in ( - app.config["CACHE_PATH"] - / "cache/TESTVERSION/testtarget/testsubtarget/.config" - ).read_text() + response.json.get("build_cmd")[3] + == "PACKAGES=-base-files -busybox -dnsmasq -dropbear -firewall -fstools -ip6tables -iptables -kmod-ath9k -kmod-gpio-button-hotplug -kmod-ipt-offload -kmod-usb-chipidea2 -kmod-usb-storage -kmod-usb2 -libc -libgcc -logd -mtd -netifd -odhcp6c -odhcpd-ipv6only -opkg -ppp -ppp-mod-pppoe -swconfig -uboot-envtools -uci -uclient-fetch -urandom-seed -urngd -wpad-basic test1 test2" ) -def test_api_build_filesystem_bad(client, upstream): - response = client.post( - "/api/v1/build", - json=dict( - version="TESTVERSION", - target="testtarget/testsubtarget", - profile="testprofile", - packages=["test1", "test2"], - filesystem="bad", - ), - ) - assert response.status == "400 BAD REQUEST" - - def test_api_latest_default(client): response = client.get("/api/latest") assert response.status == "302 FOUND" -def test_api_build_mapping(client, upstream): +def test_api_build_mapping(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testvendor,testprofile", packages=["test1", "test2"], ), ) assert response.status == "200 OK" - assert response.json.get("request_hash") == "697a3aa34dcc7e2577a69960287c3b9b" -def test_api_build_mapping_abi(client, upstream): +def test_api_build_mapping_abi(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testvendor,testprofile", packages=["test1-1", "test2"], ), ) assert response.status == "200 OK" - assert response.json.get("request_hash") == "4c1e7161dd3f0c4ca2ba04a65c6bf0fb" def test_api_build_bad_target(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtargetbad", profile="testvendor,testprofile", packages=["test1", "test2"], @@ -185,43 +125,62 @@ def test_api_build_bad_target(client): ) -def test_api_build_get(client, upstream): +def test_api_build_get(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages=["test1", "test2"], ), ) - assert response.json["request_hash"] == "df1dfbb6f6deca36b389e4b2917cb8f0" - response = client.get("/api/v1/build/df1dfbb6f6deca36b389e4b2917cb8f0") + request_hash = response.json["request_hash"] + response = client.get(f"/api/v1/build/{request_hash}") assert response.status == "200 OK" - assert response.json.get("request_hash") == "df1dfbb6f6deca36b389e4b2917cb8f0" + assert response.json.get("request_hash") == request_hash -def test_api_build_packages_versions(client, upstream): +def test_api_build_packages_versions(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages_versions={"test1": "1.0", "test2": "2.0"}, ), ) - assert response.json["request_hash"] == "bb873a96483917da5b320a7a90b75985" - response = client.get("/api/v1/build/bb873a96483917da5b320a7a90b75985") + request_hash = response.json["request_hash"] + response = client.get(f"/api/v1/build/{request_hash}") assert response.status == "200 OK" - assert response.json.get("request_hash") == "bb873a96483917da5b320a7a90b75985" + assert response.json.get("request_hash") == request_hash -def test_api_build_packages_duplicate(client, upstream): +def test_api_build_packages_versions_bad(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", + target="testtarget/testsubtarget", + profile="testprofile", + packages_versions={"test1": "0.0", "test2": "2.0"}, + ), + ) + request_hash = response.json["request_hash"] + response = client.get(f"/api/v1/build/{request_hash}") + assert response.status == "500 INTERNAL SERVER ERROR" + assert ( + response.json.get("detail") + == "Error: Impossible package selection: test1 version not as requested: 0.0 vs. 1.0" + ) + + +def test_api_build_packages_duplicate(client): + response = client.post( + "/api/v1/build", + json=dict( + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages=["test1", "test2"], @@ -241,31 +200,29 @@ def test_api_build_get_no_post(client): assert response.status == "405 METHOD NOT ALLOWED" -def test_api_build_empty_packages_list(client, upstream): +def test_api_build_empty_packages_list(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages=[], ), ) assert response.status == "200 OK" - assert response.json.get("request_hash") == "c1175efc86abda8d1b03f38204e7dc02" -def test_api_build_withouth_packages_list(client, upstream): +def test_api_build_withouth_packages_list(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", ), ) assert response.status == "200 OK" - assert response.json.get("request_hash") == "c1175efc86abda8d1b03f38204e7dc02" def test_api_build_prerelease_snapshot(client): @@ -296,11 +253,11 @@ def test_api_build_prerelease_rc(client): assert response.json.get("detail") == "Unsupported profile: testprofile" -def test_api_build_bad_packages_str(client, upstream): +def test_api_build_bad_packages_str(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages="testpackage", @@ -393,13 +350,13 @@ def test_api_build_needed(client): assert response.json.get("title") == "Bad Request" response = client.post( "/api/v1/build", - json=dict(version="TESTVERSION", target="testtarget/testsubtarget"), + json=dict(version="1.2.3", target="testtarget/testsubtarget"), ) assert response.status == "400 BAD REQUEST" assert response.json.get("detail") == "'profile' is a required property" assert response.json.get("title") == "Bad Request" response = client.post( - "/api/v1/build", json=dict(version="TESTVERSION", profile="testprofile") + "/api/v1/build", json=dict(version="1.2.3", profile="testprofile") ) assert response.status == "400 BAD REQUEST" assert response.json.get("detail") == "'target' is a required property" @@ -412,7 +369,7 @@ def test_api_build_bad_distro(client): json=dict( distro="Foobar", target="testtarget/testsubtarget", - version="TESTVERSION", + version="1.2.3", profile="testprofile", packages=["test1", "test2"], ), @@ -453,7 +410,7 @@ def test_api_build_bad_profile(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="Foobar", packages=["test1", "test2"], @@ -463,74 +420,24 @@ def test_api_build_bad_profile(client): assert response.json.get("detail") == "Unsupported profile: Foobar" -def test_api_build_bad_packages(client): - response = client.post( - "/api/v1/build", - json=dict( - version="TESTVERSION", - target="testtarget/testsubtarget", - profile="testprofile", - packages=["test4"], - ), - ) - assert response.json.get("detail") == "Unsupported package(s): test4" - assert response.status == "422 UNPROCESSABLE ENTITY" - - -def test_api_build_package_to_remove_diff_packages_false(client, upstream): - response = client.post( - "/api/v1/build", - json=dict( - version="TESTVERSION", - target="testtarget/testsubtarget", - profile="testprofile", - packages=["test1", "test2", "package_to_remove"], - diff_packages=False, - ), - ) - assert response.status == "422 UNPROCESSABLE ENTITY" - - -def test_api_build_cleanup(app, upstream): - client = app.test_client() +def test_api_build_defaults_empty(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", - target="testtarget/testsubtarget", - profile="testprofile", - packages=["test1", "test2"], - filesystem="ext4", - ), - ) - assert response.status == "200 OK" - assert not ( - app.config["CACHE_PATH"] - / "cache/TESTVERSION/testtarget/testsubtarget" - / "pseudo_kernel_build_dir/tmp/" - / "fake_trash" - ).exists() - - -def test_api_build_defaults_empty(client, upstream): - response = client.post( - "/api/v1/build", - json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", defaults="", ), ) assert response.status == "200 OK" - assert response.json.get("request_hash") == "c1175efc86abda8d1b03f38204e7dc02" -def test_api_build_defaults_filled_not_allowed(client, upstream): +def test_api_build_defaults_filled_not_allowed(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", defaults="echo", @@ -540,31 +447,18 @@ def test_api_build_defaults_filled_not_allowed(client, upstream): assert response.status == "400 BAD REQUEST" -def test_api_build_defaults_filled_allowed(app, upstream): +def test_api_build_defaults_filled_allowed(app): app.config["ALLOW_DEFAULTS"] = True client = app.test_client() response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", defaults="echo", ), ) - assert response.status == "200 OK" - assert response.json.get("request_hash") == "95850740d931c460d77f8de35f298b9a" - -def test_api_build_diff_packages(client, upstream): - response = client.post( - "/api/v1/build", - json=dict( - version="TESTVERSION", - target="testtarget/testsubtarget", - profile="testprofile", - packages=["test1", "test2"], - diff_packages=True, - ), - ) assert response.status == "200 OK" + assert response.json.get("request_hash") == "ca6122559630df13592439686ae32ebe" diff --git a/tests/test_common.py b/tests/test_common.py index 97ed2ad6..f1bfaf8b 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,5 +1,6 @@ import os import tempfile +from os import getenv from pathlib import Path, PosixPath from asu.common import * @@ -31,20 +32,17 @@ def test_get_request_hash(): "package_hash": get_packages_hash(["test"]), } - assert get_request_hash(request) == "289a492f0ed178ab35cdd24f9b6b01cf" - -def test_get_request_hash_diff_packages(): - request = { - "distro": "test", - "version": "test", - "profile": "test", - "package_hash": get_packages_hash(["test"]), - "diff_packages": True, +def test_diff_packages(): + assert diff_packages({"test1"}, {"test1", "test2"}) == {"test1", "-test2"} + assert diff_packages({"test1"}, {"test1"}) == {"test1"} + assert diff_packages({"test1"}, {"test2", "test3"}) == {"test1", "-test2", "-test3"} + assert diff_packages({"test1"}, {"test2", "-test3"}) == { + "test1", + "-test2", + "-test3", } - assert get_request_hash(request) == "fe8893a4c872d14e7da222b0810bfd99" - def test_fingerprint_pubkey_usign(): pub_key = "RWSrHfFmlHslUcLbXFIRp+eEikWF9z1N77IJiX5Bt/nJd1a/x+L+SU89" @@ -74,3 +72,38 @@ def test_remove_prefix(): assert remove_prefix("test", "test") == "" assert remove_prefix("+test", "+") == "test" assert remove_prefix("++test", "+") == "+test" + + +def test_get_version_container_tag(): + assert get_container_version_tag("1.0.0") == "v1.0.0" + assert get_container_version_tag("SNAPSHOT") == "master" + assert get_container_version_tag("1.0.0-SNAPSHOT") == "openwrt-1.0.0" + + +def test_check_manifest(): + assert check_manifest({"test": "1.0"}, {"test": "1.0"}) == None + assert ( + check_manifest({"test": "1.0"}, {"test": "2.0"}) + == "Impossible package selection: test version not as requested: 2.0 vs. 1.0" + ) + assert ( + check_manifest({"test": "1.0"}, {"test2": "1.0"}) + == "Impossible package selection: test2 not in manifest" + ) + + +def test_run_container(): + if getenv("CONTAINER_HOST"): + podman = PodmanClient().from_env() + else: + podman = PodmanClient( + base_url="unix:///Users/user/.lima/default/sock/podman.sock" + ) + returncode, stdout, stderr = run_container( + podman, + "ghcr.io/openwrt/imagebuilder:testtarget-testsubtarget-v1.2.3", + ["make", "info"], + ) + + assert returncode == 0 + assert "testtarget/testsubtarget" in stdout diff --git a/tests/test_janitor.py b/tests/test_janitor.py index e1fc5163..ba0a54fc 100644 --- a/tests/test_janitor.py +++ b/tests/test_janitor.py @@ -1,9 +1,7 @@ from pathlib import Path import pytest -from pytest_httpserver import HTTPServer -from asu.build import build from asu.janitor import * @@ -28,14 +26,14 @@ def upstream(httpserver): def test_update_branch(app, upstream): - with app.app_context(): - update_branch(app.config["BRANCHES"]["SNAPSHOT"]) + # with app.app_context(): + update_branch(app.config, app.config["BRANCHES"]["SNAPSHOT"]) assert (app.config["JSON_PATH"] / "snapshots/overview.json").is_file() def test_update_meta_latest_json(app): with app.app_context(): - update_meta_json() + update_meta_json(app.config) latest_json = json.loads((app.config["JSON_PATH"] / "latest.json").read_text()) assert "19.07.7" in latest_json["latest"] assert "21.02.0" in latest_json["latest"] @@ -44,53 +42,6 @@ def test_update_meta_latest_json(app): def test_update_meta_overview_json(app): with app.app_context(): - update_meta_json() + update_meta_json(app.config) overview_json = json.loads((app.config["JSON_PATH"] / "overview.json").read_text()) - assert "package_changes" in overview_json["branches"]["TESTVERSION"] - - -def test_parse_packages_file(app, upstream): - url = ( - app.config["UPSTREAM_URL"] - + "/snapshots/packages/testarch/base/Packages.manifest" - ) - with app.app_context(): - packages = parse_packages_file(url, "base") - assert "6rd" in packages.keys() - - -def test_parse_packages_file_bad(app, upstream): - url = app.config["UPSTREAM_URL"] + "/snapshots/packages/testarch/base/NoPackages" - with app.app_context(): - packages = parse_packages_file(url, "base") - - -def test_get_packages_target_base(app, upstream): - branch = app.config["BRANCHES"]["SNAPSHOT"] - version = "snapshots" - target = "testtarget/testsubtarget" - with app.app_context(): - packages = get_packages_target_base(branch, version, target) - assert "base-files" in packages.keys() - - -def test_update_target_packages(app, upstream): - branch = app.config["BRANCHES"]["SNAPSHOT"] - version = "snapshots" - target = "testtarget/testsubtarget" - with app.app_context(): - packages = update_target_packages(branch, version, target) - assert ( - app.config["JSON_PATH"] - / "snapshots/targets/testtarget/testsubtarget/index.json" - ).is_file() - - -def test_update_arch_packages(app, upstream): - branch = app.config["BRANCHES"]["SNAPSHOT"] - arch = "testarch" - with app.app_context(): - packages = update_arch_packages(branch, arch) - assert ( - app.config["JSON_PATH"] / "snapshots/packages/testarch-index.json" - ).is_file() + assert "package_changes" in overview_json["branches"]["1.2"] diff --git a/tests/test_stats.py b/tests/test_stats.py index 99c1cb82..4e96282b 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -1,58 +1,55 @@ from prometheus_client import REGISTRY -def test_stats_image_builds(client, upstream): +def test_stats_image_builds(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages=["test1", "test2"], ), ) assert response.status == "200 OK" - assert response.json.get("request_hash") == "df1dfbb6f6deca36b389e4b2917cb8f0" response = client.get("/metrics") print(response.get_data(as_text=True)) assert ( - 'builds_total{branch="TESTVERSION",profile="testprofile",target="testtarget/testsubtarget",version="TESTVERSION"} 1.0' + 'builds_total{profile="testprofile",target="testtarget/testsubtarget",version="1.2.3"} 1.0' in response.get_data(as_text=True) ) response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages=["test1"], ), ) assert response.status == "200 OK" - assert response.json.get("request_hash") == "39bfd960818b32759982b09989e62809" response = client.get("/metrics") print(response.get_data(as_text=True)) assert ( - 'builds_total{branch="TESTVERSION",profile="testprofile",target="testtarget/testsubtarget",version="TESTVERSION"} 2.0' + 'builds_total{profile="testprofile",target="testtarget/testsubtarget",version="1.2.3"} 2.0' in response.get_data(as_text=True) ) -def test_stats_cache(client, upstream): +def test_stats_cache(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages=["test1", "test2"], ), ) assert response.status == "200 OK" - assert response.json.get("request_hash") == "df1dfbb6f6deca36b389e4b2917cb8f0" response = client.get("/metrics") print(response.get_data(as_text=True)) @@ -61,25 +58,24 @@ def test_stats_cache(client, upstream): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages=["test1", "test2"], ), ) assert response.status == "200 OK" - assert response.json.get("request_hash") == "df1dfbb6f6deca36b389e4b2917cb8f0" response = client.get("/metrics") print(response.get_data(as_text=True)) assert "cache_hits 1.0" in response.get_data(as_text=True) -def test_stats_clients_luci(client, upstream): +def test_stats_clients_luci(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages=["test1", "test2"], @@ -95,11 +91,11 @@ def test_stats_clients_luci(client, upstream): ) -def test_stats_clients_unknown(client, upstream): +def test_stats_clients_unknown(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages=["test1", "test2"], @@ -113,11 +109,11 @@ def test_stats_clients_unknown(client, upstream): ) -def test_stats_clients_auc(client, upstream): +def test_stats_clients_auc(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages=["test1", "test2"], @@ -132,11 +128,11 @@ def test_stats_clients_auc(client, upstream): ) -def test_stats_clients_auc_possible_new_format(client, upstream): +def test_stats_clients_auc_possible_new_format(client): response = client.post( "/api/v1/build", json=dict( - version="TESTVERSION", + version="1.2.3", target="testtarget/testsubtarget", profile="testprofile", packages=["test1", "test2"], diff --git a/tests/upstream/snapshots/targets/testtarget/testsubtarget/openwrt-imagebuilder-testtarget-testsubtarget.Linux-x86_64.tar.xz b/tests/upstream/snapshots/targets/testtarget/testsubtarget/openwrt-imagebuilder-testtarget-testsubtarget.Linux-x86_64.tar.xz deleted file mode 100644 index 254ae645..00000000 Binary files a/tests/upstream/snapshots/targets/testtarget/testsubtarget/openwrt-imagebuilder-testtarget-testsubtarget.Linux-x86_64.tar.xz and /dev/null differ diff --git a/tests/upstream/snapshots/targets/testtarget/testsubtarget/openwrt-imagebuilder-testtarget-testsubtarget.Linux-x86_64/Makefile b/tests/upstream/snapshots/targets/testtarget/testsubtarget/openwrt-imagebuilder-testtarget-testsubtarget.Linux-x86_64/Makefile index 625d4f9e..8fd0dcee 100644 --- a/tests/upstream/snapshots/targets/testtarget/testsubtarget/openwrt-imagebuilder-testtarget-testsubtarget.Linux-x86_64/Makefile +++ b/tests/upstream/snapshots/targets/testtarget/testsubtarget/openwrt-imagebuilder-testtarget-testsubtarget.Linux-x86_64/Makefile @@ -1,5 +1,6 @@ image: + mkdir -p $(BIN_DIR)/ cp ./openwrt-testtarget-testsubtarget-testprofile-sysupgrade.bin $(BIN_DIR)/ cp ./openwrt-testtarget-testsubtarget-testprofile.manifest $(BIN_DIR)/ cp ./profiles.json $(BIN_DIR)/ diff --git a/tests/upstream/snapshots/targets/testtarget/testsubtarget/sha256sums b/tests/upstream/snapshots/targets/testtarget/testsubtarget/sha256sums deleted file mode 100644 index 35fe96fb..00000000 --- a/tests/upstream/snapshots/targets/testtarget/testsubtarget/sha256sums +++ /dev/null @@ -1 +0,0 @@ -4e645577d934129a65d0f79cf55d90e1199f5ab90ab27803cefae8badc868599 *openwrt-imagebuilder-testtarget-testsubtarget.Linux-x86_64.tar.xz diff --git a/tests/upstream/snapshots/targets/testtarget/testsubtarget/sha256sums.sig b/tests/upstream/snapshots/targets/testtarget/testsubtarget/sha256sums.sig deleted file mode 100644 index 6999e851..00000000 --- a/tests/upstream/snapshots/targets/testtarget/testsubtarget/sha256sums.sig +++ /dev/null @@ -1,2 +0,0 @@ -untrusted comment: verify with testkey.pub -RWRqylWEtrAZQwwqVGXn0U/Q3nnrboIdIVK2aW7Q8JMN3pmCzHa8o5MppWUqMhxh/h2Dc/3WUjVtWgvfDGrR0tJJ/Wauy9r7QwM= diff --git a/tests/upstream/snapshots/targets/testtarget/testsubtarget/update_imagebuilder.sh b/tests/upstream/snapshots/targets/testtarget/testsubtarget/update_imagebuilder.sh deleted file mode 100755 index 1e5404f8..00000000 --- a/tests/upstream/snapshots/targets/testtarget/testsubtarget/update_imagebuilder.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -tar czf openwrt-imagebuilder-testtarget-testsubtarget.Linux-x86_64.tar.xz openwrt-imagebuilder-testtarget-testsubtarget.Linux-x86_64/ -sha256sum -b openwrt-imagebuilder-testtarget-testsubtarget.Linux-x86_64.tar.xz > sha256sums -signify -S -m sha256sums -s ../../../../../keys/testkey.sec -#usign -S -m sha256sums -s ../../../../../keys/testkey.sec