Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
from app.utils.state import LifespanState

if TYPE_CHECKING:
from redis import Redis
from redis.asyncio import Redis

from app.types.factory import Factory

Expand Down Expand Up @@ -408,21 +408,21 @@ async def initialize_notification_topics(
)


def use_route_path_as_operation_ids(app: FastAPI) -> None:
def use_route_path_as_operation_id(route: APIRoute) -> str:
"""
Simplify operation IDs so that generated API clients have simpler function names.
Simplify operation ID so that generated API clients have simpler function names.

Theses names may be used by API clients to generate function names.
The operation_id will have the format "method_path", like "get_users_me".

See https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/
"""
for route in app.routes:
if isinstance(route, APIRoute):
# The operation_id should be unique.
# It is possible to set multiple methods for the same endpoint method but it's not considered a good practice.
method = "_".join(route.methods)
route.operation_id = method.lower() + route.path.replace("/", "_")
if route.methods:
# The operation_id should be unique.
# It is possible to set multiple methods for the same endpoint method but it's not considered a good practice.
method = "_".join(route.methods)
return method.lower() + route.path.replace("/", "_")
return route.name


def init_db(
Expand Down Expand Up @@ -652,9 +652,9 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[LifespanState]:
title="Hyperion",
version=settings.HYPERION_VERSION,
lifespan=lifespan,
custom_generate_unique_id=use_route_path_as_operation_id,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
custom_generate_unique_id=use_route_path_as_operation_id,
generate_unique_id_function=use_route_path_as_operation_id,

)
app.include_router(api.api_router)
use_route_path_as_operation_ids(app)

