Skip to content

Commit

Permalink
HH-225824 fix fastapi routing
Browse files Browse the repository at this point in the history
  • Loading branch information
712u3 committed Aug 10, 2024
1 parent 908f365 commit 4c761ae
Show file tree
Hide file tree
Showing 110 changed files with 498 additions and 427 deletions.
7 changes: 3 additions & 4 deletions docs/arguments-validation.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
## Argument validation

Use methods described in
For fastapi routing see fastapi docs

- Query - https://fastapi.tiangolo.com/tutorial/query-params-str-validations/
- Body - https://fastapi.tiangolo.com/tutorial/body-fields/
- Body - https://fastapi.tiangolo.com/tutorial/body-nested-models/
- Path - https://fastapi.tiangolo.com/tutorial/path-params/

Unlike other params, path params don't work as https://fastapi.tiangolo.com/tutorial/path-params/
because right now there is no way to set parametric url. Temporarily you can use regex router as a workaround.
We are going to migrate on fastapi native routing soon, so don't use very complicated regex
In legacy controllers, path params don't work as fastapi. You need to use regex router and then get params with `get_path_argument`:

Path param example:

Expand Down
7 changes: 2 additions & 5 deletions docs/frontik-app.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## Frontik application structure

Each Frontik instance serves one application, which is imported from a package specified in
`app` parameter (see [Running Frontik](/docs/running.md)).
`app_class` parameter (see [Running Frontik](/docs/running.md)).

Frontik application is a set of files, organized in the following structure
(where `app` is a root folder for an application):
Expand All @@ -16,7 +16,7 @@ Frontik application is a set of files, organized in the following structure
```

Application root module may contain class with overrides frontik.app.FrontikApplication class, providing application
specific configs and url mappings, for example:
specific configs, for example:

```
from frontik.app import FrontikApplication
Expand All @@ -25,9 +25,6 @@ from frontik.app import FrontikApplication
class MyApplication(FrontikApplication):
def application_config(self):
return config
def application_urls(self):
return config.urls
```

Application initialization is done in 2 steps:
Expand Down
13 changes: 12 additions & 1 deletion docs/http-client.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
## Making HTTP requests

Frontik's [PageHandler](/frontik/handler.py) contains several methods for making HTTP requests to backends:
Frontik uses [balancing client](https://github.com/hhru/balancing-http-client). You can get instance through special dependency:

```python
from frontik.balancing_client import HttpClientT
from frontik.routing import router

@router.post('/handler')
async def post_page(http_client: HttpClientT) -> None:
result = await http_client.post_url('http://backend', request.url.path, fail_fast=True)
```

In legacy controllers frontik's [PageHandler](/frontik/handler.py) contains several methods for making HTTP requests to backends:
get_url, post_url, put_url, etc...

Method parameters are:
Expand Down
2 changes: 2 additions & 0 deletions docs/page-generation.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Page generation process

Deprecated, works only in routes with `cls=PageHandler`. Use fastapi dependencies/middleware in new code

![Page generation scheme](/docs/page-generation.png)

Typically page generation process is split into several steps:
Expand Down
2 changes: 2 additions & 0 deletions docs/postprocessing.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Postprocessing

Deprecated, works only in routes with `cls=PageHandler`. Use fastapi dependencies in new code

Postprocessing is a mechanism for inserting and running hooks after finishing all page requests
(see [Making HTTP requests](/docs/http-client.md)).

Expand Down
37 changes: 24 additions & 13 deletions docs/routing.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,45 @@
## Routing in Frontik applications

On application start, frontik import all modules from {app_module}.pages so that any controller should be located there.
We use fastapi routing, read [these docs](https://fastapi.tiangolo.com/reference/apirouter/?h=apirouter) for details. A small important difference - you must inherit `frontik.routing.FastAPIRouter` instead `fastapi.APIRouter`. And use `from frontik.routing import router`, if you need default router.

example:

Controller example:
```python
from frontik.routing import router

@router.get("/users/me")
async def read_user_me():
return {"username": "fakecurrentuser"}

```

### Deprecated

Page generation logic with (pre/post)processors and finish group from previous versions is temporarily supported. You need to add `cls=PageHandler` arg to route:

```python
from frontik.routing import plain_router
from frontik.handler import PageHandler

@router.get('/simple_page', cls=PageHandler) # or .post .put .delete .head

