Skip to content

Commit

Permalink
[api-server] fix typings, update dependencies (#886)
Browse files Browse the repository at this point in the history
* upate deps

Signed-off-by: Teo Koon Peng <koonpeng@openrobotics.org>

* make pylance happy

Signed-off-by: Teo Koon Peng <koonpeng@openrobotics.org>

* tests finally passing

Signed-off-by: Teo Koon Peng <koonpeng@openrobotics.org>

* make pylint happy

Signed-off-by: Teo Koon Peng <koonpeng@openrobotics.org>

* fix imports

Signed-off-by: Teo Koon Peng <koonpeng@openrobotics.org>

* fix imports

Signed-off-by: Teo Koon Peng <koonpeng@openrobotics.org>

* ignore pyright false positive

Signed-off-by: Teo Koon Peng <koonpeng@openrobotics.org>

* fix stub authenticator

Signed-off-by: Teo Koon Peng <koonpeng@openrobotics.org>

* fix health not being written to db

Signed-off-by: Teo Koon Peng <koonpeng@openrobotics.org>

---------

Signed-off-by: Teo Koon Peng <koonpeng@openrobotics.org>
  • Loading branch information
Teo Koon Peng committed Jan 30, 2024
1 parent 5840346 commit 9567672
Show file tree
Hide file tree
Showing 62 changed files with 1,568 additions and 1,162 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pylint = "==2.10.2"
coverage = "~=5.5"
# api-server
api-server = {editable = true, path = "./packages/api-server"}
httpx = "~=0.26.0"
asyncpg = "~=0.25.0"
datamodel-code-generator = "==0.11.19"
requests = "~=2.25"
Expand Down
1,512 changes: 833 additions & 679 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"husky": "^8.0.1",
"lint-staged": "^10.5.4",
"prettier": "^2.7.1",
"pyright": "^1.1.257",
"pyright": "1.1.257",
"typescript": "~4.4.4"
},
"lint-staged": {
Expand Down
16 changes: 9 additions & 7 deletions packages/api-server/api_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
import signal
import threading
from typing import Any, Callable, Coroutine, List, Optional, Union
from typing import Any, Callable, Coroutine, Union

import schedule
from fastapi import Depends
Expand Down Expand Up @@ -37,7 +37,7 @@
from .types import is_coroutine


async def on_sio_connect(sid: str, _environ: dict, auth: Optional[dict] = None):
async def on_sio_connect(sid: str, _environ: dict, auth: dict | None = None) -> bool:
session = await app.sio.get_session(sid)
token = None
if auth:
Expand Down Expand Up @@ -83,7 +83,7 @@ async def on_sio_connect(sid: str, _environ: dict, auth: Optional[dict] = None):
)

# will be called in reverse order on app shutdown
shutdown_cbs: List[Union[Coroutine[Any, Any, Any], Callable[[], None]]] = []
shutdown_cbs: list[Union[Coroutine[Any, Any, Any], Callable[[], None]]] = []

rmf_bookkeeper = RmfBookKeeper(rmf_events, logger=logger.getChild("BookKeeper"))

Expand Down Expand Up @@ -257,7 +257,7 @@ async def _load_states():
logger.info(f"loaded {len(door_states)} door states")

door_health = [
await DoorHealth.from_tortoise(x) for x in await ttm.DoorHealth.all()
await DoorHealth.from_tortoise_orm(x) for x in await ttm.DoorHealth.all()
]
for health in door_health:
rmf_events.door_health.on_next(health)
Expand All @@ -269,7 +269,7 @@ async def _load_states():
logger.info(f"loaded {len(lift_states)} lift states")

lift_health = [
await LiftHealth.from_tortoise(x) for x in await ttm.LiftHealth.all()
await LiftHealth.from_tortoise_orm(x) for x in await ttm.LiftHealth.all()
]
for health in lift_health:
rmf_events.lift_health.on_next(health)
Expand All @@ -283,7 +283,8 @@ async def _load_states():
logger.info(f"loaded {len(dispenser_states)} dispenser states")

dispenser_health = [
await DispenserHealth.from_tortoise(x) for x in await ttm.DispenserHealth.all()
await DispenserHealth.from_tortoise_orm(x)
for x in await ttm.DispenserHealth.all()
]
for health in dispenser_health:
rmf_events.dispenser_health.on_next(health)
Expand All @@ -297,7 +298,8 @@ async def _load_states():
logger.info(f"loaded {len(ingestor_states)} ingestor states")

ingestor_health = [
await IngestorHealth.from_tortoise(x) for x in await ttm.IngestorHealth.all()
await IngestorHealth.from_tortoise_orm(x)
for x in await ttm.IngestorHealth.all()
]
for health in ingestor_health:
rmf_events.ingestor_health.on_next(health)
Expand Down
4 changes: 2 additions & 2 deletions packages/api-server/api_server/app_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import urllib.parse
from dataclasses import dataclass
from importlib.abc import Loader
from typing import Any, List, Optional, cast
from typing import List, Optional, cast


