From f7011d26969f47617a15f44a0bbcdfcf118e748c Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Thu, 8 Feb 2024 15:51:04 +0100 Subject: [PATCH] churn: replace sqlalchemy with odmantic -> use mongodb instead of sqlite --- .pre-commit-config.yaml | 2 +- DEPENDENCIES | 12 +- docker/docker-compose.base.yml | 26 +- docker/docker-compose.dev.yml | 5 + docker/init_webapp.sh | 34 ++ otterdog/app.py | 1 - otterdog/config.py | 20 +- otterdog/providers/github/rest/__init__.py | 2 +- .../providers/github/rest/content_client.py | 2 +- otterdog/providers/github/rest/requester.py | 7 +- otterdog/webapp/__init__.py | 23 +- otterdog/webapp/config.py | 5 +- otterdog/webapp/db/__init__.py | 38 +- otterdog/webapp/db/models.py | 55 ++- otterdog/webapp/db/service.py | 181 +++++--- otterdog/webapp/home/routes.py | 21 +- otterdog/webapp/tasks/__init__.py | 22 +- otterdog/webapp/tasks/apply_changes.py | 36 +- otterdog/webapp/tasks/check_sync.py | 35 +- otterdog/webapp/tasks/help_comment.py | 6 +- .../webapp/tasks/retrieve_team_membership.py | 6 +- .../webapp/tasks/validate_pull_request.py | 35 +- .../webapp/templates/home/organizations.html | 3 +- otterdog/webapp/utils.py | 57 ++- otterdog/webapp/webhook/__init__.py | 30 +- otterdog/webapp/webhook/github_models.py | 8 +- poetry.lock | 387 +++++++++++------- pyproject.toml | 4 +- 28 files changed, 673 insertions(+), 390 deletions(-) create mode 100644 docker/init_webapp.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 76228bc1..7d4a8627 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: rev: v1.8.0 hooks: - id: mypy - additional_dependencies: [types-requests] + additional_dependencies: [types-requests, types-aiofiles] - repo: https://github.com/shellcheck-py/shellcheck-py rev: v0.9.0.6 hooks: diff --git a/DEPENDENCIES b/DEPENDENCIES index 179af15f..b42bbf42 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -16,10 +16,10 @@ pypi/pypi/-/chevron/0.14.0 pypi/pypi/-/click/8.1.7 pypi/pypi/-/colorama/0.4.6 pypi/pypi/-/cryptography/42.0.2 +pypi/pypi/-/dnspython/2.5.0 pypi/pypi/-/exceptiongroup/1.2.0 pypi/pypi/-/flask/3.0.2 pypi/pypi/-/flask-minify/0.42 -pypi/pypi/-/flask-sqlalchemy/3.1.1 pypi/pypi/-/frozenlist/1.4.1 pypi/pypi/-/ghp-import/2.1.0 pypi/pypi/-/gojsonnet/0.20.0 @@ -47,14 +47,16 @@ pypi/pypi/-/mergedeep/1.3.4 pypi/pypi/-/mintotp/0.3.0 pypi/pypi/-/mkdocs/1.5.3 pypi/pypi/-/mkdocs-exclude/1.0.2 -pypi/pypi/-/mkdocs-material/9.5.7 +pypi/pypi/-/mkdocs-material/9.5.8 pypi/pypi/-/mkdocs-material-extensions/1.3.1 +pypi/pypi/-/motor/3.3.2 pypi/pypi/-/multidict/6.0.5 +pypi/pypi/-/odmantic/1.0.0 pypi/pypi/-/packaging/23.2 pypi/pypi/-/paginate/0.5.6 pypi/pypi/-/pathspec/0.12.1 pypi/pypi/-/platformdirs/4.2.0 -pypi/pypi/-/playwright/1.41.1 +pypi/pypi/-/playwright/1.41.2 pypi/pypi/-/ply/3.11 pypi/pypi/-/priority/2.0.0 pypi/pypi/-/pycparser/2.21 @@ -63,6 +65,7 @@ pypi/pypi/-/pydantic-core/2.16.2 pypi/pypi/-/pyee/11.0.1 pypi/pypi/-/pygments/2.17.2 pypi/pypi/-/pymdown-extensions/10.7 +pypi/pypi/-/pymongo/4.6.1 pypi/pypi/-/pynacl/1.5.0 pypi/pypi/-/python-dateutil/2.8.2 pypi/pypi/-/python-decouple/3.8 @@ -81,13 +84,12 @@ pypi/pypi/-/requests/2.31.0 pypi/pypi/-/requests-cache/1.1.1 pypi/pypi/-/rpds-py/0.17.1 pypi/pypi/-/six/1.16.0 -pypi/pypi/-/sqlalchemy/2.0.25 pypi/pypi/-/taskgroup/0.0.0a4 pypi/pypi/-/tomli/2.0.1 pypi/pypi/-/typing-extensions/4.9.0 pypi/pypi/-/url-normalize/1.4.3 pypi/pypi/-/urllib3/2.2.0 -pypi/pypi/-/watchdog/3.0.0 +pypi/pypi/-/watchdog/4.0.0 pypi/pypi/-/werkzeug/3.0.1 pypi/pypi/-/wsproto/1.2.0 pypi/pypi/-/wtforms/3.1.2 diff --git a/docker/docker-compose.base.yml b/docker/docker-compose.base.yml index 14322f0c..92be476e 100644 --- a/docker/docker-compose.base.yml +++ b/docker/docker-compose.base.yml @@ -3,6 +3,8 @@ version: '3.8' services: webapp: restart: always + depends_on: + - mongodb env_file: ../.env build: context: .. @@ -13,13 +15,17 @@ services: volumes: - ../github-app:/app/work/config -# nginx: -# container_name: nginx -# restart: always -# image: "nginx:latest" -# ports: -# - "80:80" -# volumes: -# - ./nginx:/etc/nginx/conf.d -# depends_on: -# - webapp + init-webapp: + image: curlimages/curl:8.2.1 + command: sh /init_webapp.sh webapp 5000 + volumes: + - ./init_webapp.sh:/init_webapp.sh + depends_on: + webapp: + condition: service_started + + mongodb: + image: mongo:7.0.5 + command: mongod --quiet --logpath /dev/null + ports: + - 27017:27017 diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 77088e66..76d47a80 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -3,5 +3,10 @@ version: '3.8' services: webapp: volumes: + - ../docker/hypercorn-cfg.toml:/app/hypercorn-cfg.toml - ../otterdog:/app/otterdog - ../approot:/app/work + + mongodb: + volumes: + - ../approot/db:/data/db diff --git a/docker/init_webapp.sh b/docker/init_webapp.sh new file mode 100644 index 00000000..6caa5557 --- /dev/null +++ b/docker/init_webapp.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# +# ******************************************************************************* +# Copyright (c) 2024 Eclipse Foundation and others. +# This program and the accompanying materials are made available +# under the terms of the Eclipse Public License 2.0 +# which is available at http://www.eclipse.org/legal/epl-v20.html +# SPDX-License-Identifier: EPL-2.0 +# ******************************************************************************* +# + +# exits if any of your variables is not set +set -o nounset + +HOST="${1:-webapp}" +PORT="${2:-5000}" +DELAY_BETWEEN_ATTEMPTS=1 + +HEAL_URL="http://${HOST}:${PORT}/health" +INIT_URL="http://${HOST}:${PORT}/init" + +while true; do + echo "Checking health url ${HEAL_URL}..." + + status_code=$(curl -s -I "${HEAL_URL}" -o /dev/null -w "%{http_code}") + if [ "${status_code}" == "200" ]; then + echo "Triggering init via ${INIT_URL}..." + curl -s "${INIT_URL}" + exit 0 + fi + + sleep "${DELAY_BETWEEN_ATTEMPTS}" +done diff --git a/otterdog/app.py b/otterdog/app.py index c73f2150..2afcefa6 100644 --- a/otterdog/app.py +++ b/otterdog/app.py @@ -57,7 +57,6 @@ app.logger.info("Environment = " + config_mode) app.logger.info("QUART_APP = " + app_config.QUART_APP) app.logger.info("APP_ROOT = " + app_config.APP_ROOT) - app.logger.info("DB = " + app_config.SQLALCHEMY_DATABASE_URI) def run(): diff --git a/otterdog/config.py b/otterdog/config.py index cf4fe8ce..ac4d1a5f 100644 --- a/otterdog/config.py +++ b/otterdog/config.py @@ -105,28 +105,16 @@ def of( cls, project_name: str, github_id: str, + config_repo: str, + base_template: str, credential_data: dict[str, Any], work_dir: str, - otterdog_config: OtterdogConfig, ) -> OrganizationConfig: - # get the default organization config to retrieve some properties - try: - org_config = otterdog_config.get_organization_config(project_name) - config_repo = org_config.config_repo - base_template = org_config.base_template - except RuntimeError: - # if no organization config can be found for the project, assume the defaults - config_repo = otterdog_config.default_config_repo - base_template = otterdog_config.default_base_template - - # construct a custom jsonnet config based on the given work_dir - base_dir = os.path.join(otterdog_config.jsonnet_base_dir, work_dir) - jsonnet_config = JsonnetConfig( github_id, - base_dir, + work_dir, base_template, - otterdog_config.local_mode, + False, ) return cls(project_name, github_id, config_repo, base_template, jsonnet_config, credential_data) diff --git a/otterdog/providers/github/rest/__init__.py b/otterdog/providers/github/rest/__init__.py index 879c16bb..04298d13 100644 --- a/otterdog/providers/github/rest/__init__.py +++ b/otterdog/providers/github/rest/__init__.py @@ -23,7 +23,7 @@ class RestApi: _GH_API_VERSION = "2022-11-28" _GH_API_URL_ROOT = "https://api.github.com" - def __init__(self, auth_strategy: AuthStrategy): + def __init__(self, auth_strategy: Optional[AuthStrategy] = None): self._auth_strategy = auth_strategy self._requester = Requester(auth_strategy, self._GH_API_URL_ROOT, self._GH_API_VERSION) diff --git a/otterdog/providers/github/rest/content_client.py b/otterdog/providers/github/rest/content_client.py index 3913c3e1..ce046c91 100644 --- a/otterdog/providers/github/rest/content_client.py +++ b/otterdog/providers/github/rest/content_client.py @@ -37,7 +37,7 @@ async def get_content_object( tb = ex.__traceback__ raise RuntimeError(f"failed retrieving content '{path}' from repo '{repo_name}':\n{ex}").with_traceback(tb) - async def get_content(self, org_id: str, repo_name: str, path: str, ref: Optional[str]) -> str: + async def get_content(self, org_id: str, repo_name: str, path: str, ref: Optional[str] = None) -> str: json_response = await self.get_content_object(org_id, repo_name, path, ref) return base64.b64decode(json_response["content"]).decode("utf-8") diff --git a/otterdog/providers/github/rest/requester.py b/otterdog/providers/github/rest/requester.py index 0fc04bab..8dd8092f 100644 --- a/otterdog/providers/github/rest/requester.py +++ b/otterdog/providers/github/rest/requester.py @@ -23,9 +23,9 @@ class Requester: - def __init__(self, auth_strategy: AuthStrategy, base_url: str, api_version: str): + def __init__(self, auth_strategy: Optional[AuthStrategy], base_url: str, api_version: str): self._base_url = base_url - self._auth = auth_strategy.get_auth() + self._auth = auth_strategy.get_auth() if auth_strategy is not None else None self._headers = { "Accept": "application/vnd.github+json", @@ -176,7 +176,8 @@ async def async_request_raw( print_trace(f"async '{method}' url = {url_path}, data = {data}, headers = {self._headers}") headers = self._headers.copy() - self._auth.update_headers_with_authorization(headers) + if self._auth is not None: + self._auth.update_headers_with_authorization(headers) async with AsyncCachedSession(cache=FileBackend(cache_name=_AIOHTTP_CACHE_DIR, use_temp=False)) as session: url = self._build_url(url_path) diff --git a/otterdog/webapp/__init__.py b/otterdog/webapp/__init__.py index 86a4106a..07df6c4b 100644 --- a/otterdog/webapp/__init__.py +++ b/otterdog/webapp/__init__.py @@ -10,21 +10,20 @@ from importlib.util import find_spec import quart_flask_patch # type: ignore # noqa: F401 -from flask_sqlalchemy import SQLAlchemy from quart import Quart from quart_auth import QuartAuth from .config import AppConfig -from .db import Base +from .db import Mongo, init_mongo_database _BLUEPRINT_MODULES: list[str] = ["home"] -db = SQLAlchemy(model_class=Base) +mongo = Mongo() auth_manager = QuartAuth() def register_extensions(app): - db.init_app(app) + mongo.init_app(app) auth_manager.init_app(app) @@ -47,21 +46,9 @@ def register_blueprints(app): def configure_database(app): @app.before_serving - async def create_tables(): + async def configure(): async with app.app_context(): - models_fqn = "otterdog.webapp.db.models" - import_module(models_fqn) - db.create_all() - - @app.before_serving - async def fill_database(): - from otterdog.webapp.db.service import fill_organization_table - - await fill_organization_table(app) - - @app.teardown_request - def shutdown_session(exception=None): - db.session.remove() + await init_mongo_database(mongo) def create_app(app_config: AppConfig): diff --git a/otterdog/webapp/config.py b/otterdog/webapp/config.py index 94fbd3ad..6d34c1e3 100644 --- a/otterdog/webapp/config.py +++ b/otterdog/webapp/config.py @@ -28,12 +28,11 @@ class AppConfig(object): if not os.path.exists(DB_ROOT): os.makedirs(DB_ROOT) - SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(DB_ROOT, "otterdog-db.sqlite3") - SQLALCHEMY_TRACK_MODIFICATIONS = False + MONGO_URI = config("MONGO_URI", default="mongodb://mongodb:27017/otterdog") - OTTERDOG_CONFIG_URL = config("OTTERDOG_CONFIG_URL", default=None) OTTERDOG_CONFIG_OWNER = config("OTTERDOG_CONFIG_OWNER", default=None) OTTERDOG_CONFIG_REPO = config("OTTERDOG_CONFIG_REPO", default=None) + OTTERDOG_CONFIG_PATH = config("OTTERDOG_CONFIG_PATH", default=None) # Set up the App SECRET_KEY SECRET_KEY = config("SECRET_KEY", default=None) diff --git a/otterdog/webapp/db/__init__.py b/otterdog/webapp/db/__init__.py index 2104cd7b..55eaa839 100644 --- a/otterdog/webapp/db/__init__.py +++ b/otterdog/webapp/db/__init__.py @@ -6,8 +6,40 @@ # SPDX-License-Identifier: EPL-2.0 # ******************************************************************************* -from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass +import re +from typing import Optional +from motor.motor_asyncio import AsyncIOMotorClient +from odmantic import AIOEngine +from quart import Quart -class Base(MappedAsDataclass, DeclarativeBase): - pass + +class Mongo: + def __init__(self) -> None: + self._client: Optional[AsyncIOMotorClient] = None + self._engine: Optional[AIOEngine] = None + + def init_app(self, app: Quart) -> None: + connection_uri = app.config["MONGO_URI"] + + m = re.match(r"^((mongodb:(?:/{2})?)((\w+?):(\w+?)@|:?@?)(\w+?):(\d+)/)(\w+?)$", connection_uri) + + if m is not None: + server_uri = m.group(1) + database = m.group(8) + else: + raise RuntimeError(f"failed to parse mongo connection uri: '{connection_uri}'") + + self._client = AsyncIOMotorClient(server_uri) + self._engine = AIOEngine(client=self._client, database=database) + + @property + def odm(self) -> AIOEngine: + assert self._engine is not None + return self._engine + + +async def init_mongo_database(mongo: Mongo) -> None: + from .models import OrganizationConfigModel, TaskModel + + await mongo.odm.configure_database([OrganizationConfigModel, TaskModel]) # type: ignore diff --git a/otterdog/webapp/db/models.py b/otterdog/webapp/db/models.py index f28d4808..bf341c86 100644 --- a/otterdog/webapp/db/models.py +++ b/otterdog/webapp/db/models.py @@ -6,43 +6,38 @@ # SPDX-License-Identifier: EPL-2.0 # ******************************************************************************* -import uuid +from __future__ import annotations + from datetime import datetime +from enum import Enum from typing import Optional -from sqlalchemy import String, func -from sqlalchemy.orm import Mapped, mapped_column - -from . import Base - +from odmantic import Field, Model -class Organization(Base): # type: ignore - __tablename__ = "Organization" - installation_id: Mapped[int] = mapped_column(primary_key=True) - github_id: Mapped[str] = mapped_column(String(255), unique=True) - project_name: Mapped[Optional[str]] = mapped_column(String(255), unique=True, default=None) +class InstallationStatus(str, Enum): + installed = "installed" + not_installed = "not_installed" + suspended = "suspended" - def __repr__(self) -> str: - return ( - f"Organization(installation_id={self.installation_id!r}, project_name={self.project_name!r}, " - f"github_id={self.github_id!r})" - ) + def __str__(self) -> str: + return self.name -class DBTask(Base): - __tablename__ = "Task" +class OrganizationConfigModel(Model): + github_id: str = Field(primary_field=True) + project_name: Optional[str] = Field(unique=True, index=True) + installation_id: int = Field(index=True, default=0) + installation_status: InstallationStatus + config_repo: Optional[str] = None + base_template: Optional[str] = None - id: Mapped[uuid.UUID] = mapped_column(primary_key=True, server_default=func.gen_random_uuid()) - type: Mapped[str] = mapped_column(String(255)) - org_id: Mapped[str] = mapped_column(String(255)) - repo_name: Mapped[str] = mapped_column(String(255)) - pull_request: Mapped[int] = mapped_column() - status: Mapped[str] = mapped_column(String(255)) - created_at: Mapped[datetime] = mapped_column(server_default=func.now()) - updated_at: Mapped[Optional[datetime]] = mapped_column(server_default=func.now(), onupdate=func.now()) - def __init__(self, **kwargs): - if "id" not in kwargs: - kwargs["id"] = uuid.uuid4() - super().__init__(**kwargs) +class TaskModel(Model): + type: str + org_id: str + repo_name: str + pull_request: int + status: str + created_at: datetime = datetime.utcnow() + updated_at: datetime = datetime.utcnow() diff --git a/otterdog/webapp/db/service.py b/otterdog/webapp/db/service.py index 8996f50a..7ac9a866 100644 --- a/otterdog/webapp/db/service.py +++ b/otterdog/webapp/db/service.py @@ -8,71 +8,144 @@ from __future__ import annotations -from typing import Sequence, cast +from logging import getLogger +from typing import Optional -from quart import Quart -from sqlalchemy import desc, func, select +from odmantic import query -from otterdog.webapp import db +from otterdog.webapp import mongo from otterdog.webapp.utils import get_otterdog_config, get_rest_api_for_app -from .models import DBTask, Organization +from .models import InstallationStatus, OrganizationConfigModel, TaskModel +logger = getLogger(__name__) -async def fill_organization_table(app: Quart): - async with app.app_context(): - app.logger.info("filling database with app installations") - rest_api = get_rest_api_for_app() - otterdog_config = get_otterdog_config() - for app_installation in await rest_api.app.get_app_installations(): + +async def get_organization_config_by_installation_id(installation_id: int) -> Optional[OrganizationConfigModel]: + config = await mongo.odm.find_one( + OrganizationConfigModel, OrganizationConfigModel.installation_id == installation_id + ) + + if config is None: + logger.error(f"did not find an OrganizationConfig document for installation_id '{installation_id}'") + + return config + + +async def update_installation_status(installation_id: int, action: str) -> None: + logger.info(f"updating installation status for installation with id '{installation_id}': {action}") + + match action: + case "created": + await update_organization_configs() + + case "deleted": + await update_organization_configs() + + case "suspend": + installation = await mongo.odm.find_one( + OrganizationConfigModel, OrganizationConfigModel.installation_id == installation_id + ) + + if installation is not None: + installation.installation_status = InstallationStatus.suspended + await mongo.odm.save(installation) + + case "unsuspend": + installation = await mongo.odm.find_one( + OrganizationConfigModel, OrganizationConfigModel.installation_id == installation_id + ) + + if installation is not None: + installation.installation_status = InstallationStatus.installed + await mongo.odm.save(installation) + + case _: + pass + + +async def update_organization_configs() -> None: + logger.info("updating all organization configs") + + rest_api = get_rest_api_for_app() + otterdog_config = await get_otterdog_config() + all_configured_organization_names: set[str] = set(otterdog_config.organization_names) + all_installations = await rest_api.app.get_app_installations() + + async with mongo.odm.session() as session: + existing_organizations: set[str] = set() + async for org in session.find(OrganizationConfigModel): + existing_organizations.add(org.github_id) + + for app_installation in all_installations: installation_id = app_installation["id"] github_id = app_installation["account"]["login"] - project_name = otterdog_config.get_project_name(github_id) or github_id - - organization = db.session.get(Organization, installation_id) # type: ignore - if organization: - organization.project_name = project_name + project_name = otterdog_config.get_project_name(github_id) + suspended_at = app_installation["suspended_at"] + installation_status = InstallationStatus.installed if suspended_at is None else InstallationStatus.suspended + + if project_name is not None: + org_config = otterdog_config.get_organization_config(project_name) + config_repo = org_config.config_repo + base_template = org_config.base_template + all_configured_organization_names.remove(project_name) + else: + project_name = None + config_repo = None + base_template = None + + model = OrganizationConfigModel( # type: ignore + installation_id=installation_id, + installation_status=installation_status, + project_name=project_name, + github_id=github_id, + config_repo=config_repo, + base_template=base_template, + ) + + if github_id in existing_organizations: + existing_organizations.remove(github_id) + + await session.save(model) + + # process organizations that have the GitHub App not installed + for github_id in existing_organizations: + project_name = otterdog_config.get_project_name(github_id) + if project_name is None: + await session.remove(OrganizationConfigModel, OrganizationConfigModel.github_id == github_id) else: - organization = Organization( - installation_id=installation_id, github_id=github_id, project_name=project_name + existing_model = await mongo.odm.find_one( + OrganizationConfigModel, OrganizationConfigModel.github_id == github_id ) - db.session.add(organization) # type: ignore - - db.session.commit() # type: ignore - - -def get_or_create(session, model, defaults=None, **kwargs): - instance = session.query(model).filter_by(**kwargs).one_or_none() - if instance: - return instance, False - else: - kwargs |= defaults or {} - instance = model(**kwargs) - try: - session.add(instance) - session.commit() - except Exception: - # The actual exception depends on the specific database, so we catch all exceptions. - # This is similar to the official documentation: - # https://docs.sqlalchemy.org/en/latest/orm/session_transaction.html - session.rollback() - instance = session.query(model).filter_by(**kwargs).one() - return instance, False - else: - return instance, True - - -def get_organization_count() -> int: - return cast( - int, db.session.execute(db.session.query(func.count(Organization.installation_id))).scalar() # type: ignore - ) + if existing_model is not None: + existing_model.project_name = project_name + existing_model.installation_status = InstallationStatus.not_installed + await mongo.odm.save(existing_model) + + # finally add all organizations that are in the config but have the app not installed yet + for name in all_configured_organization_names: + config = otterdog_config.get_organization_config(name) + + if config is not None: + model = OrganizationConfigModel( # type: ignore + installation_status=InstallationStatus.not_installed, + project_name=config.name, + github_id=config.github_id, + config_repo=config.config_repo, + base_template=config.base_template, + ) -def get_organizations() -> Sequence[Organization]: - return db.session.execute(select(Organization)).scalars().all() # type: ignore + await mongo.odm.save(model) -def get_tasks(limit: int) -> Sequence[DBTask]: - return ( - db.session.execute(select(DBTask).order_by(desc(DBTask.created_at))).scalars().fetchmany(limit) # type: ignore - ) +async def get_organization_count() -> int: + return await mongo.odm.count(OrganizationConfigModel) + + +async def get_organizations() -> list[OrganizationConfigModel]: + return await mongo.odm.find(OrganizationConfigModel) + + +async def get_tasks(limit: int) -> list[TaskModel]: + return await mongo.odm.find(TaskModel, limit=limit, sort=query.desc(TaskModel.created_at)) diff --git a/otterdog/webapp/home/routes.py b/otterdog/webapp/home/routes.py index 3eee8420..5fee82f9 100644 --- a/otterdog/webapp/home/routes.py +++ b/otterdog/webapp/home/routes.py @@ -7,7 +7,7 @@ # ******************************************************************************* from jinja2 import TemplateNotFound -from quart import redirect, render_template, request, url_for +from quart import make_response, redirect, render_template, request, url_for from otterdog.webapp.db.service import ( get_organization_count, @@ -25,22 +25,35 @@ def route_default(): @blueprint.route("/index.html") async def index(): - org_count = get_organization_count() + org_count = await get_organization_count() return await render_template("home/index.html", segment="index", org_count=org_count) @blueprint.route("/organizations.html") async def organizations(): - orgs = get_organizations() + orgs = await get_organizations() return await render_template("home/organizations.html", segment="organizations", organizations=orgs) @blueprint.route("/tasks.html") async def tasks(): - latest_tasks = get_tasks(100) + latest_tasks = await get_tasks(100) return await render_template("home/tasks.html", segment="tasks", tasks=latest_tasks) +@blueprint.route("/health") +async def health(): + return await make_response({}, 200) + + +@blueprint.route("/init") +async def init(): + from otterdog.webapp.db.service import update_organization_configs + + await update_organization_configs() + return await make_response({}, 200) + + @blueprint.route("/