@plain_router.get('/simple_page', cls=PageHandler) # or .post .put .delete .head
async def get_page():
...
```

First argument path should be exact string for url matching.
If you need regex with path params use `from frontik.routing import regex_router`

Argument `cls=PageHandler` is required for legacy compatibility, it defines which class will be used to create the handler object.
The handler object can be accessed via `request.state.handler`

You can use 'dependencies' functions in router or method arguments, see https://fastapi.tiangolo.com/tutorial/dependencies/ and https://fastapi.tiangolo.com/tutorial/bigger-applications/#another-module-with-apirouter for details
If you need regex with path params use `from frontik.routing import regex_router`. The handler object can be accessed via `request.state.handler`
You can use fastapi dependencies functions in router, see https://fastapi.tiangolo.com/tutorial/dependencies/ and https://fastapi.tiangolo.com/tutorial/bigger-applications/#another-module-with-apirouter for details

Example with dependencies:

```python
from frontik.routing import router
from frontik.routing import plain_router
from frontik.handler import PageHandler, get_current_handler
from fastapi import Depends

@router.get('/simple_page', cls=PageHandler, dependencies=[Depends(my_foo)])

@plain_router.get('/simple_page', cls=PageHandler, dependencies=[Depends(my_foo)])
async def get_page(handler=get_current_handler()):
...
```

You can create your own router object by extending `frontik.routing.FrontikRouter` and `frontik.routing.FrontikRegexRouter`
4 changes: 2 additions & 2 deletions examples/example_app/pages/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from frontik.handler import PageHandler, get_current_handler
from frontik.routing import router
from frontik.routing import plain_router


@router.get('/tpl', cls=PageHandler)
@plain_router.get('/tpl', cls=PageHandler)
def get_page(handler: PageHandler = get_current_handler()) -> None:
handler.json.put({'text': 'Hello, world!'})
4 changes: 2 additions & 2 deletions examples/example_app/pages/tpl.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from fastapi import Request

from frontik.handler import PageHandler, get_current_handler
from frontik.routing import router
from frontik.routing import plain_router


@router.get('/tpl', cls=PageHandler)
@plain_router.get('/tpl', cls=PageHandler)
def get_page(request: Request, handler: PageHandler = get_current_handler()) -> None:
handler.set_template('main.html') # This template is located in the `templates` folder
handler.json.put(handler.get_url(request.headers.get('host', ''), '/example'))
100 changes: 42 additions & 58 deletions frontik/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from collections.abc import Callable
from ctypes import c_bool, c_int
from threading import Lock
from typing import Awaitable, Optional, Union
from typing import Optional, Union

from aiokafka import AIOKafkaProducer
from fastapi import FastAPI, HTTPException
Expand All @@ -19,55 +19,40 @@

import frontik.producers.json_producer
import frontik.producers.xml_producer
from frontik import integrations, media_types, request_context
from frontik import integrations, media_types
from frontik.debug import get_frontik_and_apps_versions
from frontik.handler import PageHandler, get_current_handler
from frontik.handler_asgi import serve_request
from frontik.handler_asgi import serve_tornado_request
from frontik.handler_return_values import ReturnedValueHandlers, get_default_returned_value_handlers
from frontik.integrations.statsd import StatsDClient, StatsDClientStub, create_statsd_client
from frontik.options import options
from frontik.process import WorkerState
from frontik.routing import import_all_pages, method_not_allowed_router, not_found_router, regex_router, router
from frontik.routing import (
import_all_pages,
method_not_allowed_router,
not_found_router,
plain_router,
regex_router,
router,
routers,
)
from frontik.service_discovery import UpstreamManager
from frontik.util import check_request_id, generate_uniq_timestamp_request_id

app_logger = logging.getLogger('app_logger')


class AsgiRouter:
async def __call__(self, scope, receive, send):
assert scope['type'] == 'http'

if 'router' not in scope:
scope['router'] = self

route = scope['route']
scope['endpoint'] = route.endpoint

await route.handle(scope, receive, send)
_server_tasks = set()


class FrontikAsgiApp(FastAPI):
def __init__(self) -> None:
super().__init__()
self.router = AsgiRouter() # type: ignore
self.http_client = None


@router.get('/version', cls=PageHandler)
@regex_router.get('/version', cls=PageHandler)
async def get_version(handler: PageHandler = get_current_handler()) -> None:
handler.set_header('Content-Type', 'text/xml')
handler.finish(
etree.tostring(get_frontik_and_apps_versions(handler.application), encoding='utf-8', xml_declaration=True),
)
self.router = router
self.http_client_factory = None

if options.openapi_enabled:
self.setup()

@router.get('/status', cls=PageHandler)
@regex_router.get('/status', cls=PageHandler)
async def get_status(handler: PageHandler = get_current_handler()) -> None:
handler.set_header('Content-Type', media_types.APPLICATION_JSON)
handler.finish(handler.application.get_current_status())
for _router in routers:
self.include_router(_router)


class FrontikApplication:
Expand Down Expand Up @@ -113,30 +98,11 @@ def __init__(self, app_module_name: Optional[str] = None) -> None:

self.asgi_app = FrontikAsgiApp()

def __call__(self, tornado_request: httputil.HTTPServerRequest) -> Optional[Awaitable[None]]:
# for making more asgi, reimplement tornado.http1connection._server_request_loop and ._read_message
request_id = tornado_request.headers.get('X-Request-Id') or generate_uniq_timestamp_request_id()
if options.validate_request_id:
check_request_id(request_id)
tornado_request.request_id = request_id # type: ignore

async def _serve_tornado_request(
frontik_app: FrontikApplication,
_tornado_request: httputil.HTTPServerRequest,
asgi_app: FrontikAsgiApp,
) -> None:
status, reason, headers, data = await serve_request(frontik_app, _tornado_request, asgi_app)

assert _tornado_request.connection is not None
_tornado_request.connection.set_close_callback(None) # type: ignore

start_line = httputil.ResponseStartLine('', status, reason)
future = _tornado_request.connection.write_headers(start_line, headers, data)
_tornado_request.connection.finish()
return await future

with request_context.request_context(request_id):
return asyncio.create_task(_serve_tornado_request(self, tornado_request, self.asgi_app))
def __call__(self, tornado_request: httputil.HTTPServerRequest) -> None:
# for make it more asgi, reimplement tornado.http1connection._server_request_loop and ._read_message
task = asyncio.create_task(serve_tornado_request(self, self.asgi_app, tornado_request))
_server_tasks.add(task)
task.add_done_callback(_server_tasks.discard)

def create_upstream_manager(
self,
Expand Down Expand Up @@ -188,6 +154,8 @@ async def init(self) -> None:
kafka_producer=kafka_producer,
)
self.http_client_factory = HttpClientFactory(self.app_name, self.http_client, request_balancer_builder)
self.asgi_app.http_client_factory = self.http_client_factory

if self.worker_state.single_worker_mode:
self.worker_state.master_done.value = True

Expand Down Expand Up @@ -226,3 +194,19 @@ def get_kafka_producer(self, producer_name: str) -> Optional[AIOKafkaProducer]:

def log_request(self, tornado_handler: PageHandler) -> None:
pass


@plain_router.get('/version', cls=PageHandler)
@regex_router.get('/version', cls=PageHandler)
async def get_version(handler: PageHandler = get_current_handler()) -> None:
handler.set_header('Content-Type', 'text/xml')
handler.finish(
etree.tostring(get_frontik_and_apps_versions(handler.application), encoding='utf-8', xml_declaration=True),
)


@plain_router.get('/status', cls=PageHandler)
@regex_router.get('/status', cls=PageHandler)
async def get_status(handler: PageHandler = get_current_handler()) -> None:
handler.set_header('Content-Type', media_types.APPLICATION_JSON)
handler.finish(handler.application.get_current_status())
4 changes: 2 additions & 2 deletions frontik/balancing_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def modify_http_client_request(request: Request, balanced_request: RequestBuilde
)
timeout_checker.check(balanced_request)

if request['pass_debug']:
if request['debug_mode'].pass_debug:
balanced_request.headers[DEBUG_HEADER_NAME] = 'true'

# debug_timestamp is added to avoid caching of debug responses
Expand All @@ -46,7 +46,7 @@ async def _get_http_client(request: Request) -> HttpClient:

http_client = request['http_client_factory'].get_http_client(
modify_http_request_hook=hook,
debug_enabled=request['debug_enabled'],
debug_enabled=request['debug_mode'].enabled,
)

return http_client
Expand Down
Loading

0 comments on commit 4c761ae

Please sign in to comment.