app.add_middleware(
CORSMiddleware,
Expand Down Expand Up @@ -709,7 +709,7 @@ async def logging_middleware(
# We test the ip address with the redis limiter
process = True
if redis_client and settings.ENABLE_RATE_LIMITER: # If redis is configured
process, log = limiter(
process, log = await limiter(
redis_client,
ip_address,
settings.REDIS_LIMIT,
Expand Down Expand Up @@ -740,7 +740,7 @@ async def validation_exception_handler(
)

return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
)

Expand Down
3 changes: 2 additions & 1 deletion app/core/permissions/endpoints_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from app.core.groups.groups_type import GroupType
from app.core.permissions import cruds_permissions, schemas_permissions
from app.core.permissions.factory_permissions import CorePermissionsFactory
from app.dependencies import (
get_db,
is_user,
Expand All @@ -30,7 +31,7 @@
root="permissions",
tag="Permissions",
router=router,
factory=None,
factory=CorePermissionsFactory(),
)

hyperion_security_logger = logging.getLogger("hyperion.security")
Expand Down
31 changes: 31 additions & 0 deletions app/core/permissions/factory_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.groups.groups_type import GroupType
from app.core.permissions import cruds_permissions, schemas_permissions
from app.core.utils.config import Settings
from app.module import permissions_list
from app.types.factory import Factory


class CorePermissionsFactory(Factory):
Comment thread
Rotheem marked this conversation as resolved.
depends_on = []

@classmethod
async def run(cls, db: AsyncSession, settings: Settings) -> None:
for permission in permissions_list:
await cruds_permissions.create_group_permission(
permission=schemas_permissions.CoreGroupPermission(
permission_name=permission,
group_id=GroupType.admin.value,
),
db=db,
)
await db.commit()

@classmethod
async def should_run(cls, db: AsyncSession):
permissions = await cruds_permissions.get_permissions(
permissions_list,
db,
)
return not any(permission.groups for permission in permissions)
6 changes: 3 additions & 3 deletions app/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ async def init_state(

SessionLocal = init_SessionLocal(engine)

redis_client = init_redis_client(
redis_client = await init_redis_client(
settings=settings,
hyperion_error_logger=hyperion_error_logger,
)
Expand Down Expand Up @@ -135,7 +135,7 @@ async def disconnect_state(
This methode should be called as a dependency as tests may need to run additional steps
"""

disconnect_redis_client(GLOBAL_STATE["redis_client"])
await disconnect_redis_client(GLOBAL_STATE["redis_client"])
await disconnect_scheduler(GLOBAL_STATE["scheduler"])
await disconnect_websocket_connection_manager(GLOBAL_STATE["ws_manager"])

Expand Down Expand Up @@ -229,7 +229,7 @@ async def get_unsafe_db() -> AsyncGenerator[AsyncSession]:
yield db


def get_redis_client() -> redis.Redis | None:
def get_redis_client() -> redis.asyncio.Redis | None:
"""
Dependency that returns the redis client

Expand Down
21 changes: 10 additions & 11 deletions app/modules/amap/endpoints_amap.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from datetime import UTC, datetime

from fastapi import Depends, HTTPException, Response
from redis import Redis
from redis.asyncio import Redis
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.groups.groups_type import AccountType
Expand Down Expand Up @@ -506,12 +506,12 @@ async def add_order_to_delievery(
raise HTTPException(status_code=400, detail="You can't order nothing")

redis_key = "amap_" + order.user_id
if not isinstance(redis_client, Redis) or locker_get(
if not isinstance(redis_client, Redis) or await locker_get(
redis_client=redis_client,
key=redis_key,
):
raise HTTPException(status_code=429, detail="Too fast !")
locker_set(redis_client=redis_client, key=redis_key, lock=True)
await locker_set(redis_client=redis_client, key=redis_key, lock=True)

try:
await cruds_amap.add_order_to_delivery(
Expand Down Expand Up @@ -548,7 +548,7 @@ async def add_order_to_delievery(
**orderret.__dict__,
)
finally:
locker_set(redis_client=redis_client, key=redis_key, lock=False)
await locker_set(redis_client=redis_client, key=redis_key, lock=False)


@module.router.patch(
Expand Down Expand Up @@ -624,7 +624,6 @@ async def edit_order_from_delivery(
db_order = schemas_amap.OrderComplete(
order_id=order_id,
ordering_date=previous_order.ordering_date,
delivery_date=delivery.delivery_date,
delivery_id=previous_order.delivery_id,
user_id=previous_order.user_id,
amount=amount,
Expand All @@ -637,12 +636,12 @@ async def edit_order_from_delivery(
raise HTTPException(status_code=404, detail="No cash found")

redis_key = "amap_" + previous_order.user_id
if not isinstance(redis_client, Redis) or locker_get(
if not isinstance(redis_client, Redis) or await locker_get(
redis_client=redis_client,
key=redis_key,
):
raise HTTPException(status_code=429, detail="Too fast !")
locker_set(redis_client=redis_client, key=redis_key, lock=True)
await locker_set(redis_client=redis_client, key=redis_key, lock=True)

try:
await cruds_amap.edit_order_with_products(
Expand Down Expand Up @@ -670,7 +669,7 @@ async def edit_order_from_delivery(
)

finally:
locker_set(redis_client=redis_client, key=redis_key, lock=False)
await locker_set(redis_client=redis_client, key=redis_key, lock=False)


@module.router.delete(
Expand Down Expand Up @@ -721,12 +720,12 @@ async def remove_order(

redis_key = "amap_" + order.user_id

if not isinstance(redis_client, Redis) or locker_get(
if not isinstance(redis_client, Redis) or await locker_get(
redis_client=redis_client,
key=redis_key,
):
raise HTTPException(status_code=429, detail="Too fast !")
locker_set(redis_client=redis_client, key=redis_key, lock=True)
await locker_set(redis_client=redis_client, key=redis_key, lock=True)

try:
await cruds_amap.remove_order(
Expand All @@ -744,7 +743,7 @@ async def remove_order(
return Response(status_code=204)

finally:
locker_set(redis_client=redis_client, key=redis_key, lock=False)
await locker_set(redis_client=redis_client, key=redis_key, lock=False)


@module.router.post(
Expand Down
14 changes: 7 additions & 7 deletions app/modules/raffle/endpoints_raffle.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from fastapi import Depends, File, HTTPException, UploadFile
from fastapi.responses import FileResponse
from redis import Redis
from redis.asyncio import Redis
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.groups import cruds_groups
Expand Down Expand Up @@ -517,13 +517,13 @@ async def buy_ticket(

redis_key = "raffle_" + user.id

if not isinstance(redis_client, Redis) or locker_get(
if not isinstance(redis_client, Redis) or await locker_get(
redis_client=redis_client,
key=redis_key,
):
raise HTTPException(status_code=429, detail="Too fast !")

locker_set(redis_client=redis_client, key=redis_key, lock=True)
await locker_set(redis_client=redis_client, key=redis_key, lock=True)

try:
new_amount = balance.balance - pack_ticket.price
Expand All @@ -545,7 +545,7 @@ async def buy_ticket(
return tickets

finally:
locker_set(redis_client=redis_client, key=redis_key, lock=False)
await locker_set(redis_client=redis_client, key=redis_key, lock=False)


@module.router.get(
Expand Down Expand Up @@ -985,12 +985,12 @@ async def edit_cash_by_id(

redis_key = "raffle_" + user_id

if not isinstance(redis_client, Redis) or locker_get(
if not isinstance(redis_client, Redis) or await locker_get(
redis_client=redis_client,
key=redis_key,
):
raise HTTPException(status_code=403, detail="Too fast !")
locker_set(redis_client=redis_client, key=redis_key, lock=True)
await locker_set(redis_client=redis_client, key=redis_key, lock=True)

try:
await cruds_raffle.edit_cash(
Expand All @@ -999,7 +999,7 @@ async def edit_cash_by_id(
db=db,
)
finally:
locker_set(redis_client=redis_client, key=redis_key, lock=False)
await locker_set(redis_client=redis_client, key=redis_key, lock=False)


@module.router.post(
Expand Down
16 changes: 9 additions & 7 deletions app/utils/initialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
import logging
import os
from collections.abc import Callable
from uuid import UUID

import psutil
import redis
import redis.asyncio
from pydantic import ValidationError
from sqlalchemy import Connection, MetaData, delete, select
from sqlalchemy.engine import Engine, create_engine
Expand Down Expand Up @@ -146,7 +148,7 @@ def set_core_data_crud_sync(


def get_school_by_id_sync(
school_id: str,
school_id: UUID,
db: Session,
) -> models_schools.CoreSchool | None:
"""
Expand Down Expand Up @@ -291,7 +293,7 @@ def drop_db_sync(conn: Connection):
async def use_lock_for_workers[**P, R](
job_function: Callable[P, R],
key: str,
redis_client: redis.Redis | None,
redis_client: redis.asyncio.Redis | None,
number_of_workers: int,
logger: logging.Logger,
unlock_key: str | None = None,
Expand Down Expand Up @@ -331,27 +333,27 @@ async def use_lock_for_workers[**P, R](
):
await execute_async_or_sync_method(job_function, *args, **kwargs)

elif redis_client.set(key, "1", nx=True, ex=120):
elif await redis_client.set(key, "1", nx=True, ex=120):
# We acquired the lock, we execute the function
logger.info(f"Running {job_function.__name__}")

await execute_async_or_sync_method(job_function, *args, **kwargs)

if unlock_key is not None:
# We set the unlock_key for other workers to resume operation
redis_client.set(unlock_key, "1")
await redis_client.set(unlock_key, "1")

# After 60 seconds we remove the key for both performance and reloading issues
# we assume other jobs won't take more than 60 seconds and will check this key before expiration
redis_client.expire(unlock_key, 60)
await redis_client.expire(unlock_key, 60)

# After 60 seconds we remove the key for both performance and reloading issues
# we assume other jobs won't take more than 60 seconds and will check this key before expiration
redis_client.expire(key, 60)
await redis_client.expire(key, 60)

elif unlock_key:
# As an `unlock_key` is provided, we will wait until an other worker has finished executing `job_function`
while redis_client.get(unlock_key) is None:
while await redis_client.get(unlock_key) is None:
logger.debug(f"Waiting for {job_function.__name__} to finish")
await asyncio.sleep(1)

Expand Down
16 changes: 8 additions & 8 deletions app/utils/redis.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import redis
from redis.asyncio import Redis


def limiter(redis_client: redis.Redis, key: str, limit: int, window: int):
async def limiter(redis_client: Redis, key: str, limit: int, window: int):
"""Simple fixed window rate limiter, returns a couple of booleans: the first is True if the request can be processed, False otherwise; the second indicates if an alert should be issued. key should be an ip address or a user id"""
# Fixed window: see https://konghq.com/blog/how-to-design-a-scalable-rate-limiting-algorithm.
nb = redis_client.incr(key)
nb = await redis_client.incr(key)
if nb == 1:
redis_client.expire(key, window)
await redis_client.expire(key, window)
elif nb == limit:
return (
False,
Expand All @@ -17,12 +17,12 @@ def limiter(redis_client: redis.Redis, key: str, limit: int, window: int):
return True, False


def locker_get(redis_client: redis.Redis, key: str):
value = redis_client.get(key)
async def locker_get(redis_client: Redis, key: str):
value = await redis_client.get(key)
if value is None:
return False
return bool(int(value))


def locker_set(redis_client: redis.Redis, key: str, lock: bool):
redis_client.set(key, int(lock))
async def locker_set(redis_client: Redis, key: str, lock: bool):
await redis_client.set(key, int(lock))
Loading
Loading