From 445d2e0edaf0cc6a33dbc228c83d07443f28e095 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 20 Oct 2025 08:16:38 +0200 Subject: [PATCH 1/2] LCORE-739: quota limiter stub code --- src/quota/quota_exceed_error.py | 36 ++++++++++++++++++++++ src/quota/quota_limiter.py | 49 ++++++++++++++++++++++++++++++ src/quota/quota_limiter_factory.py | 24 +++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 src/quota/quota_exceed_error.py create mode 100644 src/quota/quota_limiter.py create mode 100644 src/quota/quota_limiter_factory.py diff --git a/src/quota/quota_exceed_error.py b/src/quota/quota_exceed_error.py new file mode 100644 index 00000000..3287b73a --- /dev/null +++ b/src/quota/quota_exceed_error.py @@ -0,0 +1,36 @@ +"""Any exception that can occur when user does not have enough tokens available.""" + + +class QuotaExceedError(Exception): + """Any exception that can occur when user does not have enough tokens available.""" + + def __init__( + self, subject_id: str, subject_type: str, available: int, needed: int = 0 + ) -> None: + """Construct exception object.""" + message: str = "" + + if needed == 0 and available <= 0: + match subject_type: + case "u": + message = f"User {subject_id} has no available tokens" + case "c": + message = "Cluster has no available tokens" + case _: + message = f"Unknown subject {subject_id} has no available tokens" + else: + match subject_type: + case "u": + message = f"User {subject_id} has {available} tokens, but {needed} tokens are needed" # noqa: E501 + case "c": + message = f"Cluster has {available} tokens, but {needed} tokens are needed" + case _: + message = f"Unknown subject {subject_id} has {available} tokens, but {needed} tokens are needed" # noqa: E501 + + # call the base class constructor with the parameters it needs + super().__init__(message) + + # custom attributes + self.subject_id = subject_id + self.available = available + self.needed = needed diff --git a/src/quota/quota_limiter.py b/src/quota/quota_limiter.py new file mode 100644 index 00000000..7bd071ca --- /dev/null +++ b/src/quota/quota_limiter.py @@ -0,0 +1,49 @@ +"""Abstract class that is parent for all quota limiter implementations.""" + +import logging +from abc import ABC, abstractmethod + + +logger = logging.getLogger(__name__) + + +class QuotaLimiter(ABC): + """Abstract class that is parent for all quota limiter implementations.""" + + @abstractmethod + def available_quota(self, subject_id: str) -> int: + """Retrieve available quota for given user.""" + + @abstractmethod + def revoke_quota(self) -> None: + """Revoke quota for given user.""" + + @abstractmethod + def increase_quota(self) -> None: + """Increase quota for given user.""" + + @abstractmethod + def ensure_available_quota(self, subject_id: str = "") -> None: + """Ensure that there's avaiable quota left.""" + + @abstractmethod + def consume_tokens( + self, input_tokens: int, output_tokens: int, subject_id: str = "" + ) -> None: + """Consume tokens by given user.""" + + @abstractmethod + def __init__(self) -> None: + """Initialize connection config.""" + + @abstractmethod + def _initialize_tables(self) -> None: + """Initialize tables and indexes.""" + + # pylint: disable=W0201 + def connect(self) -> None: + """Initialize connection to database.""" + + def connected(self) -> bool: + """Check if connection to cache is alive.""" + return True diff --git a/src/quota/quota_limiter_factory.py b/src/quota/quota_limiter_factory.py new file mode 100644 index 00000000..6f1a8efb --- /dev/null +++ b/src/quota/quota_limiter_factory.py @@ -0,0 +1,24 @@ +"""Quota limiter factory class.""" + +import logging + +from models.config import QuotaHandlersConfiguration + +from constants import USER_QUOTA_LIMITER, CLUSTER_QUOTA_LIMITER +from quota.quota_limiter import QuotaLimiter + +logger = logging.getLogger(__name__) + + +class QuotaLimiterFactory: + """Quota limiter factory class.""" + + @staticmethod + def quota_limiters(config: QuotaHandlersConfiguration) -> list[QuotaLimiter]: + """Create instances of quota limiters based on loaded configuration. + + Returns: + List of instances of 'QuotaLimiter', + """ + limiters: list[QuotaLimiter] = [] + return limiters From de233f8390c857f9c2be6ac645597e48bf26e23c Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 20 Oct 2025 08:24:16 +0200 Subject: [PATCH 2/2] Fixed issues found by linters --- src/quota/quota_exceed_error.py | 2 ++ src/quota/quota_limiter.py | 2 +- src/quota/quota_limiter_factory.py | 10 +++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/quota/quota_exceed_error.py b/src/quota/quota_exceed_error.py index 3287b73a..3adb0a10 100644 --- a/src/quota/quota_exceed_error.py +++ b/src/quota/quota_exceed_error.py @@ -1,5 +1,7 @@ """Any exception that can occur when user does not have enough tokens available.""" +# pylint: disable=line-too-long) + class QuotaExceedError(Exception): """Any exception that can occur when user does not have enough tokens available.""" diff --git a/src/quota/quota_limiter.py b/src/quota/quota_limiter.py index 7bd071ca..d2731741 100644 --- a/src/quota/quota_limiter.py +++ b/src/quota/quota_limiter.py @@ -24,7 +24,7 @@ def increase_quota(self) -> None: @abstractmethod def ensure_available_quota(self, subject_id: str = "") -> None: - """Ensure that there's avaiable quota left.""" + """Ensure that there's available quota left.""" @abstractmethod def consume_tokens( diff --git a/src/quota/quota_limiter_factory.py b/src/quota/quota_limiter_factory.py index 6f1a8efb..92f743f1 100644 --- a/src/quota/quota_limiter_factory.py +++ b/src/quota/quota_limiter_factory.py @@ -4,12 +4,14 @@ from models.config import QuotaHandlersConfiguration -from constants import USER_QUOTA_LIMITER, CLUSTER_QUOTA_LIMITER from quota.quota_limiter import QuotaLimiter logger = logging.getLogger(__name__) +# pylint: disable=too-few-public-methods + + class QuotaLimiterFactory: """Quota limiter factory class.""" @@ -21,4 +23,10 @@ def quota_limiters(config: QuotaHandlersConfiguration) -> list[QuotaLimiter]: List of instances of 'QuotaLimiter', """ limiters: list[QuotaLimiter] = [] + + limiters_config = config.limiters + if limiters_config is None: + logger.warning("Quota limiters are not specified in configuration") + return limiters + return limiters