Skip to content

Commit

Permalink
HH-206678 use fastapi dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
712u3 committed Feb 16, 2024
1 parent 1e12d88 commit 1d4d283
Show file tree
Hide file tree
Showing 154 changed files with 890 additions and 1,415 deletions.
105 changes: 15 additions & 90 deletions docs/dependency_injection.md
Original file line number Diff line number Diff line change
@@ -1,104 +1,29 @@
## Dependency injection

Dependency injection is a pattern when a function receives other functions that it requires,
instead of creating them internally.
In frontik implementation, dependencies are simple functions,
which run after `RequestHandler.prepare` and before* handler code is executed.
Dependencies are great for running common actions before actual request processing takes place.

Here is what a dependencies may look like:

```python
from frontik.dependency_manager import dependency


async def get_session_dependency(handler: PageHandler) -> Session:
token = handler.get_cookie('token')
return await session_client.get_session(token)


class Page(PageHandler):
# Can be used on class level
dependencies = (another_dependency,)

async def get_page(self, session=dependency(get_session_dependency)):
self.json.put({'result': session})
```

If you have several dependencies without results, you can put them all to one dependency marker
```python
from frontik.dependency_manager import dependency


async def check_host(handler: PageHandler) -> None:
if handler.request.host != 'example':
raise HttpError()


async def check_session(session=dependency(get_session_dependency)) -> None:
if session.role != 'admin':
raise HttpError()


class Page(PageHandler):
async def get_page(self, _=dependency(check_host, check_session)):
...
```

Dependency can be sync or async functions. When page is executed all ready to run
async dependencies run in parallel with asyncio.gather(). If something finishes the page
(call self.finish() or raise Exception), then we stop executing the remaining dependencies

Dependencies can depend on another dependencies, thus we have a dependency graph.
Within one execution of a graph, the same dependencies will be executed once.
Sameness is determined by {function.__module__}.{function.__name__}
Dependencies can come from factories, then it turns out that there are several different dependencies
with the same name. In this case the one that is specified explicitly in the method arg or
in class level will be taken, the rest from the graph depths will be discarded


There is an opportunity to specify priorities for dependencies:
We are in the process of migrating to fastapi. But, we still use the server and routing from tornado.
So, in order to specify dependencies for handler, you should pass dependencies to router decorator.
There is special dependency `get_current_handler` for getting PageHandler object

```python
from frontik.dependency_manager import dependency

from frontik.dependency_manager import APIRouter
from fastapi import Depends
from frontik.handler import PageHandler, get_current_handler

async def get_session_dependency(handler: PageHandler) -> Session:
token = handler.get_cookie('token')
return await session_client.get_session(token)
router = APIRouter(dependencies=[Depends(some_dependency_function)])


class Page(PageHandler):
# Can be used on class level
dependencies = (another_dependency,)
_priority_dependency_names: list[str] = [
side_dependency,
another_dependency,
]

async def get_page(self, session=dependency(get_session_dependency)):
self.json.put({'result': session})
```
If any of the _priority_dependency_names are present in the current graph,
they will be executed before all the other dependencies sequentially.
In the given example `another_dependency` -> `get_session_dependency` -> `get_page`


*It is also possible to specify "async" dependencies:

```python
from frontik.dependency_manager import dependency, async_dependencies


async def get_session_dependency(handler: PageHandler) -> Session:
async def get_session_dependency(handler: PageHandler = Depends(get_current_handler)):
token = handler.get_cookie('token')
return await session_client.get_session(token)
await session_client.get_session(token)


class Page(PageHandler):
@async_dependencies([get_session_dependency])
@router.get(dependencies=[Depends(get_session_dependency)])
async def get_page(self):
self.json.put({'result': 'done'})
...
```
The passed list will not block the execution of the page_method, so they can be executed in parallel

If you don’t need to use special dependencies at the router level, then you can use router from the frontik.handler
```py
from frontik.handler import router
```
55 changes: 55 additions & 0 deletions frontik/dependency_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from typing import Any, Callable

from fastapi import APIRouter as FastApiRouter
from fastapi import Request
from fastapi.dependencies.utils import solve_dependencies


class APIRouter(FastApiRouter):
def get(self, **kwargs) -> Callable: # type: ignore
return super().get('', **kwargs)

def post(self, **kwargs) -> Callable: # type: ignore
return super().post('', **kwargs)

def put(self, **kwargs) -> Callable: # type: ignore
return super().put('', **kwargs)

def delete(self, **kwargs) -> Callable: # type: ignore
return super().delete('', **kwargs)

def api_route(self, *args, **kwargs):
decorator = super().api_route(*args, **kwargs)

def frontik_decorator(func):
decorator(func)
func._route = self.routes[-1]
route_method = func._route.methods # type: ignore

if func.__name__ in ('get_page', 'post_page', 'put_page', 'delete_page') and route_method != {
func.__name__.split('_')[0].upper(),
}:
raise RuntimeError(f'Wrong router type func={func.__name__} method={route_method}')
return func

return frontik_decorator


async def execute_page_method_with_dependencies(handler: Any, get_page_method: Callable) -> Any:
request = Request({
'type': 'http',
'query_string': '',
'headers': '',
'handler': handler,
})

route = get_page_method._route # type: ignore

await solve_dependencies(
request=request,
dependant=route.dependant,
body=None,
dependency_overrides_provider=None,
)

return await get_page_method()
68 changes: 0 additions & 68 deletions frontik/dependency_manager/__init__.py

This file was deleted.

101 changes: 0 additions & 101 deletions frontik/dependency_manager/dependencies.py

This file was deleted.

Loading

0 comments on commit 1d4d283

Please sign in to comment.