Skip to content

Commit

Permalink
RootController for prefixless routes, fix up imports
Browse files Browse the repository at this point in the history
  • Loading branch information
Etienne Jodry authored and Etienne Jodry committed Apr 12, 2024
1 parent ecfe2ac commit c06281c
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 146 deletions.
5 changes: 2 additions & 3 deletions src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
import uvicorn

from core.api import Api
from core.basics.routes import routes
from core.basics.controllers import CORE_CONTROLLERS
from core.basics import CORE_CONTROLLERS
from instance import config, CONTROLLERS


def main():
app = Api(
debug=config.DEBUG,
routes=routes,
routes=[],
controllers=CORE_CONTROLLERS+CONTROLLERS,
)
return app
Expand Down
30 changes: 2 additions & 28 deletions src/core/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@
from starlette.routing import Route
from starlette.schemas import SchemaGenerator

from core.basics.login import login, syn_ack, authenticated
from core.components.managers import DatabaseManager, KeycloakManager, S3Manager
from core.components.controllers import Controller
from core.errors import onerror
from core.exceptions import RequestError
from core.utils.utils import to_it
from core.utils.security import extract_and_decode_token, auth_header
from core.utils.utils import json_response
from core.tables import History

from instance import config
Expand Down Expand Up @@ -59,8 +58,6 @@ def __init__(self, controllers=[], routes=[], *args, **kwargs):
## Controllers
self.controllers = []
routes.extend(self.adopt_controllers(controllers))
routes.extend(self.setup_login())
routes.extend(self.setup_schema())

## Schema Generator
# TODO: take from config
Expand Down Expand Up @@ -107,34 +104,11 @@ def adopt_controllers(self, controllers: List[Controller]=[]) -> List:
# Instanciate.
c = controller.init(app=self)
# Fetch and add routes.
routes.append(c.routes())
routes.extend(to_it(c.routes()))
# Keep Track of controllers.
self.controllers.append(c)
return routes

def setup_login(self) -> List:
"""Setup login routes."""
return [
Route("/login", endpoint=login),
Route("/syn_ack", endpoint=syn_ack),
Route("/authenticated", endpoint=authenticated)
]

async def openapi_schema(self, request):
# starlette: https://www.starlette.io/schemas/
# doctrings: https://apispec.readthedocs.io/en/stable/
# status codes: https://restfulapi.net/http-status-codes/
return json_response(json.dumps(
self.schema_generator.get_schema(routes=self.routes),
indent=config.INDENT
), status_code=200)

def setup_schema(self) -> List:
"""Setup login routes."""
return [
Route("/schema", endpoint=self.openapi_schema),
]

async def onstart(self) -> None:
if config.DEV:
"""Dev mode: drop all and create tables."""
Expand Down
5 changes: 5 additions & 0 deletions src/core/basics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .root_controller import RootController
from .kc_controller import UserController, GroupController


CORE_CONTROLLERS = [RootController, UserController, GroupController]
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from core.exceptions import ImplementionErrror
from core.components.table import Base

# TODO: base controller that takes on Login/Schema and all other basic routes responsabilities.

# class KCController(AdminController):
class KCController(ActiveController):
Expand Down Expand Up @@ -78,6 +77,3 @@ def __init__(self):
entity="Group",
table=Group,
schema=GroupSchema)


CORE_CONTROLLERS = [UserController, GroupController]
58 changes: 0 additions & 58 deletions src/core/basics/login.py

This file was deleted.

97 changes: 97 additions & 0 deletions src/core/basics/root_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import json
import requests
from typing import List

from starlette.routing import Route
from starlette.responses import PlainTextResponse

from instance import config
from core.components.controllers import Controller, HttpMethod
from core.utils.security import login_required
from core.utils.utils import json_response


class RootController(Controller):
"""
Bundles Routes located at the root of the app i.e. '/'
"""
def routes(self, **_) -> List[Route]:
return [
Route("/live", endpoint=self.live),
Route("/login", endpoint=self.login),
Route("/syn_ack", endpoint=self.syn_ack),
Route("/authenticated", endpoint=self.authenticated), # methods=[]
Route("/schema", endpoint=self.openapi_schema),
]

async def live(_):
"""
description: Liveness check endpoint
"""
return PlainTextResponse("live\n")

async def openapi_schema(self, _):
"""
description: Returns the full schema
"""
return json_response(json.dumps(
self.schema_gen.get_schema(routes=self.app.routes),
indent=config.INDENT
), status_code=200)

# Login.
HANDSHAKE = (f"{config.SERVER_SCHEME}{config.SERVER_HOST}:"
f"{config.SERVER_PORT}/syn_ack")

async def login(self, _):
"""
description: Returns the url for keycloak login page.
responses:
200:
description: Creates associated entity.
examples: |
https://mykeycloak/realms/myrealm/protocol/openid-connect/auth?scope=openid&response_type=code&client_id=myclientid&redirect_uri=http://myapp/syn_ack
"""
login_url = (
f"{config.KC_HOST}/realms/{config.KC_REALM}/"
"protocol/openid-connect/auth?"
"scope=openid" "&response_type=code"
f"&client_id={config.CLIENT_ID}"
f"&redirect_uri={self.HANDSHAKE}"
)
return PlainTextResponse(login_url + "\n")


