Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e569ef5
WIP Basic cruds
warix8 Nov 30, 2025
c461a17
Small fixes
warix8 Nov 30, 2025
5e37262
Create factory_ticketing.py
warix8 Nov 30, 2025
e1eefcd
Fix typing
warix8 Nov 30, 2025
593e498
Added migrations
warix8 Nov 30, 2025
344c86a
Various fixes
warix8 Nov 30, 2025
35a480b
WIP
warix8 Feb 9, 2026
e3bfeb4
WIP Test init
warix8 Feb 9, 2026
2589bcd
WIP tests init
warix8 Feb 17, 2026
b3a7bc6
Finish rebase
warix8 Feb 17, 2026
f21a8ad
Handle new permissions
warix8 Feb 17, 2026
ac00327
Fix parametrized test
warix8 Feb 17, 2026
a1dc661
Cannot parametrize global variables
warix8 Feb 17, 2026
7c3683e
Migrations rebase
warix8 Feb 17, 2026
4bc9536
WIP
warix8 Feb 17, 2026
cbf0ffc
repaired migrations
warix8 Feb 17, 2026
3609b35
First fixes
warix8 Feb 18, 2026
fe4bfec
Renaming due to SQLAlchemy class conflict
warix8 Feb 18, 2026
cd44280
Redone migrations
warix8 Feb 18, 2026
9139cfc
Fix factory test
warix8 Feb 18, 2026
16f0d85
Forgot to return a 404
warix8 Feb 18, 2026
d8def2c
Tests for events
warix8 Feb 28, 2026
4de2f76
Lint
warix8 Feb 28, 2026
265cd01
Fixed tests (locally)
warix8 Feb 28, 2026
093a2e8
Rebase migration
warix8 Feb 28, 2026
8a77ad9
add organisers
NakoGH Mar 3, 2026
ef5ec3f
add: date to sessions
NakoGH Mar 10, 2026
4710cfe
wip
NakoGH Mar 13, 2026
3bfe0b1
rebase migrations & fix tests
warix8 Mar 19, 2026
9b2704d
Finished tests for sessions
warix8 Mar 19, 2026
225d157
ruff format
warix8 Mar 19, 2026
f1dffa5
relint
warix8 Mar 19, 2026
f26d7b8
WIP Tests categories
warix8 Apr 2, 2026
af457d1
Fixed Categories
warix8 Apr 27, 2026
cb8956e
Format
warix8 Apr 27, 2026
ed4644d
Remove print()
warix8 Apr 27, 2026
d8b5cc5
Redis helpers
warix8 Apr 27, 2026
a259547
Fix mypy issues
warix8 Apr 27, 2026
7b936cf
Fix every mypy, ruff errors
warix8 Apr 27, 2026
b22b22e
Wtf ruff format != check
warix8 Apr 27, 2026
dff8365
Ruff is broken Grr
warix8 Apr 27, 2026
8f26070
WIP tickets
warix8 Jun 1, 2026
bee5bbb
Adding the remaining quota (with cache)
warix8 Jun 6, 2026
b41b0d9
Lint and format
warix8 Jun 6, 2026
f9e2023
removed used quota property
warix8 Jun 6, 2026
9bdb319
Trailing comma missing
warix8 Jun 6, 2026
b43bd2a
Format again
warix8 Jun 6, 2026
4d10e0e
Rebase migration
warix8 Jun 6, 2026
0a242f0
Missing used_quota init
warix8 Jun 6, 2026
c25b843
add user relationship for tickets
NakoGH Jun 9, 2026
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
Empty file.
214 changes: 214 additions & 0 deletions app/modules/ticketing/cache_ticketing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# Redis Cache for Ticketing Module

import logging
from collections.abc import Awaitable, Callable, Coroutine
from typing import Any, ParamSpec, TypeVar
from uuid import UUID

from pydantic import BaseModel
from redis import Redis
from sqlalchemy.ext.asyncio import AsyncSession

from app.modules.ticketing import cruds_ticketing

hyperion_error_logger = logging.getLogger("hyperion.error")

SchemaT = TypeVar("SchemaT", bound=BaseModel)
CrudFuncT = ParamSpec("CrudFuncT")


class RedisKeysList:
"""List of Redis keys used in the ticketing module."""

@staticmethod
def event_remaining_quota(event_id: UUID) -> str:
return f"ticketing:event:{event_id}:quota"

@staticmethod
def category_remaining_quota(category_id: UUID) -> str:
return f"ticketing:category:{category_id}:quota"

@staticmethod
def session_remaining_quota(session_id: UUID) -> str:
return f"ticketing:session:{session_id}:quota"

@staticmethod
def events() -> str:
return "ticketing:events"

@staticmethod
def event(event_id: UUID) -> str:
return f"ticketing:event:{event_id}"

# @staticmethod
# def categories(event_id: UUID) -> str:
# return f"ticketing:event:{event_id}:categories"

# @staticmethod
# def category(category_id: UUID) -> str:
# return f"ticketing:category:{category_id}"

