Merge pull request #14 from sbidoul/finish-typing

Finish-typing
This commit is contained in:
Stéphane Bidoul 2021-11-14 14:49:46 +01:00 committed by GitHub
commit 020c6609b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 78 additions and 46 deletions

View file

@ -16,15 +16,17 @@ jobs:
- name: Install project - name: Install project
run: | run: |
pip install -U "pip>=21.3.1" 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 - name: Run tests
run: | run: pytest -v --cov --cov-report=xml ./tests
pytest -v --cov --cov-report=xml ./tests - name: Run mypy
run: mypy ./src/runboat ./tests
- uses: codecov/codecov-action@v1 - uses: codecov/codecov-action@v1
build-image: build-image:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- test - test
if: ${{ github.repository_owner == 'sbidoul' && github.ref == 'refs/heads/main' }}
steps: steps:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1

View file

@ -39,4 +39,4 @@ repos:
rev: v2.29.0 rev: v2.29.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: ["--py38-plus"] args: ["--py39-plus"]

View file

@ -27,6 +27,9 @@ test = [
"pytest-cov", "pytest-cov",
"pytest-dotenv", "pytest-dotenv",
] ]
mypy = [
"mypy",
]
[project.urls] [project.urls]
Home = "https://github.com/sbidoul/runboat" Home = "https://github.com/sbidoul/runboat"
@ -39,3 +42,23 @@ env_override_existing_values = 1
env_files = [".env.test"] env_files = [".env.test"]
# flake8 config is in .flake8 # 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

4
requirements-mypy.txt Normal file
View file

@ -0,0 +1,4 @@
# frozen requirements generated by pip-deepfreeze
mypy==0.910
mypy-extensions==0.4.3
toml==0.10.2

View file