async def syn_ack(self, request):
"""Login callback function when the user logs in through the browser.
We get an authorization code that we redeem to keycloak for a token.
This way the client_secret remains hidden to the user.
"""
code = request.query_params['code']

kc_token_url = (
f"{config.KC_HOST}/realms/{config.KC_REALM}/"
"protocol/openid-connect/token?"
)
r = requests.post(kc_token_url,
headers={'Content-Type': 'application/x-www-form-urlencoded'},
data={
'grant_type': 'authorization_code',
'client_id': config.CLIENT_ID,
'client_secret': config.CLIENT_SECRET,
'code': code,
# !! Must be the same as in /login
'redirect_uri': self.HANDSHAKE
}
)
if r.status_code != 200:
raise RuntimeError(f"keycloak token handshake failed: {r.text} {r.status_code}")

return PlainTextResponse(json.loads(r.text)['access_token'] + '\n')


@login_required
async def authenticated(self, userid, groups, projects):
"""Route to check token validity."""
return PlainTextResponse(f"{userid}, {groups}, {projects}\n")
9 changes: 0 additions & 9 deletions src/core/basics/routes.py

This file was deleted.

2 changes: 1 addition & 1 deletion src/core/components/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .controller import Controller, ActiveController
from .controller import Controller, EntityController, ActiveController, HttpMethod
from .admincontroller import AdminController
from .s3controller import S3Controller
52 changes: 22 additions & 30 deletions src/core/components/controllers/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@
from instance.entities import tables, schemas


# """ SchemaGenerator object for openapi_schema generation."""
# schema_generator = SchemaGenerator(
# {"openapi": "3.0.0", "info": {"title": "biodm", "version": "0.1.0"}}
# )


class HttpMethod(Enum):
GET = "GET"
POST = "POST"
Expand All @@ -40,6 +34,27 @@ def init(cls, app) -> None:
cls.app = app
return cls()

# Routes
@abstractmethod
def routes(self, **kwargs):
raise NotImplementedError

@property
def schema_gen(self):
return self.app.schema_generator

async def openapi_schema(self, _):
# starlette: https://www.starlette.io/schemas/
# doctrings: https://apispec.readthedocs.io/en/stable/
# status codes: https://restfulapi.net/http-status-codes/
return json_response(json.dumps(
self.schema_gen.get_schema(routes=self.routes().routes),
indent=config.INDENT
), status_code=200)


class EntityController(Controller, ABC):
# Validation & Serialization
@staticmethod
def deserialize(data: Any, schema: Schema) -> (Any | list | dict | None):
"""Deserialize statically passing a schema."""
Expand All @@ -62,16 +77,6 @@ def serialize(data: Any, schema: Schema, many: bool) -> (str | Any):
schema.many = many
return schema.dumps(data, indent=config.INDENT)

# Routes
@abstractmethod
def routes(self, child_routes):
raise NotImplementedError

# OpenAPISchema
@abstractmethod
def openapi_schema(self, request):
raise NotImplementedError

# CRUD operations
@abstractmethod
def create(self, request):
Expand All @@ -98,7 +103,7 @@ def query(self, request):
raise NotImplementedError


class ActiveController(Controller):
class ActiveController(EntityController):
"""Basic class for controllers. Implements the interface CRUD methods."""
def __init__(self,
entity: str=None,
Expand Down Expand Up @@ -126,10 +131,6 @@ def prefix(self):
def qp_id(self):
"""Put primary key in queryparam form."""
return "".join(["{" + k + "}_" for k in self.pk])[:-1]

@property
def schema_gen(self):
return self.app.schema_generator

def _infer_svc(self) -> DatabaseService:
"""Set approriate service for given controller.
Expand Down Expand Up @@ -194,15 +195,6 @@ async def _extract_body(self, request):
raise EmptyPayloadException
return body

async def openapi_schema(self, request):
# starlette: https://www.starlette.io/schemas/
# doctrings: https://apispec.readthedocs.io/en/stable/
# status codes: https://restfulapi.net/http-status-codes/
return json_response(json.dumps(
self.schema_gen.get_schema(routes=self.routes().routes),
indent=config.INDENT
), status_code=200)

async def create(self, request):
"""
responses:
Expand Down
4 changes: 2 additions & 2 deletions src/core/components/managers/dbmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
)
from sqlalchemy.sql import Select, Insert, Update, Delete

from core.components import Base
from core.exceptions import FailedRead, FailedDelete, FailedUpdate
from instance.config import DATABASE_URL, DEBUG

from ..table import Base

class DatabaseManager(object):
def __init__(self, sync=False) -> None:
Expand Down Expand Up @@ -46,6 +45,7 @@ async def init_db(self) -> None:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)

@staticmethod
def in_session(db_exec):
"""Decorator that ensures db_exec receives a session.
Expand Down
Loading

0 comments on commit c06281c

Please sign in to comment.