Skip to content
Merged
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
15 changes: 15 additions & 0 deletions api/adapters/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Adapter Layer (Strangler Pattern)

This package holds backward-compatibility shims while god-object modules are split into focused services.

## Rules
- New call sites should import extracted modules directly.
- Legacy entry points can call extracted modules through adapter wrappers.
- Use `deprecated_in_favor_of(...)` to log old API usage during migration.
- Remove adapter modules only after all call sites are migrated and validated.

## Typical Migration Flow
1. Extract new module and add unit tests.
2. Update legacy module to delegate through adapter.
3. Migrate callers incrementally.
4. Remove adapter when usage drops to zero.
39 changes: 39 additions & 0 deletions api/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Compatibility adapters used during strangler-pattern refactors."""

from __future__ import annotations

from collections.abc import Callable
from functools import wraps
from typing import TypeVar, ParamSpec

from logging_config import logger

P = ParamSpec("P")
R = TypeVar("R")


def deprecated_in_favor_of(new_module: str) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""Log calls to legacy APIs that are being replaced.

Parameters
----------
new_module:
The replacement module path or API identifier.
"""

def decorator(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
logger.warning(
"deprecated_api_called",
old_api=f"{func.__module__}.{func.__name__}",
replacement=new_module,
)
return func(*args, **kwargs)

return wrapper

return decorator


__all__ = ["deprecated_in_favor_of"]
35 changes: 23 additions & 12 deletions api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,19 @@ def invalidate_pro_cache(user_id: str) -> None:
return
with _PRO_STATE_CACHE_LOCK:
_PRO_STATE_CACHE.pop(user_id, None)
async def _clear() -> None:
redis = await safe_redis_call(get_redis, operation="connect")
if redis is None:
return
await safe_redis_call(redis.delete, f"knowbear:user:is_pro:{user_id}", operation="delete")

try:
async def _clear() -> None:
redis = await safe_redis_call(get_redis, operation="connect")
if redis is None:
return
await safe_redis_call(redis.delete, f"knowbear:user:is_pro:{user_id}", operation="delete")
asyncio.create_task(_clear())
except Exception:
task_coro = _clear()
asyncio.create_task(task_coro)
except Exception as exc:
task_coro.close()
# Best-effort cache invalidation only.
pass
logger.debug("auth_invalidate_pro_cache_failed", user_id_hash=anonymize_user_id(user_id), error=str(exc))

@lru_cache(maxsize=1)
def get_supabase() -> Client | None:
Expand Down Expand Up @@ -287,8 +290,12 @@ async def check_is_pro(user_id: str, force_refresh: bool = False) -> bool:
_PRO_STATE_CACHE.move_to_end(user_id)
_prune_pro_cache_locked(now)
return is_pro
except Exception:
pass
except Exception as exc:
logger.debug(
"auth_pro_cache_redis_read_failed",
user_id_hash=anonymize_user_id(user_id),
error=str(exc),
)

supabase = get_supabase_admin()
if not supabase:
Expand All @@ -312,8 +319,12 @@ async def check_is_pro(user_id: str, force_refresh: bool = False) -> bool:
"1" if is_pro else "0",
operation="setex",
)
except Exception:
pass
except Exception as exc:
logger.debug(
"auth_pro_cache_redis_write_failed",
user_id_hash=anonymize_user_id(user_id),
error=str(exc),
)
with _PRO_STATE_CACHE_LOCK:
_prune_pro_cache_locked(now)
_PRO_STATE_CACHE[user_id] = (is_pro, now + _pro_cache_ttl_seconds())
Expand Down
15 changes: 15 additions & 0 deletions api/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Centralized constants for quick-win technical debt cleanup."""

REDIS_REST_CALL_TIMEOUT_SECONDS = 0.8
UPSTASH_HTTP_TIMEOUT_SECONDS = 1.5
UPSTASH_HTTP_CONNECT_TIMEOUT_SECONDS = 0.75

MESSAGE_GATE_DEFAULT_TIMEOUT_SECONDS = 0.8
STREAM_IDEMPOTENCY_TTL_MIN_SECONDS = 60
STREAM_IDEMPOTENCY_TTL_MAX_SECONDS = 120
STREAM_IDEMPOTENCY_STALE_MIN_SECONDS = 5

RATE_LIMIT_HOURLY_WINDOW_MINUTES = 60
RATE_LIMIT_HOURLY_WINDOW_SECONDS = RATE_LIMIT_HOURLY_WINDOW_MINUTES * 60

PROVIDER_USAGE_TTL_SECONDS = 86400
3 changes: 0 additions & 3 deletions api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ supabase>=2.3.4
tenacity>=8.2.3
orjson>=3.9.13
structlog>=24.1.0
fastapi-limiter>=0.1.6
markdown>=3.5.2
openai>=1.51.0
sentry-sdk[fastapi]>=2.20.0
slowapi>=0.1.9
dodopayments[webhooks]>=1.92.0,<2
standardwebhooks>=1.0.0
tiktoken>=0.7.0
Loading
Loading