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
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

View file

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

View file

@ -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

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
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()

View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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}
)

View file

@ -18,15 +18,18 @@
<p>Branch: <a href="{{ build.repo_link }}">{{ build.target_branch }}</a></p>
{% endif %}
<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>
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>
{% if build.status == 'started' %}
<p><a href="{{ build.deploy_link }}">=> live</a></p>
<button onclick="stop()">stop</button>
{% else %}
<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
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):

View file

@ -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"))

View file

@ -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

View file

@ -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",