- build.py: _TEMPLATE_COMPONENTS rewritten around the vet sample (53 kinds —
per-resource slices, jobs, worked appointment_reminder family, tests);
seed tokens dropped from the manifest ('sample: vet' instead)
- registry.py: get_app_component(kind) no longer renames item/example seeds;
prompts and tool docs updated to 'copy a slice and adapt'
- templates/README.md, 01_build_recipe, 40_ui_admin, wiki/03 updated off the
old seed scheme; repo inventory/config now exclude vet (docs move under
standards; react/vet itself is deleted in a later phase)
- regenerated MCP bundle, vendored templates, and site output
89 lines
2.9 KiB
Python
89 lines
2.9 KiB
Python
"""# Summary
|
|
|
|
Species-scoped action stream — the SSE event source behind ``stream_actions``.
|
|
|
|
---
|
|
|
|
Every pet gets a background asyncio task that emits random, species-appropriate
|
|
behaviors (a wagging dog, a kneading cat, a chirping bird) onto per-subscriber
|
|
queues. The HTTP handler subscribes one queue per connected client and
|
|
unsubscribes when the response closes.
|
|
|
|
Notes:
|
|
Process-local and best-effort — the stream is generated, not persisted,
|
|
and is intentionally independent of the persistence backend.
|
|
"""
|
|
import asyncio
|
|
import json
|
|
import random
|
|
|
|
_BEHAVIORS_BY_SPECIES: dict[str, list[str]] = {
|
|
"dog": ["wagging_tail", "stretching", "showing_belly", "barking", "panting"],
|
|
"cat": ["blep", "stretching", "showing_belly", "purring", "kneading"],
|
|
"bird": ["head_tilt", "stretching", "chirping", "wing_flap"],
|
|
}
|
|
|
|
_subscribers: dict[int, list[asyncio.Queue]] = {}
|
|
_worker_tasks: dict[int, asyncio.Task] = {}
|
|
|
|
|
|
async def _behavior_worker(pet_id: int, species: str) -> None:
|
|
"""Emit random behaviors for one pet until cancelled.
|
|
|
|
Args:
|
|
pet_id (int): Pet whose subscribers receive the events.
|
|
species (str): Animal species selecting the behavior pool.
|
|
"""
|
|
behaviors = _BEHAVIORS_BY_SPECIES.get(species, ["wagging_tail"])
|
|
while True:
|
|
action = random.choice(behaviors)
|
|
data = json.dumps({"action": action})
|
|
for q in _subscribers.get(pet_id, []):
|
|
await q.put(data)
|
|
await asyncio.sleep(random.uniform(1, 5))
|
|
|
|
|
|
def _ensure_worker(pet_id: int, species: str) -> None:
|
|
"""Start the behavior worker for ``pet_id`` if it is not running.
|
|
|
|
Args:
|
|
pet_id (int): Pet identifier.
|
|
species (str): Animal species selecting the behavior pool.
|
|
"""
|
|
if pet_id not in _worker_tasks or _worker_tasks[pet_id].done():
|
|
_subscribers.setdefault(pet_id, [])
|
|
_worker_tasks[pet_id] = asyncio.create_task(_behavior_worker(pet_id, species))
|
|
|
|
|
|
async def subscribe(pet_id: int, species: str) -> asyncio.Queue:
|
|
"""Subscribe a new listener queue for a pet's action stream.
|
|
|
|
Args:
|
|
pet_id (int): Pet identifier.
|
|
species (str): Animal species selecting the behavior pool.
|
|
|
|
Returns:
|
|
asyncio.Queue: Queue that receives JSON-encoded action payloads.
|
|
"""
|
|
q: asyncio.Queue = asyncio.Queue()
|
|
_subscribers.setdefault(pet_id, []).append(q)
|
|
_ensure_worker(pet_id, species)
|
|
return q
|
|
|
|
|
|
def unsubscribe(pet_id: int, q: asyncio.Queue) -> None:
|
|
"""Remove a listener queue and stop the worker when the last one leaves.
|
|
|
|
Args:
|
|
pet_id (int): Pet identifier.
|
|
q (asyncio.Queue): Queue returned by :func:`subscribe`.
|
|
"""
|
|
queues = _subscribers.get(pet_id, [])
|
|
if q in queues:
|
|
queues.remove(q)
|
|
if not queues:
|
|
task = _worker_tasks.pop(pet_id, None)
|
|
if task and not task.done():
|
|
task.cancel()
|
|
_subscribers.pop(pet_id, None)
|