diff --git a/cognee/infrastructure/databases/vector/embeddings/FastembedEmbeddingEngine.py b/cognee/infrastructure/databases/vector/embeddings/FastembedEmbeddingEngine.py index c2acd516e..810178041 100644 --- a/cognee/infrastructure/databases/vector/embeddings/FastembedEmbeddingEngine.py +++ b/cognee/infrastructure/databases/vector/embeddings/FastembedEmbeddingEngine.py @@ -17,6 +17,7 @@ from cognee.infrastructure.databases.exceptions import EmbeddingException from cognee.infrastructure.llm.tokenizer.TikToken import ( TikTokenTokenizer, ) +from cognee.shared.rate_limiting import embedding_rate_limiter_context_manager litellm.set_verbose = False logger = get_logger("FastembedEmbeddingEngine") @@ -68,7 +69,7 @@ class FastembedEmbeddingEngine(EmbeddingEngine): @retry( stop=stop_after_delay(128), - wait=wait_exponential_jitter(2, 128), + wait=wait_exponential_jitter(8, 128), retry=retry_if_not_exception_type(litellm.exceptions.NotFoundError), before_sleep=before_sleep_log(logger, logging.DEBUG), reraise=True, @@ -96,11 +97,12 @@ class FastembedEmbeddingEngine(EmbeddingEngine): if self.mock: return [[0.0] * self.dimensions for _ in text] else: - embeddings = self.embedding_model.embed( - text, - batch_size=len(text), - parallel=None, - ) + async with embedding_rate_limiter_context_manager(): + embeddings = self.embedding_model.embed( + text, + batch_size=len(text), + parallel=None, + ) return list(embeddings) diff --git a/cognee/infrastructure/databases/vector/embeddings/LiteLLMEmbeddingEngine.py b/cognee/infrastructure/databases/vector/embeddings/LiteLLMEmbeddingEngine.py index 03ce86bee..12de57617 100644 --- a/cognee/infrastructure/databases/vector/embeddings/LiteLLMEmbeddingEngine.py +++ b/cognee/infrastructure/databases/vector/embeddings/LiteLLMEmbeddingEngine.py @@ -25,6 +25,7 @@ from cognee.infrastructure.llm.tokenizer.Mistral import ( from cognee.infrastructure.llm.tokenizer.TikToken import ( TikTokenTokenizer, ) +from cognee.shared.rate_limiting import embedding_rate_limiter_context_manager litellm.set_verbose = False logger = get_logger("LiteLLMEmbeddingEngine") @@ -109,13 +110,14 @@ class LiteLLMEmbeddingEngine(EmbeddingEngine): response = {"data": [{"embedding": [0.0] * self.dimensions} for _ in text]} return [data["embedding"] for data in response["data"]] else: - response = await litellm.aembedding( - model=self.model, - input=text, - api_key=self.api_key, - api_base=self.endpoint, - api_version=self.api_version, - ) + async with embedding_rate_limiter_context_manager(): + response = await litellm.aembedding( + model=self.model, + input=text, + api_key=self.api_key, + api_base=self.endpoint, + api_version=self.api_version, + ) return [data["embedding"] for data in response.data] diff --git a/cognee/infrastructure/databases/vector/embeddings/OllamaEmbeddingEngine.py b/cognee/infrastructure/databases/vector/embeddings/OllamaEmbeddingEngine.py index f4510f374..1d5e7fbfe 100644 --- a/cognee/infrastructure/databases/vector/embeddings/OllamaEmbeddingEngine.py +++ b/cognee/infrastructure/databases/vector/embeddings/OllamaEmbeddingEngine.py @@ -18,10 +18,7 @@ from cognee.infrastructure.databases.vector.embeddings.EmbeddingEngine import Em from cognee.infrastructure.llm.tokenizer.HuggingFace import ( HuggingFaceTokenizer, ) -from cognee.infrastructure.databases.vector.embeddings.embedding_rate_limiter import ( - embedding_rate_limit_async, - embedding_sleep_and_retry_async, -) +from cognee.shared.rate_limiting import embedding_rate_limiter_context_manager from cognee.shared.utils import create_secure_ssl_context logger = get_logger("OllamaEmbeddingEngine") @@ -101,7 +98,7 @@ class OllamaEmbeddingEngine(EmbeddingEngine): @retry( stop=stop_after_delay(128), - wait=wait_exponential_jitter(2, 128), + wait=wait_exponential_jitter(8, 128), retry=retry_if_not_exception_type(litellm.exceptions.NotFoundError), before_sleep=before_sleep_log(logger, logging.DEBUG), reraise=True, @@ -120,14 +117,15 @@ class OllamaEmbeddingEngine(EmbeddingEngine): ssl_context = create_secure_ssl_context() connector = aiohttp.TCPConnector(ssl=ssl_context) async with aiohttp.ClientSession(connector=connector) as session: - async with session.post( - self.endpoint, json=payload, headers=headers, timeout=60.0 - ) as response: - data = await response.json() - if "embeddings" in data: - return data["embeddings"][0] - else: - return data["data"][0]["embedding"] + async with embedding_rate_limiter_context_manager(): + async with session.post( + self.endpoint, json=payload, headers=headers, timeout=60.0 + ) as response: + data = await response.json() + if "embeddings" in data: + return data["embeddings"][0] + else: + return data["data"][0]["embedding"] def get_vector_size(self) -> int: """ diff --git a/cognee/infrastructure/databases/vector/embeddings/embedding_rate_limiter.py b/cognee/infrastructure/databases/vector/embeddings/embedding_rate_limiter.py deleted file mode 100644 index 27688d2c9..000000000 --- a/cognee/infrastructure/databases/vector/embeddings/embedding_rate_limiter.py +++ /dev/null @@ -1,544 +0,0 @@ -import threading -import logging -import functools -import os -import time -import asyncio -import random -from cognee.shared.logging_utils import get_logger -from cognee.infrastructure.llm.config import get_llm_config - - -logger = get_logger() - -# Common error patterns that indicate rate limiting -RATE_LIMIT_ERROR_PATTERNS = [ - "rate limit", - "rate_limit", - "ratelimit", - "too many requests", - "retry after", - "capacity", - "quota", - "limit exceeded", - "tps limit exceeded", - "request limit exceeded", - "maximum requests", - "exceeded your current quota", - "throttled", - "throttling", -] - -# Default retry settings -DEFAULT_MAX_RETRIES = 5 -DEFAULT_INITIAL_BACKOFF = 1.0 # seconds -DEFAULT_BACKOFF_FACTOR = 2.0 # exponential backoff multiplier -DEFAULT_JITTER = 0.1 # 10% jitter to avoid thundering herd - - -class EmbeddingRateLimiter: - """ - Rate limiter for embedding API calls. - - This class implements a singleton pattern to ensure that rate limiting - is consistent across all embedding requests. It uses the limits - library with a moving window strategy to control request rates. - - The rate limiter uses the same configuration as the LLM API rate limiter - but uses a separate key to track embedding API calls independently. - - Public Methods: - - get_instance - - reset_instance - - hit_limit - - wait_if_needed - - async_wait_if_needed - - Instance Variables: - - enabled - - requests_limit - - interval_seconds - - request_times - - lock - """ - - _instance = None - lock = threading.Lock() - - @classmethod - def get_instance(cls): - """ - Retrieve the singleton instance of the EmbeddingRateLimiter. - - This method ensures that only one instance of the class exists and - is thread-safe. It lazily initializes the instance if it doesn't - already exist. - - Returns: - -------- - - The singleton instance of the EmbeddingRateLimiter class. - """ - if cls._instance is None: - with cls.lock: - if cls._instance is None: - cls._instance = cls() - return cls._instance - - @classmethod - def reset_instance(cls): - """ - Reset the singleton instance of the EmbeddingRateLimiter. - - This method is thread-safe and sets the instance to None, allowing - for a new instance to be created when requested again. - """ - with cls.lock: - cls._instance = None - - def __init__(self): - config = get_llm_config() - self.enabled = config.embedding_rate_limit_enabled - self.requests_limit = config.embedding_rate_limit_requests - self.interval_seconds = config.embedding_rate_limit_interval - self.request_times = [] - self.lock = threading.Lock() - - logging.info( - f"EmbeddingRateLimiter initialized: enabled={self.enabled}, " - f"requests_limit={self.requests_limit}, interval_seconds={self.interval_seconds}" - ) - - def hit_limit(self) -> bool: - """ - Check if the current request would exceed the rate limit. - - This method checks if the rate limiter is enabled and evaluates - the number of requests made in the elapsed interval. - - Returns: - - bool: True if the rate limit would be exceeded, False otherwise. - - Returns: - -------- - - - bool: True if the rate limit would be exceeded, otherwise False. - """ - if not self.enabled: - return False - - current_time = time.time() - - with self.lock: - # Remove expired request times - cutoff_time = current_time - self.interval_seconds - self.request_times = [t for t in self.request_times if t > cutoff_time] - - # Check if adding a new request would exceed the limit - if len(self.request_times) >= self.requests_limit: - logger.info( - f"Rate limit hit: {len(self.request_times)} requests in the last {self.interval_seconds} seconds" - ) - return True - - # Otherwise, we're under the limit - return False - - def wait_if_needed(self) -> float: - """ - Block until a request can be made without exceeding the rate limit. - - This method will wait if the current request would exceed the - rate limit and returns the time waited in seconds. - - Returns: - - float: Time waited in seconds before a request is allowed. - - Returns: - -------- - - - float: Time waited in seconds before proceeding. - """ - if not self.enabled: - return 0 - - wait_time = 0 - start_time = time.time() - - while self.hit_limit(): - time.sleep(0.5) # Poll every 0.5 seconds - wait_time = time.time() - start_time - - # Record this request - with self.lock: - self.request_times.append(time.time()) - - return wait_time - - async def async_wait_if_needed(self) -> float: - """ - Asynchronously wait until a request can be made without exceeding the rate limit. - - This method will wait if the current request would exceed the - rate limit and returns the time waited in seconds. - - Returns: - - float: Time waited in seconds before a request is allowed. - - Returns: - -------- - - - float: Time waited in seconds before proceeding. - """ - if not self.enabled: - return 0 - - wait_time = 0 - start_time = time.time() - - while self.hit_limit(): - await asyncio.sleep(0.5) # Poll every 0.5 seconds - wait_time = time.time() - start_time - - # Record this request - with self.lock: - self.request_times.append(time.time()) - - return wait_time - - -def embedding_rate_limit_sync(func): - """ - Apply rate limiting to a synchronous embedding function. - - Parameters: - ----------- - - - func: Function to decorate with rate limiting logic. - - Returns: - -------- - - Returns the decorated function that applies rate limiting. - """ - - @functools.wraps(func) - def wrapper(*args, **kwargs): - """ - Wrap the given function with rate limiting logic to control the embedding API usage. - - Checks if the rate limit has been exceeded before allowing the function to execute. If - the limit is hit, it logs a warning and raises an EmbeddingException. Otherwise, it - updates the request count and proceeds to call the original function. - - Parameters: - ----------- - - - *args: Variable length argument list for the wrapped function. - - **kwargs: Keyword arguments for the wrapped function. - - Returns: - -------- - - Returns the result of the wrapped function if rate limiting conditions are met. - """ - limiter = EmbeddingRateLimiter.get_instance() - - # Check if rate limiting is enabled and if we're at the limit - if limiter.hit_limit(): - error_msg = "Embedding API rate limit exceeded" - logger.warning(error_msg) - - # Create a custom embedding rate limit exception - from cognee.infrastructure.databases.exceptions import EmbeddingException - - raise EmbeddingException(error_msg) - - # Add this request to the counter and proceed - limiter.wait_if_needed() - return func(*args, **kwargs) - - return wrapper - - -def embedding_rate_limit_async(func): - """ - Decorator that applies rate limiting to an asynchronous embedding function. - - Parameters: - ----------- - - - func: Async function to decorate. - - Returns: - -------- - - Returns the decorated async function that applies rate limiting. - """ - - @functools.wraps(func) - async def wrapper(*args, **kwargs): - """ - Handle function calls with embedding rate limiting. - - This asynchronous wrapper checks if the embedding API rate limit is exceeded before - allowing the function to execute. If the limit is exceeded, it logs a warning and raises - an EmbeddingException. If not, it waits as necessary and proceeds with the function - call. - - Parameters: - ----------- - - - *args: Positional arguments passed to the wrapped function. - - **kwargs: Keyword arguments passed to the wrapped function. - - Returns: - -------- - - Returns the result of the wrapped function after handling rate limiting. - """ - limiter = EmbeddingRateLimiter.get_instance() - - # Check if rate limiting is enabled and if we're at the limit - if limiter.hit_limit(): - error_msg = "Embedding API rate limit exceeded" - logger.warning(error_msg) - - # Create a custom embedding rate limit exception - from cognee.infrastructure.databases.exceptions import EmbeddingException - - raise EmbeddingException(error_msg) - - # Add this request to the counter and proceed - await limiter.async_wait_if_needed() - return await func(*args, **kwargs) - - return wrapper - - -def embedding_sleep_and_retry_sync(max_retries=5, base_backoff=1.0, jitter=0.5): - """ - Add retry with exponential backoff for synchronous embedding functions. - - Parameters: - ----------- - - - max_retries: Maximum number of retries before giving up. (default 5) - - base_backoff: Base backoff time in seconds for retry intervals. (default 1.0) - - jitter: Jitter factor to randomize the backoff time to avoid collision. (default - 0.5) - - Returns: - -------- - - A decorator that retries the wrapped function on rate limit errors, applying - exponential backoff with jitter. - """ - - def decorator(func): - """ - Wraps a function to apply retry logic on rate limit errors. - - Parameters: - ----------- - - - func: The function to be wrapped with retry logic. - - Returns: - -------- - - Returns the wrapped function with retry logic applied. - """ - - @functools.wraps(func) - def wrapper(*args, **kwargs): - """ - Retry the execution of a function with backoff on failure due to rate limit errors. - - This wrapper function will call the specified function and if it raises an exception, it - will handle retries according to defined conditions. It will check the environment for a - DISABLE_RETRIES flag to determine whether to retry or propagate errors immediately - during tests. If the error is identified as a rate limit error, it will apply an - exponential backoff strategy with jitter before retrying, up to a maximum number of - retries. If the retries are exhausted, it raises the last encountered error. - - Parameters: - ----------- - - - *args: Positional arguments passed to the wrapped function. - - **kwargs: Keyword arguments passed to the wrapped function. - - Returns: - -------- - - Returns the result of the wrapped function if successful; otherwise, raises the last - error encountered after maximum retries are exhausted. - """ - # If DISABLE_RETRIES is set, don't retry for testing purposes - disable_retries = os.environ.get("DISABLE_RETRIES", "false").lower() in ( - "true", - "1", - "yes", - ) - - retries = 0 - last_error = None - - while retries <= max_retries: - try: - return func(*args, **kwargs) - except Exception as e: - # Check if this is a rate limit error - error_str = str(e).lower() - error_type = type(e).__name__ - is_rate_limit = any( - pattern in error_str.lower() for pattern in RATE_LIMIT_ERROR_PATTERNS - ) - - if disable_retries: - # For testing, propagate the exception immediately - raise - - if is_rate_limit and retries < max_retries: - # Calculate backoff with jitter - backoff = ( - base_backoff * (2**retries) * (1 + random.uniform(-jitter, jitter)) - ) - - logger.warning( - f"Embedding rate limit hit, retrying in {backoff:.2f}s " - f"(attempt {retries + 1}/{max_retries}): " - f"({error_str!r}, {error_type!r})" - ) - - time.sleep(backoff) - retries += 1 - last_error = e - else: - # Not a rate limit error or max retries reached, raise - raise - - # If we exit the loop due to max retries, raise the last error - if last_error: - raise last_error - - return wrapper - - return decorator - - -def embedding_sleep_and_retry_async(max_retries=5, base_backoff=1.0, jitter=0.5): - """ - Add retry logic with exponential backoff for asynchronous embedding functions. - - This decorator retries the wrapped asynchronous function upon encountering rate limit - errors, utilizing exponential backoff with optional jitter to space out retry attempts. - It allows for a maximum number of retries before giving up and raising the last error - encountered. - - Parameters: - ----------- - - - max_retries: Maximum number of retries allowed before giving up. (default 5) - - base_backoff: Base amount of time in seconds to wait before retrying after a rate - limit error. (default 1.0) - - jitter: Amount of randomness to add to the backoff duration to help mitigate burst - issues on retries. (default 0.5) - - Returns: - -------- - - Returns a decorated asynchronous function that implements the retry logic on rate - limit errors. - """ - - def decorator(func): - """ - Handle retries for an async function with exponential backoff and jitter. - - Parameters: - ----------- - - - func: An asynchronous function to be wrapped with retry logic. - - Returns: - -------- - - Returns the wrapper function that manages the retry behavior for the wrapped async - function. - """ - - @functools.wraps(func) - async def wrapper(*args, **kwargs): - """ - Handle retries for an async function with exponential backoff and jitter. - - If the environment variable DISABLE_RETRIES is set to true, 1, or yes, the function will - not retry on errors. - It attempts to call the wrapped function until it succeeds or the maximum number of - retries is reached. If an exception occurs, it checks if it's a rate limit error to - determine if a retry is needed. - - Parameters: - ----------- - - - *args: Positional arguments passed to the wrapped function. - - **kwargs: Keyword arguments passed to the wrapped function. - - Returns: - -------- - - Returns the result of the wrapped async function if successful; raises the last - encountered error if all retries fail. - """ - # If DISABLE_RETRIES is set, don't retry for testing purposes - disable_retries = os.environ.get("DISABLE_RETRIES", "false").lower() in ( - "true", - "1", - "yes", - ) - - retries = 0 - last_error = None - - while retries <= max_retries: - try: - return await func(*args, **kwargs) - except Exception as e: - # Check if this is a rate limit error - error_str = str(e).lower() - error_type = type(e).__name__ - is_rate_limit = any( - pattern in error_str.lower() for pattern in RATE_LIMIT_ERROR_PATTERNS - ) - - if disable_retries: - # For testing, propagate the exception immediately - raise - - if is_rate_limit and retries < max_retries: - # Calculate backoff with jitter - backoff = ( - base_backoff * (2**retries) * (1 + random.uniform(-jitter, jitter)) - ) - - logger.warning( - f"Embedding rate limit hit, retrying in {backoff:.2f}s " - f"(attempt {retries + 1}/{max_retries}): " - f"({error_str!r}, {error_type!r})" - ) - - await asyncio.sleep(backoff) - retries += 1 - last_error = e - else: - # Not a rate limit error or max retries reached, raise - raise - - # If we exit the loop due to max retries, raise the last error - if last_error: - raise last_error - - return wrapper - - return decorator diff --git a/cognee/infrastructure/llm/structured_output_framework/baml/baml_src/extraction/acreate_structured_output.py b/cognee/infrastructure/llm/structured_output_framework/baml/baml_src/extraction/acreate_structured_output.py index 6ef27e51d..fe99d942c 100644 --- a/cognee/infrastructure/llm/structured_output_framework/baml/baml_src/extraction/acreate_structured_output.py +++ b/cognee/infrastructure/llm/structured_output_framework/baml/baml_src/extraction/acreate_structured_output.py @@ -1,7 +1,15 @@ import asyncio from typing import Type -from cognee.shared.logging_utils import get_logger +from pydantic import BaseModel +from tenacity import ( + retry, + stop_after_delay, + wait_exponential_jitter, + retry_if_not_exception_type, + before_sleep_log, +) +from cognee.shared.logging_utils import get_logger from cognee.infrastructure.llm.config import get_llm_config from cognee.infrastructure.llm.structured_output_framework.baml.baml_src.extraction.create_dynamic_baml_type import ( create_dynamic_baml_type, @@ -10,12 +18,18 @@ from cognee.infrastructure.llm.structured_output_framework.baml.baml_client.type TypeBuilder, ) from cognee.infrastructure.llm.structured_output_framework.baml.baml_client import b -from pydantic import BaseModel - +from cognee.shared.rate_limiting import llm_rate_limiter_context_manager +import logging logger = get_logger() +@retry( + stop=stop_after_delay(128), + wait=wait_exponential_jitter(8, 128), + before_sleep=before_sleep_log(logger, logging.DEBUG), + reraise=True, +) async def acreate_structured_output( text_input: str, system_prompt: str, response_model: Type[BaseModel] ): @@ -45,11 +59,12 @@ async def acreate_structured_output( tb = TypeBuilder() type_builder = create_dynamic_baml_type(tb, tb.ResponseModel, response_model) - result = await b.AcreateStructuredOutput( - text_input=text_input, - system_prompt=system_prompt, - baml_options={"client_registry": config.baml_registry, "tb": type_builder}, - ) + async with llm_rate_limiter_context_manager(): + result = await b.AcreateStructuredOutput( + text_input=text_input, + system_prompt=system_prompt, + baml_options={"client_registry": config.baml_registry, "tb": type_builder}, + ) # Transform BAML response to proper pydantic reponse model if response_model is str: diff --git a/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/anthropic/adapter.py b/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/anthropic/adapter.py index dbf0dfbea..b6f218022 100644 --- a/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/anthropic/adapter.py +++ b/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/anthropic/adapter.py @@ -15,6 +15,7 @@ from tenacity import ( from cognee.infrastructure.llm.structured_output_framework.litellm_instructor.llm.llm_interface import ( LLMInterface, ) +from cognee.shared.rate_limiting import llm_rate_limiter_context_manager from cognee.infrastructure.llm.config import get_llm_config logger = get_logger() @@ -45,7 +46,7 @@ class AnthropicAdapter(LLMInterface): @retry( stop=stop_after_delay(128), - wait=wait_exponential_jitter(2, 128), + wait=wait_exponential_jitter(8, 128), retry=retry_if_not_exception_type(litellm.exceptions.NotFoundError), before_sleep=before_sleep_log(logger, logging.DEBUG), reraise=True, @@ -69,17 +70,17 @@ class AnthropicAdapter(LLMInterface): - BaseModel: An instance of BaseModel containing the structured response. """ - - return await self.aclient( - model=self.model, - max_tokens=4096, - max_retries=5, - messages=[ - { - "role": "user", - "content": f"""Use the given format to extract information - from the following input: {text_input}. {system_prompt}""", - } - ], - response_model=response_model, - ) + async with llm_rate_limiter_context_manager(): + return await self.aclient( + model=self.model, + max_tokens=4096, + max_retries=2, + messages=[ + { + "role": "user", + "content": f"""Use the given format to extract information + from the following input: {text_input}. {system_prompt}""", + } + ], + response_model=response_model, + ) diff --git a/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/gemini/adapter.py b/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/gemini/adapter.py index 226f291d7..a8fcebbee 100644 --- a/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/gemini/adapter.py +++ b/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/gemini/adapter.py @@ -13,6 +13,7 @@ from cognee.infrastructure.llm.structured_output_framework.litellm_instructor.ll LLMInterface, ) import logging +from cognee.shared.rate_limiting import llm_rate_limiter_context_manager from cognee.shared.logging_utils import get_logger from tenacity import ( retry, @@ -73,7 +74,7 @@ class GeminiAdapter(LLMInterface): @retry( stop=stop_after_delay(128), - wait=wait_exponential_jitter(2, 128), + wait=wait_exponential_jitter(8, 128), retry=retry_if_not_exception_type(litellm.exceptions.NotFoundError), before_sleep=before_sleep_log(logger, logging.DEBUG), reraise=True, @@ -105,24 +106,25 @@ class GeminiAdapter(LLMInterface): """ try: - return await self.aclient.chat.completions.create( - model=self.model, - messages=[ - { - "role": "user", - "content": f"""{text_input}""", - }, - { - "role": "system", - "content": system_prompt, - }, - ], - api_key=self.api_key, - max_retries=5, - api_base=self.endpoint, - api_version=self.api_version, - response_model=response_model, - ) + async with llm_rate_limiter_context_manager(): + return await self.aclient.chat.completions.create( + model=self.model, + messages=[ + { + "role": "user", + "content": f"""{text_input}""", + }, + { + "role": "system", + "content": system_prompt, + }, + ], + api_key=self.api_key, + max_retries=2, + api_base=self.endpoint, + api_version=self.api_version, + response_model=response_model, + ) except ( ContentFilterFinishReasonError, ContentPolicyViolationError, @@ -140,23 +142,24 @@ class GeminiAdapter(LLMInterface): ) try: - return await self.aclient.chat.completions.create( - model=self.fallback_model, - messages=[ - { - "role": "user", - "content": f"""{text_input}""", - }, - { - "role": "system", - "content": system_prompt, - }, - ], - max_retries=5, - api_key=self.fallback_api_key, - api_base=self.fallback_endpoint, - response_model=response_model, - ) + async with llm_rate_limiter_context_manager(): + return await self.aclient.chat.completions.create( + model=self.fallback_model, + messages=[ + { + "role": "user", + "content": f"""{text_input}""", + }, + { + "role": "system", + "content": system_prompt, + }, + ], + max_retries=2, + api_key=self.fallback_api_key, + api_base=self.fallback_endpoint, + response_model=response_model, + ) except ( ContentFilterFinishReasonError, ContentPolicyViolationError, diff --git a/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/generic_llm_api/adapter.py b/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/generic_llm_api/adapter.py index 9d7f25fc5..9beb702e5 100644 --- a/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/generic_llm_api/adapter.py +++ b/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/generic_llm_api/adapter.py @@ -13,6 +13,7 @@ from cognee.infrastructure.llm.structured_output_framework.litellm_instructor.ll LLMInterface, ) import logging +from cognee.shared.rate_limiting import llm_rate_limiter_context_manager from cognee.shared.logging_utils import get_logger from tenacity import ( retry, @@ -73,7 +74,7 @@ class GenericAPIAdapter(LLMInterface): @retry( stop=stop_after_delay(128), - wait=wait_exponential_jitter(2, 128), + wait=wait_exponential_jitter(8, 128), retry=retry_if_not_exception_type(litellm.exceptions.NotFoundError), before_sleep=before_sleep_log(logger, logging.DEBUG), reraise=True, @@ -105,23 +106,24 @@ class GenericAPIAdapter(LLMInterface): """ try: - return await self.aclient.chat.completions.create( - model=self.model, - messages=[ - { - "role": "user", - "content": f"""{text_input}""", - }, - { - "role": "system", - "content": system_prompt, - }, - ], - max_retries=5, - api_key=self.api_key, - api_base=self.endpoint, - response_model=response_model, - ) + async with llm_rate_limiter_context_manager(): + return await self.aclient.chat.completions.create( + model=self.model, + messages=[ + { + "role": "user", + "content": f"""{text_input}""", + }, + { + "role": "system", + "content": system_prompt, + }, + ], + max_retries=2, + api_key=self.api_key, + api_base=self.endpoint, + response_model=response_model, + ) except ( ContentFilterFinishReasonError, ContentPolicyViolationError, @@ -139,23 +141,24 @@ class GenericAPIAdapter(LLMInterface): ) from error try: - return await self.aclient.chat.completions.create( - model=self.fallback_model, - messages=[ - { - "role": "user", - "content": f"""{text_input}""", - }, - { - "role": "system", - "content": system_prompt, - }, - ], - max_retries=5, - api_key=self.fallback_api_key, - api_base=self.fallback_endpoint, - response_model=response_model, - ) + async with llm_rate_limiter_context_manager(): + return await self.aclient.chat.completions.create( + model=self.fallback_model, + messages=[ + { + "role": "user", + "content": f"""{text_input}""", + }, + { + "role": "system", + "content": system_prompt, + }, + ], + max_retries=2, + api_key=self.fallback_api_key, + api_base=self.fallback_endpoint, + response_model=response_model, + ) except ( ContentFilterFinishReasonError, ContentPolicyViolationError, diff --git a/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/mistral/adapter.py b/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/mistral/adapter.py index 355cdae0b..e9580faeb 100644 --- a/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/mistral/adapter.py +++ b/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/mistral/adapter.py @@ -10,6 +10,7 @@ from cognee.infrastructure.llm.structured_output_framework.litellm_instructor.ll LLMInterface, ) from cognee.infrastructure.llm.config import get_llm_config +from cognee.shared.rate_limiting import llm_rate_limiter_context_manager import logging from tenacity import ( @@ -62,7 +63,7 @@ class MistralAdapter(LLMInterface): @retry( stop=stop_after_delay(128), - wait=wait_exponential_jitter(2, 128), + wait=wait_exponential_jitter(8, 128), retry=retry_if_not_exception_type(litellm.exceptions.NotFoundError), before_sleep=before_sleep_log(logger, logging.DEBUG), reraise=True, @@ -97,13 +98,14 @@ class MistralAdapter(LLMInterface): }, ] try: - response = await self.aclient.chat.completions.create( - model=self.model, - max_tokens=self.max_completion_tokens, - max_retries=5, - messages=messages, - response_model=response_model, - ) + async with llm_rate_limiter_context_manager(): + response = await self.aclient.chat.completions.create( + model=self.model, + max_tokens=self.max_completion_tokens, + max_retries=2, + messages=messages, + response_model=response_model, + ) if response.choices and response.choices[0].message.content: content = response.choices[0].message.content return response_model.model_validate_json(content) diff --git a/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/ollama/adapter.py b/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/ollama/adapter.py index aabd19867..877da23ef 100644 --- a/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/ollama/adapter.py +++ b/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/ollama/adapter.py @@ -11,6 +11,8 @@ from cognee.infrastructure.llm.structured_output_framework.litellm_instructor.ll ) from cognee.infrastructure.files.utils.open_data_file import open_data_file from cognee.shared.logging_utils import get_logger +from cognee.shared.rate_limiting import llm_rate_limiter_context_manager + from tenacity import ( retry, stop_after_delay, @@ -68,7 +70,7 @@ class OllamaAPIAdapter(LLMInterface): @retry( stop=stop_after_delay(128), - wait=wait_exponential_jitter(2, 128), + wait=wait_exponential_jitter(8, 128), retry=retry_if_not_exception_type(litellm.exceptions.NotFoundError), before_sleep=before_sleep_log(logger, logging.DEBUG), reraise=True, @@ -95,28 +97,28 @@ class OllamaAPIAdapter(LLMInterface): - BaseModel: A structured output that conforms to the specified response model. """ - - response = self.aclient.chat.completions.create( - model=self.model, - messages=[ - { - "role": "user", - "content": f"{text_input}", - }, - { - "role": "system", - "content": system_prompt, - }, - ], - max_retries=5, - response_model=response_model, - ) + async with llm_rate_limiter_context_manager(): + response = self.aclient.chat.completions.create( + model=self.model, + messages=[ + { + "role": "user", + "content": f"{text_input}", + }, + { + "role": "system", + "content": system_prompt, + }, + ], + max_retries=2, + response_model=response_model, + ) return response @retry( stop=stop_after_delay(128), - wait=wait_exponential_jitter(2, 128), + wait=wait_exponential_jitter(8, 128), retry=retry_if_not_exception_type(litellm.exceptions.NotFoundError), before_sleep=before_sleep_log(logger, logging.DEBUG), reraise=True, diff --git a/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/openai/adapter.py b/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/openai/adapter.py index 778c8eec7..407b720a8 100644 --- a/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/openai/adapter.py +++ b/cognee/infrastructure/llm/structured_output_framework/litellm_instructor/llm/openai/adapter.py @@ -22,6 +22,7 @@ from cognee.infrastructure.llm.structured_output_framework.litellm_instructor.ll from cognee.infrastructure.llm.exceptions import ( ContentPolicyFilterError, ) +from cognee.shared.rate_limiting import llm_rate_limiter_context_manager from cognee.infrastructure.files.utils.open_data_file import open_data_file from cognee.modules.observability.get_observe import get_observe from cognee.shared.logging_utils import get_logger @@ -105,7 +106,7 @@ class OpenAIAdapter(LLMInterface): @observe(as_type="generation") @retry( stop=stop_after_delay(128), - wait=wait_exponential_jitter(2, 128), + wait=wait_exponential_jitter(8, 128), retry=retry_if_not_exception_type(litellm.exceptions.NotFoundError), before_sleep=before_sleep_log(logger, logging.DEBUG), reraise=True, @@ -135,34 +136,9 @@ class OpenAIAdapter(LLMInterface): """ try: - return await self.aclient.chat.completions.create( - model=self.model, - messages=[ - { - "role": "user", - "content": f"""{text_input}""", - }, - { - "role": "system", - "content": system_prompt, - }, - ], - api_key=self.api_key, - api_base=self.endpoint, - api_version=self.api_version, - response_model=response_model, - max_retries=self.MAX_RETRIES, - ) - except ( - ContentFilterFinishReasonError, - ContentPolicyViolationError, - InstructorRetryException, - ) as e: - if not (self.fallback_model and self.fallback_api_key): - raise e - try: + async with llm_rate_limiter_context_manager(): return await self.aclient.chat.completions.create( - model=self.fallback_model, + model=self.model, messages=[ { "role": "user", @@ -173,11 +149,38 @@ class OpenAIAdapter(LLMInterface): "content": system_prompt, }, ], - api_key=self.fallback_api_key, - # api_base=self.fallback_endpoint, + api_key=self.api_key, + api_base=self.endpoint, + api_version=self.api_version, response_model=response_model, max_retries=self.MAX_RETRIES, ) + except ( + ContentFilterFinishReasonError, + ContentPolicyViolationError, + InstructorRetryException, + ) as e: + if not (self.fallback_model and self.fallback_api_key): + raise e + try: + async with llm_rate_limiter_context_manager(): + return await self.aclient.chat.completions.create( + model=self.fallback_model, + messages=[ + { + "role": "user", + "content": f"""{text_input}""", + }, + { + "role": "system", + "content": system_prompt, + }, + ], + api_key=self.fallback_api_key, + # api_base=self.fallback_endpoint, + response_model=response_model, + max_retries=self.MAX_RETRIES, + ) except ( ContentFilterFinishReasonError, ContentPolicyViolationError, diff --git a/cognee/shared/rate_limiting.py b/cognee/shared/rate_limiting.py new file mode 100644 index 000000000..1eb952fa2 --- /dev/null +++ b/cognee/shared/rate_limiting.py @@ -0,0 +1,30 @@ +from aiolimiter import AsyncLimiter +from contextlib import nullcontext +from cognee.infrastructure.llm.config import get_llm_config + +llm_config = get_llm_config() + +llm_rate_limiter = AsyncLimiter( + llm_config.llm_rate_limit_requests, llm_config.embedding_rate_limit_interval +) +embedding_rate_limiter = AsyncLimiter( + llm_config.embedding_rate_limit_requests, llm_config.embedding_rate_limit_interval +) + + +def llm_rate_limiter_context_manager(): + global llm_rate_limiter + if llm_config.llm_rate_limit_enabled: + return llm_rate_limiter + else: + # Return a no-op context manager if rate limiting is disabled + return nullcontext() + + +def embedding_rate_limiter_context_manager(): + global embedding_rate_limiter + if llm_config.embedding_rate_limit_enabled: + return embedding_rate_limiter + else: + # Return a no-op context manager if rate limiting is disabled + return nullcontext() diff --git a/cognee/tests/unit/infrastructure/mock_embedding_engine.py b/cognee/tests/unit/infrastructure/mock_embedding_engine.py index c114d1dc8..39344fb6e 100644 --- a/cognee/tests/unit/infrastructure/mock_embedding_engine.py +++ b/cognee/tests/unit/infrastructure/mock_embedding_engine.py @@ -4,10 +4,7 @@ from typing import List from cognee.infrastructure.databases.vector.embeddings.LiteLLMEmbeddingEngine import ( LiteLLMEmbeddingEngine, ) -from cognee.infrastructure.databases.vector.embeddings.embedding_rate_limiter import ( - embedding_rate_limit_async, - embedding_sleep_and_retry_async, -) +from cognee.shared.rate_limiting import embedding_rate_limiter_context_manager class MockEmbeddingEngine(LiteLLMEmbeddingEngine): @@ -34,8 +31,6 @@ class MockEmbeddingEngine(LiteLLMEmbeddingEngine): self.fail_every_n_requests = fail_every_n_requests self.add_delay = add_delay - @embedding_sleep_and_retry_async() - @embedding_rate_limit_async async def embed_text(self, text: List[str]) -> List[List[float]]: """ Mock implementation that returns fixed embeddings and can @@ -52,4 +47,5 @@ class MockEmbeddingEngine(LiteLLMEmbeddingEngine): raise Exception(f"Mock failure on request #{self.request_count}") # Return mock embeddings of the correct dimension - return [[0.1] * self.dimensions for _ in text] + async with embedding_rate_limiter_context_manager(): + return [[0.1] * self.dimensions for _ in text] diff --git a/cognee/tests/unit/infrastructure/test_embedding_rate_limiting_realistic.py b/cognee/tests/unit/infrastructure/test_embedding_rate_limiting_realistic.py index 25566dce1..90a46b73a 100644 --- a/cognee/tests/unit/infrastructure/test_embedding_rate_limiting_realistic.py +++ b/cognee/tests/unit/infrastructure/test_embedding_rate_limiting_realistic.py @@ -6,9 +6,6 @@ import logging from cognee.infrastructure.llm.config import ( get_llm_config, ) -from cognee.infrastructure.databases.vector.embeddings.embedding_rate_limiter import ( - EmbeddingRateLimiter, -) from cognee.tests.unit.infrastructure.mock_embedding_engine import MockEmbeddingEngine # Configure logging @@ -33,7 +30,6 @@ async def test_embedding_rate_limiting_realistic(): # Clear the config and rate limiter caches to ensure our settings are applied get_llm_config.cache_clear() - EmbeddingRateLimiter.reset_instance() # Create a fresh config instance and verify settings config = get_llm_config() @@ -170,7 +166,6 @@ async def test_with_mock_failures(): # Clear caches get_llm_config.cache_clear() - EmbeddingRateLimiter.reset_instance() # Create a mock engine configured to fail every 3rd request engine = MockEmbeddingEngine() diff --git a/poetry.lock b/poetry.lock index 155f6ab9d..e68695efe 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "accelerate" @@ -242,6 +242,18 @@ files = [ {file = "aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c"}, ] +[[package]] +name = "aiolimiter" +version = "1.2.1" +description = "asyncio rate limiter, a leaky bucket implementation" +optional = false +python-versions = "<4.0,>=3.8" +groups = ["main"] +files = [ + {file = "aiolimiter-1.2.1-py3-none-any.whl", hash = "sha256:d3f249e9059a20badcb56b61601a83556133655c11d1eb3dd3e04ff069e5f3c7"}, + {file = "aiolimiter-1.2.1.tar.gz", hash = "sha256:e02a37ea1a855d9e832252a105420ad4d15011505512a1a1d814647451b5cca9"}, +] + [[package]] name = "aiosignal" version = "1.4.0" @@ -3309,8 +3321,6 @@ files = [ {file = "greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d"}, {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5"}, {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f"}, - {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7"}, - {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8"}, {file = "greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c"}, {file = "greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2"}, {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246"}, @@ -3320,8 +3330,6 @@ files = [ {file = "greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8"}, {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52"}, {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa"}, - {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c"}, - {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5"}, {file = "greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9"}, {file = "greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd"}, {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb"}, @@ -3331,8 +3339,6 @@ files = [ {file = "greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0"}, {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0"}, {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f"}, - {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0"}, - {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d"}, {file = "greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02"}, {file = "greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31"}, {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945"}, @@ -3342,8 +3348,6 @@ files = [ {file = "greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671"}, {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b"}, {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae"}, - {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b"}, - {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929"}, {file = "greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b"}, {file = "greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f"}, @@ -3351,8 +3355,6 @@ files = [ {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337"}, - {file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269"}, - {file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681"}, {file = "greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01"}, {file = "greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c"}, {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d"}, @@ -3362,8 +3364,6 @@ files = [ {file = "greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df"}, {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594"}, {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98"}, - {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28a3c6b7cd72a96f61b0e4b2a36f681025b60ae4779cc73c1535eb5f29560b10"}, - {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52206cd642670b0b320a1fd1cbfd95bca0e043179c1d8a045f2c6109dfe973be"}, {file = "greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b"}, {file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"}, {file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"}, @@ -4413,8 +4413,6 @@ groups = ["main"] markers = "extra == \"dlt\"" files = [ {file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"}, - {file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"}, - {file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"}, ] [package.dependencies] @@ -5688,11 +5686,8 @@ files = [ {file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"}, {file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"}, {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"}, - {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f7f991a68d20c75cb13c5c9142b2a3f9eb161f1f12a9489c82172d1f133c0"}, {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"}, - {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:ac7ba71f9561cd7d7b55e1ea5511543c0282e2b6450f122672a2694621d63b7e"}, {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"}, - {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:ce31158630a6ac85bddd6b830cffd46085ff90498b397bd0a259f59d27a12188"}, {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"}, {file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"}, {file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"}, @@ -9343,10 +9338,8 @@ files = [ {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d"}, {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2"}, {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b"}, - {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14"}, {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd"}, {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b"}, - {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152"}, {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e"}, {file = "psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39"}, {file = "psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10"}, @@ -9354,10 +9347,8 @@ files = [ {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4"}, {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7"}, {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee"}, - {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb"}, {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f"}, {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94"}, - {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f"}, {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908"}, {file = "psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03"}, {file = "psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4"}, @@ -9365,10 +9356,8 @@ files = [ {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a"}, {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e"}, {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db"}, - {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757"}, {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3"}, {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a"}, - {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34"}, {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d"}, {file = "psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d"}, {file = "psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c"}, @@ -9376,10 +9365,8 @@ files = [ {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0"}, {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766"}, {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3"}, - {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f"}, {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4"}, {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c"}, - {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60"}, {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1"}, {file = "psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa"}, {file = "psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1"}, @@ -9387,10 +9374,8 @@ files = [ {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5"}, {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8"}, {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c"}, - {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f"}, {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747"}, {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f"}, - {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b"}, {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d"}, {file = "psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316"}, {file = "psycopg2_binary-2.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c"}, @@ -9398,10 +9383,8 @@ files = [ {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8"}, {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49"}, {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf"}, - {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e"}, {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819"}, {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855"}, - {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d"}, {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4"}, {file = "psycopg2_binary-2.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02"}, ] @@ -10692,13 +10675,6 @@ optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, - {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, - {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, - {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, @@ -14468,4 +14444,4 @@ scraping = ["APScheduler", "beautifulsoup4", "lxml", "lxml", "playwright", "prot [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.14" -content-hash = "6c8f26955a23ff510ddd0ba4eac4046fb2738e8b5787c5eb3b7abca91fec6905" +content-hash = "09f7040236a62a2d610e79e92394bb0c23e13ed41ba4de92c064ab4d5430b84e" diff --git a/pyproject.toml b/pyproject.toml index f6962a1dd..8e4ed8a0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,7 @@ dependencies = [ "tenacity>=9.0.0", "fakeredis[lua]>=2.32.0", "diskcache>=5.6.3", + "aiolimiter>=1.2.1", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index 91aabed83..fccab8c40 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10, <3.14" resolution-markers = [ "python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", @@ -187,6 +187,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" }, ] +[[package]] +name = "aiolimiter" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/23/b52debf471f7a1e42e362d959a3982bdcb4fe13a5d46e63d28868807a79c/aiolimiter-1.2.1.tar.gz", hash = "sha256:e02a37ea1a855d9e832252a105420ad4d15011505512a1a1d814647451b5cca9", size = 7185, upload-time = "2024-12-08T15:31:51.496Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/ba/df6e8e1045aebc4778d19b8a3a9bc1808adb1619ba94ca354d9ba17d86c3/aiolimiter-1.2.1-py3-none-any.whl", hash = "sha256:d3f249e9059a20badcb56b61601a83556133655c11d1eb3dd3e04ff069e5f3c7", size = 6711, upload-time = "2024-12-08T15:31:49.874Z" }, +] + [[package]] name = "aiosignal" version = "1.4.0" @@ -942,6 +951,7 @@ source = { editable = "." } dependencies = [ { name = "aiofiles" }, { name = "aiohttp" }, + { name = "aiolimiter" }, { name = "aiosqlite" }, { name = "alembic" }, { name = "diskcache" }, @@ -1113,6 +1123,7 @@ scraping = [ requires-dist = [ { name = "aiofiles", specifier = ">=23.2.1" }, { name = "aiohttp", specifier = ">=3.11.14,<4.0.0" }, + { name = "aiolimiter", specifier = ">=1.2.1" }, { name = "aiosqlite", specifier = ">=0.20.0,<1.0.0" }, { name = "alembic", specifier = ">=1.13.3,<2" }, { name = "anthropic", marker = "extra == 'anthropic'", specifier = ">=0.27" },