@ -1,14 +1,14 @@
import datetime import datetime
from typing import Optional from typing import Optional
from ansi2html import Ansi2HTMLConverter # type: ignore from ansi2html import Ansi2HTMLConverter
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from pydantic import BaseModel from pydantic import BaseModel
from starlette.status import HTTP_404_NOT_FOUND from starlette.status import HTTP_404_NOT_FOUND
from . import github, models from . import github, models
from .controller import controller from .controller import Controller, controller
from .deps import authenticated from .deps import authenticated
from .settings import settings from .settings import settings
@ -60,12 +60,12 @@ class Build(BaseModel):
@router.get("/status", response_model=Status) @router.get("/status", response_model=Status)
async def controller_status(): async def controller_status() -> Controller:
return controller return controller
@router.get("/repos", response_model=list[Repo]) @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] return [models.Repo(name=name) for name in settings.supported_repos]
@ -74,7 +74,7 @@ async def repos():
response_model=list[Build], response_model=list[Build],
response_model_exclude_none=True, 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) return controller.db.search(repo)
@ -82,7 +82,7 @@ async def builds(repo: Optional[str] = None):
"/builds/trigger/branch", "/builds/trigger/branch",
dependencies=[Depends(authenticated)], 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.""" """Trigger build for a branch."""
branch_info = await github.get_branch_info(repo, branch) branch_info = await github.get_branch_info(repo, branch)
await controller.deploy_or_start( await controller.deploy_or_start(
@ -97,7 +97,7 @@ async def trigger_branch(repo: str, branch: str):
"/builds/trigger/pr", "/builds/trigger/pr",
dependencies=[Depends(authenticated)], 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.""" """Trigger build for a pull request."""
pull_info = await github.get_pull_info(repo, pr) pull_info = await github.get_pull_info(repo, pr)
await controller.deploy_or_start( 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) @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) return await _build_by_name(name)
@ -124,42 +124,42 @@ async def build(name: str):
"/builds/{name}/init-log", "/builds/{name}/init-log",
response_class=HTMLResponse, response_class=HTMLResponse,
) )
async def init_log(name: str): async def init_log(name: str) -> str:
build = await _build_by_name(name) build = await _build_by_name(name)
log = await build.init_log() log = await build.init_log()
if not log: if not log:
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="No log found.") 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( @router.get(
"/builds/{name}/log", "/builds/{name}/log",
response_class=HTMLResponse, response_class=HTMLResponse,
) )
async def log(name: str): async def log(name: str) -> str:
build = await _build_by_name(name) build = await _build_by_name(name)
log = await build.log() log = await build.log()
if not log: if not log:
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="No log found.") 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") @router.post("/builds/{name}/start")
async def start(name: str): async def start(name: str) -> None:
"""Start the deployment.""" """Start the deployment."""
build = await _build_by_name(name) build = await _build_by_name(name)
await build.start() await build.start()
@router.post("/builds/{name}/stop") @router.post("/builds/{name}/stop")
async def stop(name: str): async def stop(name: str) -> None:
"""Stop the deployment.""" """Stop the deployment."""
build = await _build_by_name(name) build = await _build_by_name(name)
await build.stop() await build.stop()
@router.delete("/builds/{name}", dependencies=[Depends(authenticated)]) @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.""" """Delete the deployment and drop the database."""
build = await _build_by_name(name) build = await _build_by_name(name)
await build.undeploy() await build.undeploy()

View file

@ -30,7 +30,7 @@ class Controller:
""" """
db: BuildsDb db: BuildsDb
_tasks: list[asyncio.Task] _tasks: list[asyncio.Task[None]]
_wakeup_initializer: asyncio.Event _wakeup_initializer: asyncio.Event
_wakeup_stopper: asyncio.Event _wakeup_stopper: asyncio.Event
_wakeup_undeployer: asyncio.Event _wakeup_undeployer: asyncio.Event

View file

@ -11,12 +11,12 @@ from importlib import resources
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Generator, Optional, TypedDict, cast from typing import Any, Callable, Generator, Optional, TypedDict, cast
import urllib3 # type: ignore import urllib3
from jinja2 import Template from jinja2 import Template
from kubernetes import client, config, watch # type: ignore from kubernetes import client, config, watch
from kubernetes.client.exceptions import ApiException # type: ignore from kubernetes.client.exceptions import ApiException
from kubernetes.client.models.v1_deployment import V1Deployment # type: ignore from kubernetes.client.models.v1_deployment import V1Deployment
from kubernetes.client.models.v1_job import V1Job # type: ignore from kubernetes.client.models.v1_job import V1Job
from pydantic import BaseModel from pydantic import BaseModel
from .settings import settings from .settings import settings

View file

@ -4,7 +4,7 @@ import uuid
from enum import Enum from enum import Enum
from typing import Optional 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 pydantic import BaseModel
from . import github, k8s from . import github, k8s

View file

@ -1,4 +1,4 @@
from uvicorn.workers import UvicornWorker # type: ignore from uvicorn.workers import UvicornWorker
from .settings import settings from .settings import settings

View file

@ -1,7 +1,7 @@
from pathlib import Path from pathlib import Path
from typing import Optional 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.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
@ -13,12 +13,12 @@ templates = Jinja2Templates(directory=str(Path(__file__).parent / "webui"))
@router.get("/builds/{name}", response_class=HTMLResponse) @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) build = controller.db.get(name)
if not build: if not build:
raise HTTPException(status.HTTP_404_NOT_FOUND) raise HTTPException(status.HTTP_404_NOT_FOUND)
if live is not None and build.status == BuildStatus.started: if live is not None and build.status == BuildStatus.started:
return RedirectResponse(url=build.deploy_link) return RedirectResponse(url=build.deploy_link)
return templates.TemplateResponse( return templates.TemplateResponse(
"build.html", {"request": request, "build": build} "build.html.jinja", {"request": request, "build": build}
) )

View file

@ -18,15 +18,18 @@
<p>Branch: <a href="{{ build.repo_link }}">{{ build.target_branch }}</a></p> <p>Branch: <a href="{{ build.repo_link }}">{{ build.target_branch }}</a></p>
{% endif %} {% endif %}
<p>Commit: <a href="{{ build.repo_commit_link }}">{{ build.git_commit }}</a></p> <p>Commit: <a href="{{ build.repo_commit_link }}">{{ build.git_commit }}</a></p>
<p>Status: {{ build.status }}</p> <p>Status: {{ build.status.value }}</p>
<p> <p>
Logs: Logs:
<a href="/api/v1/builds/{{ build.name }}/init-log" target="_blank">build log</a> <a href="/api/v1/builds/{{ build.name }}/init-log" target="_blank">init log</a>
{% if build.status == 'started' %}
| |
<a href="/api/v1/builds/{{ build.name }}/log" target="_blank">run log</a> <a href="/api/v1/builds/{{ build.name }}/log" target="_blank">log</a>
|
<a href="{{ build.deploy_link }}">=> live</a>
{% endif %}
</p> </p>
{% if build.status == 'started' %} {% if build.status == 'started' %}
<p><a href="{{ build.deploy_link }}">=> live</a></p>
<button onclick="stop()">stop</button> <button onclick="stop()">stop</button>
{% else %} {% else %}
<button onclick="start()">start</button> <button onclick="start()">start</button>

View file

@ -4,20 +4,20 @@ from runboat.build_images import get_build_image, get_main_branch, is_branch_sup
from runboat.exceptions import BranchNotSupported 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") == "15.0"
assert get_main_branch("15.0-ocabot-merge") == "15.0" assert get_main_branch("15.0-ocabot-merge") == "15.0"
with pytest.raises(BranchNotSupported): with pytest.raises(BranchNotSupported):
get_main_branch("8.0") 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")
assert is_branch_supported("15.0-ocabot-merge") assert is_branch_supported("15.0-ocabot-merge")
assert not is_branch_supported("8.0") 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") == "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" assert get_build_image("15.0-zzz") == "ghcr.io/oca/oca-ci/py3.8-odoo15.0:latest"
with pytest.raises(BranchNotSupported): with pytest.raises(BranchNotSupported):

View file

@ -29,14 +29,14 @@ def _make_build(
) )
def test_add(): def test_add() -> None:
db = BuildsDb() db = BuildsDb()
assert db.add(_make_build()) # new assert db.add(_make_build()) # new
assert not db.add(_make_build()) # no change assert not db.add(_make_build()) # no change
assert db.add(_make_build(status=BuildStatus.failed)) assert db.add(_make_build(status=BuildStatus.failed))
def test_remove(): def test_remove() -> None:
db = BuildsDb() db = BuildsDb()
assert not db.remove("not-a-build") assert not db.remove("not-a-build")
build = _make_build() build = _make_build()
@ -44,7 +44,7 @@ def test_remove():
assert db.remove(build.name) assert db.remove(build.name)
def test_get_for_commit(): def test_get_for_commit() -> None:
db = BuildsDb() db = BuildsDb()
build = _make_build() build = _make_build()
db.add(build) db.add(build)
@ -62,7 +62,7 @@ def test_get_for_commit():
) )
def test_search(): def test_search() -> None:
db = BuildsDb() db = BuildsDb()
db.add(build1 := _make_build(name="b1", repo="oca/repo1")) db.add(build1 := _make_build(name="b1", repo="oca/repo1"))
db.add(_make_build(name="b2", repo="oca/repo2")) db.add(_make_build(name="b2", repo="oca/repo2"))
@ -70,7 +70,7 @@ def test_search():
assert db.search("oca/repo1") == [build1] assert db.search("oca/repo1") == [build1]
def test_count_by_status(): def test_count_by_status() -> None:
db = BuildsDb() db = BuildsDb()
db.add(_make_build(name="b1", status=BuildStatus.started)) db.add(_make_build(name="b1", status=BuildStatus.started))
db.add(_make_build(name="b2", status=BuildStatus.stopped)) 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 assert db.count_by_status(BuildStatus.failed) == 0
def test_count_by_init_status(): def test_count_by_init_status() -> None:
db = BuildsDb() db = BuildsDb()
db.add(_make_build(name="b1", init_status=BuildInitStatus.started)) db.add(_make_build(name="b1", init_status=BuildInitStatus.started))
db.add(_make_build(name="b2", init_status=BuildInitStatus.todo)) 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 assert db.count_by_init_status(BuildInitStatus.failed) == 0
def test_count_all(): def test_count_all() -> None:
db = BuildsDb() db = BuildsDb()
assert db.count_all() == 0 assert db.count_all() == 0
db.add(_make_build(name="b1")) db.add(_make_build(name="b1"))

View file

@ -10,5 +10,5 @@ from runboat.k8s import _split_image_name_tag
("postgres:12", ("postgres", "12")), ("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 assert _split_image_name_tag(image) == expected

View file

@ -65,7 +65,7 @@ patches:
""" """
def test_render_kubefiles(): def test_render_kubefiles() -> None:
deployment_vars = make_deployment_vars( deployment_vars = make_deployment_vars(
mode=DeploymentMode.deployment, mode=DeploymentMode.deployment,
build_name="build-name", build_name="build-name",