feat: Add new tools and configurations for Castle platform
- Introduced `uv.lock` for dependency management with various packages including `pytest`, `colorama`, and `pluggy`. - Added `pyrightconfig.json` for Python type checking configuration. - Expanded `recommendations.md` with detailed scaling recommendations and project management strategies. - Created shared `ruff.toml` for consistent linting across projects. - Developed `Castle Tools` with various utilities including Android backup, browser automation, document conversion, and search tools. - Implemented `backup-collect` and `schedule` tools for system administration tasks. - Enhanced `search` functionality with indexing and querying capabilities using Tantivy. - Added comprehensive documentation for each tool, including usage examples and installation instructions.
This commit is contained in:
121
event-bus/src/event_bus/bus.py
Normal file
121
event-bus/src/event_bus/bus.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Event bus core — in-memory subscription table and HTTP fan-out delivery."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Subscription:
|
||||
"""A subscription to a topic."""
|
||||
|
||||
topic: str
|
||||
callback_url: str
|
||||
subscriber: str = "" # optional label for debugging
|
||||
|
||||
|
||||
@dataclass
|
||||
class EventBus:
|
||||
"""In-memory event bus with HTTP fan-out delivery."""
|
||||
|
||||
subscriptions: dict[str, list[Subscription]] = field(default_factory=dict)
|
||||
_client: httpx.AsyncClient | None = field(default=None, repr=False)
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Initialize the HTTP client."""
|
||||
self._client = httpx.AsyncClient(timeout=10.0)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Close the HTTP client."""
|
||||
if self._client:
|
||||
await self._client.aclose()
|
||||
self._client = None
|
||||
|
||||
def subscribe(self, topic: str, callback_url: str, subscriber: str = "") -> None:
|
||||
"""Register a subscription to a topic."""
|
||||
if topic not in self.subscriptions:
|
||||
self.subscriptions[topic] = []
|
||||
|
||||
# Don't duplicate
|
||||
for sub in self.subscriptions[topic]:
|
||||
if sub.callback_url == callback_url:
|
||||
return
|
||||
|
||||
self.subscriptions[topic].append(
|
||||
Subscription(topic=topic, callback_url=callback_url, subscriber=subscriber)
|
||||
)
|
||||
logger.info("Subscribed %s to topic '%s' -> %s", subscriber, topic, callback_url)
|
||||
|
||||
def unsubscribe(self, topic: str, callback_url: str) -> bool:
|
||||
"""Remove a subscription. Returns True if found and removed."""
|
||||
if topic not in self.subscriptions:
|
||||
return False
|
||||
|
||||
before = len(self.subscriptions[topic])
|
||||
self.subscriptions[topic] = [
|
||||
s for s in self.subscriptions[topic] if s.callback_url != callback_url
|
||||
]
|
||||
removed = len(self.subscriptions[topic]) < before
|
||||
|
||||
if not self.subscriptions[topic]:
|
||||
del self.subscriptions[topic]
|
||||
|
||||
return removed
|
||||
|
||||
async def publish(self, topic: str, payload: dict) -> int:
|
||||
"""Publish an event to all subscribers. Returns number of subscribers notified."""
|
||||
subscribers = self.subscriptions.get(topic, [])
|
||||
if not subscribers:
|
||||
return 0
|
||||
|
||||
event = {
|
||||
"topic": topic,
|
||||
"payload": payload,
|
||||
"published_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
# Fan out to all subscribers concurrently, fire-and-forget style
|
||||
tasks = [self._deliver(sub, event) for sub in subscribers]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
delivered = sum(1 for r in results if r is True)
|
||||
return delivered
|
||||
|
||||
async def _deliver(self, sub: Subscription, event: dict) -> bool:
|
||||
"""Deliver an event to a single subscriber."""
|
||||
if not self._client:
|
||||
logger.error("HTTP client not initialized")
|
||||
return False
|
||||
|
||||
try:
|
||||
response = await self._client.post(sub.callback_url, json=event)
|
||||
if response.status_code < 300:
|
||||
return True
|
||||
logger.warning(
|
||||
"Delivery to %s returned %d", sub.callback_url, response.status_code
|
||||
)
|
||||
return False
|
||||
except Exception:
|
||||
logger.warning("Delivery to %s failed", sub.callback_url, exc_info=True)
|
||||
return False
|
||||
|
||||
def list_topics(self) -> dict[str, list[dict]]:
|
||||
"""List all topics and their subscribers."""
|
||||
return {
|
||||
topic: [
|
||||
{"callback_url": s.callback_url, "subscriber": s.subscriber}
|
||||
for s in subs
|
||||
]
|
||||
for topic, subs in self.subscriptions.items()
|
||||
}
|
||||
|
||||
|
||||
# Singleton instance
|
||||
bus = EventBus()
|
||||
Reference in New Issue
Block a user