diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d113aab..cd4b22c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,15 +16,17 @@ jobs: - name: Install project run: | pip install -U "pip>=21.3.1" - pip install -e .[test] -c requirements.txt -c requirements-test.txt + pip install -e .[test,mypy] -c requirements.txt -c requirements-test.txt -c requirements-mypy.txt - name: Run tests - run: | - pytest -v --cov --cov-report=xml ./tests + run: pytest -v --cov --cov-report=xml ./tests + - name: Run mypy + run: mypy ./src/runboat ./tests - uses: codecov/codecov-action@v1 build-image: runs-on: ubuntu-latest needs: - test + if: ${{ github.repository_owner == 'sbidoul' && github.ref == 'refs/heads/main' }} steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fb8818b..fc5094c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,4 +39,4 @@ repos: rev: v2.29.0 hooks: - id: pyupgrade - args: ["--py38-plus"] + args: ["--py39-plus"] diff --git a/pyproject.toml b/pyproject.toml index c1509c7..ca069ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,9 @@ test = [ "pytest-cov", "pytest-dotenv", ] +mypy = [ + "mypy", +] [project.urls] Home = "https://github.com/sbidoul/runboat" @@ -39,3 +42,23 @@ env_override_existing_values = 1 env_files = [".env.test"] # flake8 config is in .flake8 + +[tool.mypy] +strict = true +show_error_codes = true + +[[tool.mypy.overrides]] +module = "uvicorn.*" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "urllib3.*" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "kubernetes.*" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "ansi2html" +ignore_missing_imports = true diff --git a/requirements-mypy.txt b/requirements-mypy.txt new file mode 100644 index 0000000..444c145 --- /dev/null +++ b/requirements-mypy.txt @@ -0,0 +1,4 @@ +# frozen requirements generated by pip-deepfreeze +mypy==0.910 +mypy-extensions==0.4.3 +toml==0.10.2 diff --git a/src/runboat/api.py b/src/runboat/api.py index 11dbe83..ca81e4c 100644 --- a/src/runboat/api.py +++ b/src/runboat/api.py @@ -1,14 +1,14 @@ import datetime from typing import Optional -from ansi2html import Ansi2HTMLConverter # type: ignore +from ansi2html import Ansi2HTMLConverter from fastapi import APIRouter, Depends, HTTPException, status from fastapi.responses import HTMLResponse from pydantic import BaseModel from starlette.status import HTTP_404_NOT_FOUND from . import github, models -from .controller import controller +from .controller import Controller, controller from .deps import authenticated from .settings import settings @@ -60,12 +60,12 @@ class Build(BaseModel): @router.get("/status", response_model=Status) -async def controller_status(): +async def controller_status() -> Controller: return controller @router.get("/repos", response_model=list[Repo]) -async def repos(): +async def repos() -> list[models.Repo]: return [models.Repo(name=name) for name in settings.supported_repos] @@ -74,7 +74,7 @@ async def repos(): response_model=list[Build], response_model_exclude_none=True, ) -async def builds(repo: Optional[str] = None): +async def builds(repo: Optional[str] = None) -> list[models.Build]: return controller.db.search(repo) @@ -82,7 +82,7 @@ async def builds(repo: Optional[str] = None): "/builds/trigger/branch", dependencies=[Depends(authenticated)], ) -async def trigger_branch(repo: str, branch: str): +async def trigger_branch(repo: str, branch: str) -> None: """Trigger build for a branch.""" branch_info = await github.get_branch_info(repo, branch) await controller.deploy_or_start( @@ -97,7 +97,7 @@ async def trigger_branch(repo: str, branch: str): "/builds/trigger/pr", dependencies=[Depends(authenticated)], ) -async def trigger_pull(repo: str, pr: int): +async def trigger_pull(repo: str, pr: int) -> None: """Trigger build for a pull request.""" pull_info = await github.get_pull_info(repo, pr) await controller.deploy_or_start( @@ -116,7 +116,7 @@ async def _build_by_name(name: str) -> models.Build: @router.get("/builds/{name}", response_model=Build) -async def build(name: str): +async def build(name: str) -> models.Build: return await _build_by_name(name) @@ -124,42 +124,42 @@ async def build(name: str): "/builds/{name}/init-log", response_class=HTMLResponse, ) -async def init_log(name: str): +async def init_log(name: str) -> str: build = await _build_by_name(name) log = await build.init_log() if not log: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="No log found.") - return Ansi2HTMLConverter().convert(log) + return Ansi2HTMLConverter().convert(log) # type: ignore [no-any-return] @router.get( "/builds/{name}/log", response_class=HTMLResponse, ) -async def log(name: str): +async def log(name: str) -> str: build = await _build_by_name(name) log = await build.log() if not log: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="No log found.") - return Ansi2HTMLConverter().convert(log) + return Ansi2HTMLConverter().convert(log) # type: ignore [no-any-return] @router.post("/builds/{name}/start") -async def start(name: str): +async def start(name: str) -> None: """Start the deployment.""" build = await _build_by_name(name) await build.start() @router.post("/builds/{name}/stop") -async def stop(name: str): +async def stop(name: str) -> None: """Stop the deployment.""" build = await _build_by_name(name) await build.stop() @router.delete("/builds/{name}", dependencies=[Depends(authenticated)]) -async def delete(name: str): +async def delete(name: str) -> None: """Delete the deployment and drop the database.""" build = await _build_by_name(name) await build.undeploy() diff --git a/src/runboat/controller.py b/src/runboat/controller.py index 7180bd0..cc3b8a5 100644 --- a/src/runboat/controller.py +++ b/src/runboat/controller.py @@ -30,7 +30,7 @@ class Controller: """ db: BuildsDb - _tasks: list[asyncio.Task] + _tasks: list[asyncio.Task[None]] _wakeup_initializer: asyncio.Event _wakeup_stopper: asyncio.Event _wakeup_undeployer: asyncio.Event diff --git a/src/runboat/k8s.py b/src/runboat/k8s.py index b0b920b..35f2f13 100644 --- a/src/runboat/k8s.py +++ b/src/runboat/k8s.py @@ -11,12 +11,12 @@ from importlib import resources from pathlib import Path from typing import Any, Callable, Generator, Optional, TypedDict, cast -import urllib3 # type: ignore +import urllib3 from jinja2 import Template -from kubernetes import client, config, watch # type: ignore -from kubernetes.client.exceptions import ApiException # type: ignore -from kubernetes.client.models.v1_deployment import V1Deployment # type: ignore -from kubernetes.client.models.v1_job import V1Job # type: ignore +from kubernetes import client, config, watch +from kubernetes.client.exceptions import ApiException +from kubernetes.client.models.v1_deployment import V1Deployment +from kubernetes.client.models.v1_job import V1Job from pydantic import BaseModel from .settings import settings diff --git a/src/runboat/models.py b/src/runboat/models.py index 609e506..7a2c483 100644 --- a/src/runboat/models.py +++ b/src/runboat/models.py @@ -4,7 +4,7 @@ import uuid from enum import Enum from typing import Optional -from kubernetes.client.models.v1_deployment import V1Deployment # type: ignore +from kubernetes.client.models.v1_deployment import V1Deployment from pydantic import BaseModel from . import github, k8s diff --git a/src/runboat/uvicorn.py b/src/runboat/uvicorn.py index c60e707..aa60081 100644 --- a/src/runboat/uvicorn.py +++ b/src/runboat/uvicorn.py @@ -1,4 +1,4 @@ -from uvicorn.workers import UvicornWorker # type: ignore +from uvicorn.workers import UvicornWorker from .settings import settings diff --git a/src/runboat/webui.py b/src/runboat/webui.py index 7910ac7..9f4a99f 100644 --- a/src/runboat/webui.py +++ b/src/runboat/webui.py @@ -1,7 +1,7 @@ from pathlib import Path from typing import Optional -from fastapi import APIRouter, HTTPException, Request, status +from fastapi import APIRouter, HTTPException, Request, Response, status from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates @@ -13,12 +13,12 @@ templates = Jinja2Templates(directory=str(Path(__file__).parent / "webui")) @router.get("/builds/{name}", response_class=HTMLResponse) -async def build(request: Request, name: str, live: Optional[str] = None): +async def build(request: Request, name: str, live: Optional[str] = None) -> Response: build = controller.db.get(name) if not build: raise HTTPException(status.HTTP_404_NOT_FOUND) if live is not None and build.status == BuildStatus.started: return RedirectResponse(url=build.deploy_link) return templates.TemplateResponse( - "build.html", {"request": request, "build": build} + "build.html.jinja", {"request": request, "build": build} ) diff --git a/src/runboat/webui/build.html b/src/runboat/webui/build.html.jinja similarity index 83% rename from src/runboat/webui/build.html rename to src/runboat/webui/build.html.jinja index 6f63d09..39c53ba 100644 --- a/src/runboat/webui/build.html +++ b/src/runboat/webui/build.html.jinja @@ -18,15 +18,18 @@
Branch: {{ build.target_branch }}
{% endif %}Commit: {{ build.git_commit }}
-Status: {{ build.status }}
+Status: {{ build.status.value }}
Logs: - build log + init log + {% if build.status == 'started' %} | - run log + log + | + => live + {% endif %}
{% if build.status == 'started' %} - {% else %} diff --git a/tests/test_build_images.py b/tests/test_build_images.py index debf669..d45086b 100644 --- a/tests/test_build_images.py +++ b/tests/test_build_images.py @@ -4,20 +4,20 @@ from runboat.build_images import get_build_image, get_main_branch, is_branch_sup from runboat.exceptions import BranchNotSupported -def test_get_main_branch(): +def test_get_main_branch() -> None: assert get_main_branch("15.0") == "15.0" assert get_main_branch("15.0-ocabot-merge") == "15.0" with pytest.raises(BranchNotSupported): get_main_branch("8.0") -def test_is_branch_supported(): +def test_is_branch_supported() -> None: assert is_branch_supported("15.0") assert is_branch_supported("15.0-ocabot-merge") assert not is_branch_supported("8.0") -def test_get_build_image(): +def test_get_build_image() -> None: assert get_build_image("15.0") == "ghcr.io/oca/oca-ci/py3.8-odoo15.0:latest" assert get_build_image("15.0-zzz") == "ghcr.io/oca/oca-ci/py3.8-odoo15.0:latest" with pytest.raises(BranchNotSupported): diff --git a/tests/test_db.py b/tests/test_db.py index 941e82f..4d3cba7 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -29,14 +29,14 @@ def _make_build( ) -def test_add(): +def test_add() -> None: db = BuildsDb() assert db.add(_make_build()) # new assert not db.add(_make_build()) # no change assert db.add(_make_build(status=BuildStatus.failed)) -def test_remove(): +def test_remove() -> None: db = BuildsDb() assert not db.remove("not-a-build") build = _make_build() @@ -44,7 +44,7 @@ def test_remove(): assert db.remove(build.name) -def test_get_for_commit(): +def test_get_for_commit() -> None: db = BuildsDb() build = _make_build() db.add(build) @@ -62,7 +62,7 @@ def test_get_for_commit(): ) -def test_search(): +def test_search() -> None: db = BuildsDb() db.add(build1 := _make_build(name="b1", repo="oca/repo1")) db.add(_make_build(name="b2", repo="oca/repo2")) @@ -70,7 +70,7 @@ def test_search(): assert db.search("oca/repo1") == [build1] -def test_count_by_status(): +def test_count_by_status() -> None: db = BuildsDb() db.add(_make_build(name="b1", status=BuildStatus.started)) db.add(_make_build(name="b2", status=BuildStatus.stopped)) @@ -79,7 +79,7 @@ def test_count_by_status(): assert db.count_by_status(BuildStatus.failed) == 0 -def test_count_by_init_status(): +def test_count_by_init_status() -> None: db = BuildsDb() db.add(_make_build(name="b1", init_status=BuildInitStatus.started)) db.add(_make_build(name="b2", init_status=BuildInitStatus.todo)) @@ -88,7 +88,7 @@ def test_count_by_init_status(): assert db.count_by_init_status(BuildInitStatus.failed) == 0 -def test_count_all(): +def test_count_all() -> None: db = BuildsDb() assert db.count_all() == 0 db.add(_make_build(name="b1")) diff --git a/tests/test_k8s.py b/tests/test_k8s.py index 68be498..74af7d3 100644 --- a/tests/test_k8s.py +++ b/tests/test_k8s.py @@ -10,5 +10,5 @@ from runboat.k8s import _split_image_name_tag ("postgres:12", ("postgres", "12")), ], ) -def test_split_image_name_tag(image, expected): +def test_split_image_name_tag(image: str, expected: tuple[str, str]) -> None: assert _split_image_name_tag(image) == expected diff --git a/tests/test_render_kubefiles.py b/tests/test_render_kubefiles.py index a150afb..e703e8c 100644 --- a/tests/test_render_kubefiles.py +++ b/tests/test_render_kubefiles.py @@ -65,7 +65,7 @@ patches: """ -def test_render_kubefiles(): +def test_render_kubefiles() -> None: deployment_vars = make_deployment_vars( mode=DeploymentMode.deployment, build_name="build-name",