# @staticmethod
# def sessions(category_id: UUID) -> str:
# return f"ticketing:category:{category_id}:sessions"

# @staticmethod
# def session(session_id: UUID) -> str:
# return f"ticketing:session:{session_id}"


async def use_or_set_cache_with_crud(
redis: Redis | None,
key: str,
crud_func: Callable[CrudFuncT, Awaitable[SchemaT]],
schema_class: type[SchemaT],
expire: int | None = 300,
*args: CrudFuncT.args,
**kwargs: CrudFuncT.kwargs,
) -> SchemaT:
"""Use cache if available, otherwise call the database function."""
# If redis is not available, call the crud directly
if redis is None or not isinstance(redis, Redis):
return await crud_func(*args, **kwargs)
cached_value: str | bytes | None = redis.get(key)
if cached_value is not None:
try:
return schema_class.model_validate_json(cached_value)
except Exception:
# If cache is corrupted, delete it and call the crud function
hyperion_error_logger.exception(
"Error parsing cache for key %s, deleting it. Value: %r",
key,
cached_value,
)
redis.delete(key)

value = await crud_func(*args, **kwargs)
redis.set(key, value.model_dump_json(), ex=expire)
return value


async def use_or_set_cache_with_crud_int(
redis: Redis | None,
key: str,
crud_func: Callable[CrudFuncT, Coroutine[Any, Any, int | None]],
expire: int | None = 300,
*args: CrudFuncT.args,
**kwargs: CrudFuncT.kwargs,
) -> int | None:
"""Use cache if available, otherwise call the database function."""
# If redis is not available, call the crud directly
if redis is None or not isinstance(redis, Redis):
return await crud_func(*args, **kwargs)
cached_value: str | bytes | None = redis.get(key)
if cached_value is not None:
try:
return int(cached_value)
except Exception:
# If cache is corrupted, delete it and call the crud function
hyperion_error_logger.exception(
"Error parsing cache for key %s, deleting it. Value: %r",
key,
cached_value,
)
redis.delete(key)

value = await crud_func(*args, **kwargs)
redis.set(key, str(value), ex=expire)
return value


def increment_key_cache(redis: Redis, key: str, amount: int = 1):
"""Increment a Redis key by a given amount."""
if redis is not None and isinstance(redis, Redis):
redis.incrby(key, amount)


def invalidate_key_cache(redis: Redis | None, key: str):
"""Invalidate a Redis cache key."""
if redis is not None and isinstance(redis, Redis):
redis.delete(key)


def update_cache_for_new_ticket(
redis: Redis | None,
event_id: UUID,
category_id: UUID,
session_id: UUID | None,
amount: int = 1, # Increase the used quota by this amount (default is 1 for a single ticket)
):
"""Update the cache for a new ticket."""
if redis is not None and isinstance(redis, Redis):
# Increment the used quota for the event, category, and session
increment_key_cache(
redis,
RedisKeysList.event_remaining_quota(event_id),
-amount,
)
increment_key_cache(
redis,
RedisKeysList.category_remaining_quota(category_id),
-amount,
)
if session_id is not None:
increment_key_cache(
redis,
RedisKeysList.session_remaining_quota(session_id),
-amount,
)
# Invalidate the cache for the event, category, and session to ensure consistency
# invalidate_key_cache(redis, RedisKeysList.events())
# invalidate_key_cache(redis, RedisKeysList.event(event_id))
# invalidate_key_cache(redis, RedisKeysList.categories(event_id))
# invalidate_key_cache(redis, RedisKeysList.category(category_id))
# invalidate_key_cache(redis, RedisKeysList.sessions(category_id))
# if session_id is not None:
# invalidate_key_cache(redis, RedisKeysList.session(session_id))


async def get_event_remaining_quota_with_cache(
redis: Redis | None,
db: AsyncSession,
event_id: UUID,
) -> int | None:
"""Get the remaining quota for an event."""
return await use_or_set_cache_with_crud_int(
redis=redis,
key=RedisKeysList.event_remaining_quota(event_id),
crud_func=cruds_ticketing.get_event_remaining_quota,
expire=6 * 3_600,
db=db,
event_id=event_id,
)


async def get_session_remaining_quota_with_cache(
redis: Redis | None,
db: AsyncSession,
session_id: UUID,
) -> int | None:
"""Get the remaining quota for a session."""
return await use_or_set_cache_with_crud_int(
redis=redis,
key=RedisKeysList.session_remaining_quota(session_id),
crud_func=cruds_ticketing.get_session_remaining_quota,
expire=6 * 3_600,
db=db,
session_id=session_id,
)


async def get_category_remaining_quota_with_cache(
redis: Redis | None,
db: AsyncSession,
category_id: UUID,
) -> int | None:
"""Get the remaining quota for a category."""
return await use_or_set_cache_with_crud_int(
redis=redis,
key=RedisKeysList.category_remaining_quota(category_id),
crud_func=cruds_ticketing.get_category_remaining_quota,
expire=6 * 3_600,
db=db,
category_id=category_id,
)
Loading
Loading