@dataclass
Expand Down Expand Up @@ -47,7 +47,7 @@ def load_config(config_file: str) -> AppConfig:
raise RuntimeError("unable to load module")
sys.path.append(os.path.dirname(config_file))
loader.exec_module(module)
config = AppConfig(**cast(Any, module).config)
config = AppConfig(**module.config)
if "RMF_API_SERVER_LOG_LEVEL" in os.environ:
config.log_level = os.environ["RMF_API_SERVER_LOG_LEVEL"]
return config
Expand Down
47 changes: 37 additions & 10 deletions packages/api-server/api_server/authenticator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from typing import Any, Callable, Coroutine, Optional, Union
import base64
import json
from typing import Any, Callable, Coroutine, Optional, Protocol, Union

import jwt
from fastapi import Depends, HTTPException
from fastapi import Depends, Header, HTTPException
from fastapi.security import OpenIdConnect

from .app_config import app_config
Expand All @@ -13,13 +15,22 @@ class AuthenticationError(Exception):
pass


class JwtAuthenticator:
class Authenticator(Protocol):
async def verify_token(self, token: Optional[str]) -> User:
...

def fastapi_dep(self) -> Callable[..., Union[Coroutine[Any, Any, User], User]]:
...


class JwtAuthenticator(Authenticator):
def __init__(self, pem_file: str, aud: str, iss: str, *, oidc_url: str = ""):
"""
Authenticates with a JWT token, the client must send an auth params with
a "token" key.
:param pem_file: path to a pem encoded certificate used to verify a token.
"""
super().__init__()
self.aud = aud
self.iss = iss
self.oidc_url = oidc_url
Expand Down Expand Up @@ -65,15 +76,31 @@ async def dep(
return dep


class StubAuthenticator(JwtAuthenticator):
def __init__(self): # pylint: disable=super-init-not-called
self._user = User(username="stub", is_admin=True)
class StubAuthenticator(Authenticator):
"""
StubAuthenticator will authenticate as an admin user called "stub" if no tokens are
present. If there is a bearer token in the `Authorization` header, then it decodes the jwt
WITHOUT verifying the signature and authenticated as the user given.
"""

async def verify_token(self, token: Optional[str]) -> User:
return self._user
async def verify_token(self, token: Optional[str]):
if not token:
return User(username="stub", is_admin=True)
# decode the jwt without verifying signature
parts = token.split(".")
# add padding to ignore incorrect padding errors
payload = base64.b64decode(parts[1] + "==")
username = json.loads(payload)["preferred_username"]
return await User.load_or_create_from_db(username)

def fastapi_dep(self) -> Callable[..., Union[Coroutine[Any, Any, User], User]]:
return lambda: self._user
def fastapi_dep(self):
async def dep(authorization: str | None = Header(None)):
if not authorization:
return await self.verify_token(None)
token = authorization.split(" ")[1]
return await self.verify_token(token)

return dep


if app_config.jwt_public_key:
Expand Down
2 changes: 1 addition & 1 deletion packages/api-server/api_server/fast_io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from fastapi import APIRouter, FastAPI
from fastapi.exceptions import HTTPException
from fastapi.routing import APIRoute
from rx.core.observable.observable import Observable
from reactivex import Observable
from starlette.routing import compile_path

from api_server.logger import logger
Expand Down
1 change: 1 addition & 0 deletions packages/api-server/api_server/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .building_map import *
from .dispensers import *
from .doors import *
from .fleets import *
from .health import *
from .ingestors import *
from .lifts import *
Expand Down
6 changes: 2 additions & 4 deletions packages/api-server/api_server/models/building_map.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import List

from . import tortoise_models as ttm
from .ros_pydantic import rmf_building_map_msgs

Expand All @@ -9,11 +7,11 @@ class AffineImage(rmf_building_map_msgs.AffineImage):


class Level(rmf_building_map_msgs.Level):
images: List[AffineImage]
images: list[AffineImage]


class BuildingMap(rmf_building_map_msgs.BuildingMap):
levels: List[Level]
levels: list[Level]

@staticmethod
def from_tortoise(tortoise: ttm.BuildingMap) -> "BuildingMap":
Expand Down
12 changes: 10 additions & 2 deletions packages/api-server/api_server/models/dispensers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
from pydantic import BaseModel

from . import tortoise_models as ttm
from .health import basic_health_model
from .health import BasicHealth, HealthStatus
from .ros_pydantic import rmf_dispenser_msgs

DispenserHealth = basic_health_model(ttm.DispenserHealth)

class DispenserHealth(BasicHealth):
@classmethod
async def from_tortoise_orm(cls, obj: ttm.DispenserHealth) -> "DispenserHealth":
return DispenserHealth(
id_=obj.id_,
health_status=HealthStatus(obj.health_status),
health_message=obj.health_message,
)


class Dispenser(BaseModel):
Expand Down
13 changes: 11 additions & 2 deletions packages/api-server/api_server/models/doors.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
from pydantic import BaseModel, Field

from . import tortoise_models as ttm
from .health import basic_health_model
from .health import BasicHealth, HealthStatus
from .ros_pydantic import rmf_building_map_msgs, rmf_door_msgs

Door = rmf_building_map_msgs.Door
DoorMode = rmf_door_msgs.DoorMode
DoorHealth = basic_health_model(ttm.DoorHealth)


class DoorHealth(BasicHealth):
@classmethod
async def from_tortoise_orm(cls, obj: ttm.DoorHealth) -> "DoorHealth":
return DoorHealth(
id_=obj.id_,
health_status=HealthStatus(obj.health_status),
health_message=obj.health_message,
)


class DoorState(rmf_door_msgs.DoorState):
Expand Down
12 changes: 12 additions & 0 deletions packages/api-server/api_server/models/fleets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from . import tortoise_models as ttm
from .health import BasicHealth, HealthStatus


class RobotHealth(BasicHealth):
@classmethod
async def from_tortoise_orm(cls, obj: ttm.RobotHealth) -> "RobotHealth":
return RobotHealth(
id_=obj.id_,
health_status=HealthStatus(obj.health_status),
health_message=obj.health_message,
)
54 changes: 6 additions & 48 deletions packages/api-server/api_server/models/health.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,15 @@
from abc import ABC, abstractmethod
from typing import Generic, Optional, Type, TypeVar, cast
from enum import Enum

from tortoise.contrib.pydantic.base import PydanticModel
from tortoise.contrib.pydantic.creator import pydantic_model_creator
from pydantic import BaseModel

from . import tortoise_models as ttm


class HealthStatus:
class HealthStatus(str, Enum):
HEALTHY = "Healthy"
UNHEALTHY = "Unhealthy"
DEAD = "Dead"


HealthModelT = TypeVar("HealthModelT", bound=ttm.BasicHealthModel)


class BaseBasicHealth(Generic[HealthModelT], ABC, PydanticModel):
class BasicHealth(BaseModel):
id_: str
health_status: str
health_message: Optional[str]

@staticmethod
@abstractmethod
async def from_tortoise(_tortoise: HealthModelT) -> "BaseBasicHealth":
pass

@abstractmethod
async def save(self) -> None:
pass


def basic_health_model(
TortoiseModel: Type[HealthModelT],
) -> Type[BaseBasicHealth[HealthModelT]]:
"""
Creates a pydantic model from a tortoise basic health model.
"""

class _BasicHealth(pydantic_model_creator(TortoiseModel)):
id_: str
health_status: str
health_message: Optional[str]

@classmethod
async def from_tortoise(cls, tortoise: ttm.BasicHealthModel):
return await cls.from_tortoise_orm(tortoise)

async def save(self):
dic = self.dict()
del dic["id_"]
await TortoiseModel.update_or_create(dic, id_=self.id_)

_BasicHealth.__name__ = TortoiseModel.__name__

return cast(Type[BaseBasicHealth[HealthModelT]], _BasicHealth)
health_status: HealthStatus
health_message: str | None
12 changes: 10 additions & 2 deletions packages/api-server/api_server/models/ingestors.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
from pydantic import BaseModel

from . import tortoise_models as ttm
from .health import basic_health_model
from .health import BasicHealth, HealthStatus
from .ros_pydantic import rmf_ingestor_msgs

IngestorHealth = basic_health_model(ttm.IngestorHealth)

class IngestorHealth(BasicHealth):
@classmethod
async def from_tortoise_orm(cls, obj: ttm.IngestorHealth) -> "IngestorHealth":
return IngestorHealth(
id_=obj.id_,
health_status=HealthStatus(obj.health_status),
health_message=obj.health_message,
)


class Ingestor(BaseModel):
Expand Down
17 changes: 12 additions & 5 deletions packages/api-server/api_server/models/lifts.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
from typing import List

from pydantic import BaseModel, Field

from . import tortoise_models as ttm
from .health import basic_health_model
from .health import BasicHealth, HealthStatus
from .ros_pydantic import rmf_building_map_msgs, rmf_lift_msgs

Lift = rmf_building_map_msgs.Lift
LiftHealth = basic_health_model(ttm.LiftHealth)


class LiftHealth(BasicHealth):
@classmethod
async def from_tortoise_orm(cls, obj: ttm.LiftHealth) -> "LiftHealth":
return LiftHealth(
id_=obj.id_,
health_status=HealthStatus(obj.health_status),
health_message=obj.health_message,
)


class LiftState(rmf_lift_msgs.LiftState):
available_modes: List[int]
available_modes: list[int]

@staticmethod
def from_tortoise(tortoise: ttm.LiftState) -> "LiftState":
Expand Down
Loading

0 comments on commit 9567672

Please sign in to comment.