feat(standards): codify codegen, ship generic Tier-B infra, add get_app_template + prompts; fix docs:// regressions
P0: fix doc:// -> docs:// across standards src/server/registry and regenerate stale mongo-ops bundle. P1: build recipe + spec-first routing now document the openapi-first codegen fast path (models/routes with --inject-request/--package). P4: CRUD template documents the model-vs-document id rule (BaseDocument owns id). P5: authless guidance (omit security keys). P6: template gains generic infra (http_cache, keyed SSE bus, KV port+in-memory+Redis), interfaces/openapi.py + .dockerignore + generic build test registered as components, and a get_app_template tool; genericity matrix marks LLM pool + human-in-loop as reference-app patterns. P7: intake/build/start_app/trial prompts. P8: template ships tests/integration/test_openapi_app_builds.py. Rebuilt standards + openapi-first bundles and re-collected.
This commit is contained in:
147
standards/mcp/templates/app/infra/adapters/kv.py
Normal file
147
standards/mcp/templates/app/infra/adapters/kv.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""# Summary
|
||||
|
||||
Key-value store port with in-memory and Redis adaptors.
|
||||
|
||||
---
|
||||
|
||||
Reserve infrastructure for caching small values by key (sessions, locks,
|
||||
result blobs, rate counters). The in-memory adaptor needs no services and is a
|
||||
safe default; the Redis adaptor is opt-in and imports ``redis`` lazily so it
|
||||
never becomes a hard dependency.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from infra.adapters.kv import InMemoryKV
|
||||
|
||||
kv = InMemoryKV()
|
||||
await kv.set("greeting", "hello", ttl=30)
|
||||
assert await kv.get("greeting") == "hello"
|
||||
```
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
class KVStore(ABC):
|
||||
"""Abstract key-value store port."""
|
||||
|
||||
@abstractmethod
|
||||
async def get(self, key: str) -> Optional[str]:
|
||||
"""Return the value at ``key``, or ``None``."""
|
||||
|
||||
@abstractmethod
|
||||
async def set(self, key: str, value: str, ttl: Optional[int] = None) -> None:
|
||||
"""Store ``value`` at ``key`` with an optional TTL in seconds."""
|
||||
|
||||
@abstractmethod
|
||||
async def delete(self, key: str) -> None:
|
||||
"""Delete ``key`` if present."""
|
||||
|
||||
@abstractmethod
|
||||
async def scan(self, pattern: str) -> List[str]:
|
||||
"""Return keys matching a glob ``pattern``."""
|
||||
|
||||
async def clear(self, pattern: str = "*") -> None:
|
||||
"""Delete all keys matching ``pattern``.
|
||||
|
||||
Args:
|
||||
pattern (str): Glob-style key pattern.
|
||||
"""
|
||||
for key in await self.scan(pattern):
|
||||
await self.delete(key)
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Release any underlying resources. Default is a no-op."""
|
||||
|
||||
|
||||
class InMemoryKV(KVStore):
|
||||
"""Process-local key-value store used as a dependency-free fallback.
|
||||
|
||||
Attributes:
|
||||
_data (Dict[str, str]): In-memory value map.
|
||||
_expiry (Dict[str, float]): Absolute expiry timestamps by key; ``0.0``
|
||||
means "no expiry".
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize an empty store with no imports or connections."""
|
||||
self._data: Dict[str, str] = {}
|
||||
self._expiry: Dict[str, float] = {}
|
||||
|
||||
def _expired(self, key: str) -> bool:
|
||||
import time
|
||||
|
||||
deadline = self._expiry.get(key, 0.0)
|
||||
if deadline and deadline < time.time():
|
||||
self._data.pop(key, None)
|
||||
self._expiry.pop(key, None)
|
||||
return True
|
||||
return False
|
||||
|
||||
async def get(self, key: str) -> Optional[str]:
|
||||
"""Return the value at ``key``, or ``None`` when absent/expired."""
|
||||
if self._expired(key):
|
||||
return None
|
||||
return self._data.get(key)
|
||||
|
||||
async def set(self, key: str, value: str, ttl: Optional[int] = None) -> None:
|
||||
"""Store ``value`` at ``key`` with an optional TTL in seconds."""
|
||||
import time
|
||||
|
||||
self._data[key] = value
|
||||
self._expiry[key] = (time.time() + ttl) if ttl else 0.0
|
||||
|
||||
async def delete(self, key: str) -> None:
|
||||
"""Delete ``key`` if present."""
|
||||
self._data.pop(key, None)
|
||||
self._expiry.pop(key, None)
|
||||
|
||||
async def scan(self, pattern: str) -> List[str]:
|
||||
"""Return live keys matching a glob ``pattern``."""
|
||||
import fnmatch
|
||||
|
||||
return [k for k in list(self._data) if fnmatch.fnmatch(k, pattern) and not self._expired(k)]
|
||||
|
||||
|
||||
class RedisKV(KVStore):
|
||||
"""Redis-backed key-value store.
|
||||
|
||||
Attributes:
|
||||
_redis: Asynchronous Redis client (imported lazily).
|
||||
"""
|
||||
|
||||
def __init__(self, url: str = "redis://localhost:6379/0", decode_responses: bool = True):
|
||||
"""Initialize the store with a Redis connection.
|
||||
|
||||
Args:
|
||||
url (str): Redis connection URL.
|
||||
decode_responses (bool): Whether Redis returns decoded strings.
|
||||
|
||||
Raises:
|
||||
ImportError: When the optional ``redis`` package is not installed.
|
||||
"""
|
||||
from redis.asyncio import Redis
|
||||
|
||||
self._redis = Redis.from_url(url, decode_responses=decode_responses)
|
||||
|
||||
async def get(self, key: str) -> Optional[str]:
|
||||
"""Return the value at ``key``, or ``None``."""
|
||||
return await self._redis.get(key)
|
||||
|
||||
async def set(self, key: str, value: str, ttl: Optional[int] = None) -> None:
|
||||
"""Store ``value`` at ``key`` with an optional TTL in seconds."""
|
||||
await self._redis.set(key, value, ex=ttl)
|
||||
|
||||
async def delete(self, key: str) -> None:
|
||||
"""Delete ``key`` if present."""
|
||||
await self._redis.delete(key)
|
||||
|
||||
async def scan(self, pattern: str) -> List[str]:
|
||||
"""Return keys matching a glob ``pattern``."""
|
||||
return [key async for key in self._redis.scan_iter(match=pattern)]
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close the underlying Redis connection."""
|
||||
await self._redis.aclose()
|
||||
54
standards/mcp/templates/app/infra/adapters/sse.py
Normal file
54
standards/mcp/templates/app/infra/adapters/sse.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""# Summary
|
||||
|
||||
SSEInMemoryBus — in-memory event bus for SSE progress events.
|
||||
|
||||
---
|
||||
|
||||
Buffer published progress events so they can be replayed (and streamed) per
|
||||
correlation key, such as a job, upload, or fetch-request id.
|
||||
"""
|
||||
from typing import Generic, List, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class SSEInMemoryBus(Generic[T]):
|
||||
"""Buffer SSE progress events in memory, keyed by an event attribute.
|
||||
|
||||
Attributes:
|
||||
_events (List[T]): Events published since the last clear.
|
||||
_key_attr (str): Event attribute used to correlate a stream.
|
||||
"""
|
||||
|
||||
def __init__(self, key_attr: str = "id"):
|
||||
"""Initialize an empty event buffer.
|
||||
|
||||
Args:
|
||||
key_attr (str): Attribute name on each event holding the stream
|
||||
correlation key (for example ``"job_id"``).
|
||||
"""
|
||||
self._events: List[T] = []
|
||||
self._key_attr = key_attr
|
||||
|
||||
async def publish(self, event: T) -> None:
|
||||
"""Append an event to the buffer.
|
||||
|
||||
Args:
|
||||
event (T): Event to append.
|
||||
"""
|
||||
self._events.append(event)
|
||||
|
||||
def get_events(self, key: str) -> List[T]:
|
||||
"""Return buffered events for a correlation key.
|
||||
|
||||
Args:
|
||||
key (str): Value matched against the configured ``key_attr``.
|
||||
|
||||
Returns:
|
||||
List[T]: Events whose ``key_attr`` equals ``key``.
|
||||
"""
|
||||
return [e for e in self._events if getattr(e, self._key_attr, None) == key]
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Remove all buffered events."""
|
||||
self._events.clear()
|
||||
Reference in New Issue
Block a user