Added pydocstyle (D) rules to Ruff and fixed all violations.
CI / Formatting (push) Successful in 12s
CI / Linting (push) Successful in 13s
CI / Tests (Python 3.12) (push) Successful in 26s
CI / Tests (Python 3.13) (push) Successful in 25s
CI / Tests (Python 3.14) (push) Successful in 25s
CI / Type Checking (push) Successful in 26s
CI / Formatting (push) Successful in 12s
CI / Linting (push) Successful in 13s
CI / Tests (Python 3.12) (push) Successful in 26s
CI / Tests (Python 3.13) (push) Successful in 25s
CI / Tests (Python 3.14) (push) Successful in 25s
CI / Type Checking (push) Successful in 26s
This commit is contained in:
+1
-2
@@ -12,8 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Owlbot - A modular chat bot for Owncast.
|
||||
"""Owlbot - A modular chat bot for Owncast.
|
||||
|
||||
The public API for building modules is in the `owlbot.api` subpackage:
|
||||
|
||||
|
||||
@@ -83,8 +83,7 @@ def on_command(
|
||||
requires_moderator: bool = False,
|
||||
cooldown: int | float = 0,
|
||||
) -> Callable[[CommandHandler], CommandHandler]:
|
||||
"""
|
||||
Decorator to register a command handler.
|
||||
"""Register a command handler.
|
||||
|
||||
:param name: Primary command name (case-insensitive).
|
||||
:param aliases: Optional list of alternative names.
|
||||
|
||||
+19
-35
@@ -36,8 +36,7 @@ class Config:
|
||||
config_path: str | Path = "config.yaml",
|
||||
overrides: dict[str, Any] | None = None,
|
||||
):
|
||||
"""
|
||||
Initialize the configuration manager.
|
||||
"""Initialize the configuration manager.
|
||||
|
||||
:param config_path: Path to the YAML config file.
|
||||
:param overrides: CLI overrides (keys match property names).
|
||||
@@ -91,9 +90,9 @@ class Config:
|
||||
default: Any = _UNSET,
|
||||
type_fn: type = str,
|
||||
) -> Any:
|
||||
"""
|
||||
Resolve a setting through the priority chain:
|
||||
CLI arg > env var > config file > default.
|
||||
"""Resolve a setting through the priority chain.
|
||||
|
||||
Priority order: CLI arg > env var > config file > default.
|
||||
|
||||
When *default* is a ``str``, the resolved value is coerced to ``str``
|
||||
(the implicit *type_fn*). For non-string types, pass both *default*
|
||||
@@ -123,8 +122,7 @@ class Config:
|
||||
|
||||
@property
|
||||
def webhook_secret(self) -> str:
|
||||
"""
|
||||
Secret string for the webhook URL path.
|
||||
"""Secret string for the webhook URL path.
|
||||
|
||||
The webhook endpoint is always /webhook/<secret>. A cryptographically
|
||||
secure value is generated on first run if not explicitly configured.
|
||||
@@ -139,8 +137,7 @@ class Config:
|
||||
|
||||
@property
|
||||
def webhook_path(self) -> str:
|
||||
"""
|
||||
URL path where Owncast sends webhooks.
|
||||
"""URL path where Owncast sends webhooks.
|
||||
|
||||
Always returns /webhook/<secret>. The secret is auto-generated
|
||||
on first run if not configured.
|
||||
@@ -207,8 +204,7 @@ class Config:
|
||||
|
||||
@property
|
||||
def public_base_url(self) -> str:
|
||||
"""
|
||||
Public base URL for Owlbot's web server.
|
||||
"""Public base URL for Owlbot's web server.
|
||||
|
||||
Used to construct URLs for module routes and the webhook endpoint.
|
||||
Falls back to ``owncast.url`` if not explicitly set.
|
||||
@@ -224,8 +220,7 @@ class Config:
|
||||
|
||||
@property
|
||||
def storage_dir(self) -> Path:
|
||||
"""
|
||||
Directory for module database files.
|
||||
"""Directory for module database files.
|
||||
|
||||
Each module gets its own database file named '<module_name>.db'.
|
||||
Defaults to 'data/' in the working directory.
|
||||
@@ -242,8 +237,7 @@ class Config:
|
||||
|
||||
@property
|
||||
def modules_dir(self) -> Path:
|
||||
"""
|
||||
Directory containing user modules.
|
||||
"""Directory containing user modules.
|
||||
|
||||
Built-in modules are loaded from the package regardless of this setting.
|
||||
Defaults to 'modules/' in the working directory.
|
||||
@@ -260,8 +254,7 @@ class Config:
|
||||
|
||||
@property
|
||||
def log_dir(self) -> Path | None:
|
||||
"""
|
||||
Directory for the log file.
|
||||
"""Directory for the log file.
|
||||
|
||||
When set, an ``owlbot.log`` file is written to this directory in
|
||||
addition to stdout. Returns ``None`` when unset (stdout only).
|
||||
@@ -405,8 +398,7 @@ class Config:
|
||||
logger.info(f"Configuration saved to: {self.config_path.absolute()}")
|
||||
|
||||
def is_module_enabled(self, module_name: str) -> bool:
|
||||
"""
|
||||
Check if a module is enabled.
|
||||
"""Check if a module is enabled.
|
||||
|
||||
Modules are enabled by default unless explicitly disabled with
|
||||
``modules.<name>.enabled: false`` in the config file.
|
||||
@@ -424,8 +416,7 @@ class Config:
|
||||
return True
|
||||
|
||||
def get_module_config(self, module_name: str) -> dict[str, Any]:
|
||||
"""
|
||||
Get the configuration dict for a module.
|
||||
"""Get the configuration dict for a module.
|
||||
|
||||
Returns merged defaults and config file values, with config file
|
||||
values taking precedence.
|
||||
@@ -443,8 +434,7 @@ class Config:
|
||||
return {**defaults, **config}
|
||||
|
||||
def set_module_config(self, module_name: str, config: dict[str, Any]) -> None:
|
||||
"""
|
||||
Update the configuration for a module at runtime and persist to disk.
|
||||
"""Update the configuration for a module at runtime and persist to disk.
|
||||
|
||||
:param module_name: The module name.
|
||||
:param config: Dict of configuration values to set.
|
||||
@@ -459,8 +449,7 @@ class Config:
|
||||
def register_module_defaults(
|
||||
self, module_name: str, defaults: dict[str, Any]
|
||||
) -> None:
|
||||
"""
|
||||
Register default configuration values for a module.
|
||||
"""Register default configuration values for a module.
|
||||
|
||||
Called by modules during setup to declare their expected config keys
|
||||
and default values. Missing keys are backfilled into the config file
|
||||
@@ -516,8 +505,7 @@ class ModuleConfig:
|
||||
"""Pre-scoped configuration for a specific module."""
|
||||
|
||||
def __init__(self, config: Config, module_name: str):
|
||||
"""
|
||||
Initialize a module-scoped configuration.
|
||||
"""Initialize a module-scoped configuration.
|
||||
|
||||
:param config: The parent Config object.
|
||||
:param module_name: The name of the module this config
|
||||
@@ -537,8 +525,7 @@ class ModuleConfig:
|
||||
return self._config.public_base_url
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Get a config value by key.
|
||||
"""Get a config value by key.
|
||||
|
||||
:param key: The configuration key.
|
||||
:param default: Value to return if key is not found.
|
||||
@@ -547,16 +534,14 @@ class ModuleConfig:
|
||||
return self.as_dict().get(key, default)
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""
|
||||
Get the full config dict for this module.
|
||||
"""Get the full config dict for this module.
|
||||
|
||||
:return: Dict of all configuration values.
|
||||
"""
|
||||
return self._config.get_module_config(self._module_name)
|
||||
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
"""
|
||||
Set a config value at runtime and persist to disk.
|
||||
"""Set a config value at runtime and persist to disk.
|
||||
|
||||
:param key: The configuration key.
|
||||
:param value: The value to set.
|
||||
@@ -566,8 +551,7 @@ class ModuleConfig:
|
||||
self._config.set_module_config(self._module_name, current)
|
||||
|
||||
def register_defaults(self, defaults: dict[str, Any]) -> None:
|
||||
"""
|
||||
Register default values for this module's config.
|
||||
"""Register default values for this module's config.
|
||||
|
||||
Called during setup() to declare expected config keys and their
|
||||
default values.
|
||||
|
||||
+7
-12
@@ -44,8 +44,7 @@ if TYPE_CHECKING:
|
||||
|
||||
@dataclass
|
||||
class ModuleContext:
|
||||
"""
|
||||
Shared services available to all module handlers.
|
||||
"""Shared services available to all module handlers.
|
||||
|
||||
Created once per module during loading and reused for all handler
|
||||
invocations. This bundles the common dependencies that every handler needs.
|
||||
@@ -83,6 +82,7 @@ class ModuleContext:
|
||||
logger: logging.Logger = field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Derive the module-scoped logger from the module name."""
|
||||
self.logger = logging.getLogger(f"owlbot.modules.{self.module_name}")
|
||||
|
||||
|
||||
@@ -96,8 +96,7 @@ class PropagationState:
|
||||
|
||||
@dataclass
|
||||
class EventContext[E]:
|
||||
"""
|
||||
Context passed to event handlers.
|
||||
"""Context passed to event handlers.
|
||||
|
||||
Each handler invocation receives its own EventContext instance with the event
|
||||
data and access to shared services via the module context.
|
||||
@@ -165,16 +164,14 @@ class EventContext[E]:
|
||||
|
||||
@property
|
||||
def propagation_stopped(self) -> bool:
|
||||
"""
|
||||
Check if event propagation has been stopped by a handler.
|
||||
"""Check if event propagation has been stopped by a handler.
|
||||
|
||||
:return: True if stop_propagation() was called by any handler.
|
||||
"""
|
||||
return self._propagation.stopped
|
||||
|
||||
def stop_propagation(self, reason: str | None = None) -> None:
|
||||
"""
|
||||
Stop event from being dispatched to remaining handlers and commands.
|
||||
"""Stop event from being dispatched to remaining handlers and commands.
|
||||
|
||||
Once called, no further handlers will be invoked for this event, and
|
||||
command dispatch (for CHAT events) will be skipped.
|
||||
@@ -189,8 +186,7 @@ class EventContext[E]:
|
||||
|
||||
@dataclass
|
||||
class CommandContext:
|
||||
"""
|
||||
Context passed to command handlers.
|
||||
"""Context passed to command handlers.
|
||||
|
||||
Provides access to the parsed command data, original chat event context,
|
||||
and shared services. Like EventContext and RouteContext, all ModuleContext
|
||||
@@ -289,8 +285,7 @@ class CommandContext:
|
||||
|
||||
@dataclass
|
||||
class RouteContext:
|
||||
"""
|
||||
Context passed to HTTP route handlers.
|
||||
"""Context passed to HTTP route handlers.
|
||||
|
||||
Similar to EventContext but includes the aiohttp request object
|
||||
for accessing HTTP-specific data (body, headers, query params).
|
||||
|
||||
+14
-28
@@ -43,8 +43,7 @@ class EventType(StrEnum):
|
||||
|
||||
|
||||
def _parse_timestamp(ts: str | None) -> datetime | None:
|
||||
"""
|
||||
Parse an ISO 8601 timestamp string into a datetime.
|
||||
"""Parse an ISO 8601 timestamp string into a datetime.
|
||||
|
||||
Handles timezone suffixes and the Go zero-value timestamp that Owncast
|
||||
sends for missing/unset timestamps.
|
||||
@@ -91,8 +90,7 @@ class User:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> User:
|
||||
"""
|
||||
Create a User from webhook JSON data.
|
||||
"""Create a User from webhook JSON data.
|
||||
|
||||
:param data: The user data from the webhook payload.
|
||||
:return: A populated User instance.
|
||||
@@ -111,8 +109,7 @@ class User:
|
||||
|
||||
@property
|
||||
def is_moderator(self) -> bool:
|
||||
"""
|
||||
Check if the user has moderator privileges.
|
||||
"""Check if the user has moderator privileges.
|
||||
|
||||
:return: True if the user has the MODERATOR scope.
|
||||
"""
|
||||
@@ -133,8 +130,7 @@ class ChatEvent:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> ChatEvent:
|
||||
"""
|
||||
Create a ChatEvent from webhook JSON data.
|
||||
"""Create a ChatEvent from webhook JSON data.
|
||||
|
||||
:param data: The event data from the webhook payload.
|
||||
:return: A populated ChatEvent instance.
|
||||
@@ -168,8 +164,7 @@ class UserJoinedEvent:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> UserJoinedEvent:
|
||||
"""
|
||||
Create a UserJoinedEvent from webhook JSON data.
|
||||
"""Create a UserJoinedEvent from webhook JSON data.
|
||||
|
||||
:param data: The event data from the webhook payload.
|
||||
:return: A populated UserJoinedEvent instance.
|
||||
@@ -193,8 +188,7 @@ class UserPartedEvent:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> UserPartedEvent:
|
||||
"""
|
||||
Create a UserPartedEvent from webhook JSON data.
|
||||
"""Create a UserPartedEvent from webhook JSON data.
|
||||
|
||||
:param data: The event data from the webhook payload.
|
||||
:return: A populated UserPartedEvent instance.
|
||||
@@ -219,8 +213,7 @@ class NameChangedEvent:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> NameChangedEvent:
|
||||
"""
|
||||
Create a NameChangedEvent from webhook JSON data.
|
||||
"""Create a NameChangedEvent from webhook JSON data.
|
||||
|
||||
Note: The user object contains the OLD display name in user.display_name.
|
||||
|
||||
@@ -248,8 +241,7 @@ class StreamStartedEvent:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> StreamStartedEvent:
|
||||
"""
|
||||
Create a StreamStartedEvent from webhook JSON data.
|
||||
"""Create a StreamStartedEvent from webhook JSON data.
|
||||
|
||||
:param data: The event data from the webhook payload.
|
||||
:return: A populated StreamStartedEvent instance.
|
||||
@@ -275,8 +267,7 @@ class StreamStoppedEvent:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> StreamStoppedEvent:
|
||||
"""
|
||||
Create a StreamStoppedEvent from webhook JSON data.
|
||||
"""Create a StreamStoppedEvent from webhook JSON data.
|
||||
|
||||
:param data: The event data from the webhook payload.
|
||||
:return: A populated StreamStoppedEvent instance.
|
||||
@@ -305,8 +296,7 @@ class StreamStatus:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> StreamStatus:
|
||||
"""
|
||||
Create a StreamStatus from webhook JSON data.
|
||||
"""Create a StreamStatus from webhook JSON data.
|
||||
|
||||
:param data: The status data from the webhook payload.
|
||||
:return: A populated StreamStatus instance.
|
||||
@@ -336,8 +326,7 @@ class StreamTitleUpdatedEvent:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> StreamTitleUpdatedEvent:
|
||||
"""
|
||||
Create a StreamTitleUpdatedEvent from webhook JSON data.
|
||||
"""Create a StreamTitleUpdatedEvent from webhook JSON data.
|
||||
|
||||
:param data: The event data from the webhook payload.
|
||||
:return: A populated StreamTitleUpdatedEvent instance.
|
||||
@@ -364,8 +353,7 @@ class VisibilityUpdateEvent:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> VisibilityUpdateEvent:
|
||||
"""
|
||||
Create a VisibilityUpdateEvent from webhook JSON data.
|
||||
"""Create a VisibilityUpdateEvent from webhook JSON data.
|
||||
|
||||
:param data: The event data from the webhook payload.
|
||||
:return: A populated VisibilityUpdateEvent instance.
|
||||
@@ -407,8 +395,7 @@ _EVENT_TYPE_MAP: dict[EventType, type[Event]] = {
|
||||
|
||||
|
||||
def parse_event(data: dict[str, Any]) -> _ParsedEvent | None:
|
||||
"""
|
||||
Parse a raw webhook payload into a typed event.
|
||||
"""Parse a raw webhook payload into a typed event.
|
||||
|
||||
:param data: The JSON payload from Owncast's webhook.
|
||||
:return: Tuple of (event_type, event_instance), or None if the event type
|
||||
@@ -431,8 +418,7 @@ def parse_event(data: dict[str, Any]) -> _ParsedEvent | None:
|
||||
|
||||
|
||||
def log_event(event_type: EventType, event: Event) -> None:
|
||||
"""
|
||||
Log event details for debugging and monitoring.
|
||||
"""Log event details for debugging and monitoring.
|
||||
|
||||
:param event_type: The type of event being logged.
|
||||
:param event: The parsed event instance.
|
||||
|
||||
@@ -43,8 +43,7 @@ type EventHandler = "Callable[[EventContext[Any]], Awaitable[None]]"
|
||||
|
||||
|
||||
class Priority(IntEnum):
|
||||
"""
|
||||
Standard priority levels for event handlers.
|
||||
"""Standard priority levels for event handlers.
|
||||
|
||||
Higher values run first. Handlers at the same priority level run in
|
||||
registration order. Custom numeric values can be used for fine-grained control.
|
||||
@@ -61,8 +60,7 @@ def on_event(
|
||||
*event_types: EventType,
|
||||
priority: int = Priority.NORMAL,
|
||||
) -> Callable[[EventHandler], EventHandler]:
|
||||
"""
|
||||
Decorator to register a function as a handler for one or more event types.
|
||||
"""Register a function as a handler for one or more event types.
|
||||
|
||||
The decorated function will be called whenever an event of the specified
|
||||
type(s) is received. Handlers are executed sequentially in priority order
|
||||
|
||||
@@ -31,8 +31,7 @@ type LifecycleHandler = "Callable[[ModuleContext], Awaitable[None]]"
|
||||
|
||||
|
||||
def on_setup(func: LifecycleHandler) -> LifecycleHandler:
|
||||
"""
|
||||
Decorator to mark a function as a module setup hook.
|
||||
"""Mark a function as a module setup hook.
|
||||
|
||||
The decorated function will be called during module loading with a
|
||||
``ModuleContext``. Setup hooks run inside a storage transaction that
|
||||
@@ -52,8 +51,7 @@ def on_setup(func: LifecycleHandler) -> LifecycleHandler:
|
||||
|
||||
|
||||
def on_teardown(func: LifecycleHandler) -> LifecycleHandler:
|
||||
"""
|
||||
Decorator to mark a function as a module teardown hook.
|
||||
"""Mark a function as a module teardown hook.
|
||||
|
||||
The decorated function will be called during module unload or bot
|
||||
shutdown with the same ``ModuleContext`` from setup.
|
||||
|
||||
@@ -186,8 +186,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
def __init__(
|
||||
self, base_url: str, username: str, password: str, http_client: HttpClient
|
||||
):
|
||||
"""
|
||||
Initialize the admin client.
|
||||
"""Initialize the admin client.
|
||||
|
||||
:param base_url: The Owncast server URL (e.g., "https://stream.logal.dev").
|
||||
:param username: Admin username.
|
||||
@@ -203,24 +202,21 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def get_status(self) -> dict[str, Any]:
|
||||
"""
|
||||
Get the current server status including stream info and viewer count.
|
||||
"""Get the current server status including stream info and viewer count.
|
||||
|
||||
:return: Server status dict.
|
||||
"""
|
||||
return dict(await self._get("/api/admin/status"))
|
||||
|
||||
async def get_active_viewers(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get a list of currently active viewers.
|
||||
"""Get a list of currently active viewers.
|
||||
|
||||
:return: Viewer list as returned by the Owncast API.
|
||||
"""
|
||||
return list(await self._get("/api/admin/viewers"))
|
||||
|
||||
async def get_viewers_over_time(self, window_start: int) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get viewer count data over time for charting.
|
||||
"""Get viewer count data over time for charting.
|
||||
|
||||
:param window_start: Unix timestamp (seconds since epoch)
|
||||
for the start of the window.
|
||||
@@ -233,24 +229,21 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def get_hardware_stats(self) -> dict[str, Any]:
|
||||
"""
|
||||
Get server hardware statistics (CPU, memory, disk).
|
||||
"""Get server hardware statistics (CPU, memory, disk).
|
||||
|
||||
:return: Hardware stats dict.
|
||||
"""
|
||||
return dict(await self._get("/api/admin/hardwarestats"))
|
||||
|
||||
async def get_server_config(self) -> dict[str, Any]:
|
||||
"""
|
||||
Get the full server configuration.
|
||||
"""Get the full server configuration.
|
||||
|
||||
:return: Server configuration dict.
|
||||
"""
|
||||
return dict(await self._get("/api/admin/serverconfig"))
|
||||
|
||||
async def disconnect_stream(self) -> dict[str, Any]:
|
||||
"""
|
||||
Disconnect the current inbound stream.
|
||||
"""Disconnect the current inbound stream.
|
||||
|
||||
:return: API response confirming the disconnect.
|
||||
"""
|
||||
@@ -258,16 +251,14 @@ class OwncastAdminClient(OwncastClient):
|
||||
return dict(await self._get("/api/admin/disconnect"))
|
||||
|
||||
async def get_chat_messages(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get chat messages from the admin perspective.
|
||||
"""Get chat messages from the admin perspective.
|
||||
|
||||
:return: Chat message list as returned by the Owncast API.
|
||||
"""
|
||||
return list(await self._get("/api/admin/chat/messages"))
|
||||
|
||||
async def get_connected_chat_clients(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get currently connected chat clients.
|
||||
"""Get currently connected chat clients.
|
||||
|
||||
:return: Client list as returned by the Owncast API.
|
||||
"""
|
||||
@@ -276,8 +267,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
async def set_message_visibility(
|
||||
self, message_ids: list[str], visible: bool
|
||||
) -> str:
|
||||
"""
|
||||
Hide or show chat messages.
|
||||
"""Hide or show chat messages.
|
||||
|
||||
:param message_ids: List of message IDs to modify.
|
||||
:param visible: True to show messages, False to hide them.
|
||||
@@ -292,8 +282,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_user_enabled(self, user_id: str, enabled: bool) -> str:
|
||||
"""
|
||||
Enable or disable a chat user.
|
||||
"""Enable or disable a chat user.
|
||||
|
||||
:param user_id: The user ID to modify.
|
||||
:param enabled: True to enable, False to disable the user.
|
||||
@@ -307,16 +296,14 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def get_disabled_users(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get a list of disabled chat users.
|
||||
"""Get a list of disabled chat users.
|
||||
|
||||
:return: User list as returned by the Owncast API.
|
||||
"""
|
||||
return list(await self._get("/api/admin/chat/users/disabled"))
|
||||
|
||||
async def set_user_moderator(self, user_id: str, is_mod: bool) -> str:
|
||||
"""
|
||||
Grant or revoke moderator status for a user.
|
||||
"""Grant or revoke moderator status for a user.
|
||||
|
||||
:param user_id: The user ID to modify.
|
||||
:param is_mod: True to grant moderator, False to revoke.
|
||||
@@ -330,16 +317,14 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def get_moderators(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get a list of moderator users.
|
||||
"""Get a list of moderator users.
|
||||
|
||||
:return: User list as returned by the Owncast API.
|
||||
"""
|
||||
return list(await self._get("/api/admin/chat/users/moderators"))
|
||||
|
||||
async def ban_ip_address(self, ip: str) -> str:
|
||||
"""
|
||||
Ban an IP address from chat.
|
||||
"""Ban an IP address from chat.
|
||||
|
||||
:param ip: The IP address to ban.
|
||||
:return: Success message from the server.
|
||||
@@ -348,8 +333,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
return await self._post("/api/admin/chat/users/ipbans/create", {"value": ip})
|
||||
|
||||
async def unban_ip_address(self, ip: str) -> str:
|
||||
"""
|
||||
Remove an IP address ban.
|
||||
"""Remove an IP address ban.
|
||||
|
||||
:param ip: The IP address to unban.
|
||||
:return: Success message from the server.
|
||||
@@ -358,16 +342,14 @@ class OwncastAdminClient(OwncastClient):
|
||||
return await self._post("/api/admin/chat/users/ipbans/remove", {"value": ip})
|
||||
|
||||
async def get_ip_address_bans(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get a list of banned IP addresses.
|
||||
"""Get a list of banned IP addresses.
|
||||
|
||||
:return: IP ban list as returned by the Owncast API.
|
||||
"""
|
||||
return list(await self._get("/api/admin/chat/users/ipbans"))
|
||||
|
||||
async def set_stream_title(self, title: str) -> str:
|
||||
"""
|
||||
Set the stream title.
|
||||
"""Set the stream title.
|
||||
|
||||
:param title: The new stream title.
|
||||
:return: Success message from the server.
|
||||
@@ -377,8 +359,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_server_name(self, name: str) -> str:
|
||||
"""
|
||||
Set the server name.
|
||||
"""Set the server name.
|
||||
|
||||
:param name: The new server name.
|
||||
:return: Success message from the server.
|
||||
@@ -388,8 +369,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_server_summary(self, summary: str) -> str:
|
||||
"""
|
||||
Set the server summary.
|
||||
"""Set the server summary.
|
||||
|
||||
:param summary: The new server summary.
|
||||
:return: Success message from the server.
|
||||
@@ -399,8 +379,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_welcome_message(self, message: str) -> str:
|
||||
"""
|
||||
Set the welcome message shown to new viewers.
|
||||
"""Set the welcome message shown to new viewers.
|
||||
|
||||
:param message: The new welcome message.
|
||||
:return: Success message from the server.
|
||||
@@ -410,8 +389,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_offline_message(self, message: str) -> str:
|
||||
"""
|
||||
Set the message shown when the stream is offline.
|
||||
"""Set the message shown when the stream is offline.
|
||||
|
||||
:param message: The new offline message.
|
||||
:return: Success message from the server.
|
||||
@@ -421,8 +399,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_page_content(self, content: str) -> str:
|
||||
"""
|
||||
Set the custom page content (HTML/markdown below the player).
|
||||
"""Set the custom page content (HTML/markdown below the player).
|
||||
|
||||
:param content: The page content.
|
||||
:return: Success message from the server.
|
||||
@@ -432,8 +409,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_admin_password(self, password: str) -> str:
|
||||
"""
|
||||
Change the admin password.
|
||||
"""Change the admin password.
|
||||
|
||||
:param password: The new admin password.
|
||||
:return: Success message from the server.
|
||||
@@ -443,8 +419,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_server_url(self, url: str) -> str:
|
||||
"""
|
||||
Set the public server URL.
|
||||
"""Set the public server URL.
|
||||
|
||||
:param url: The new server URL.
|
||||
:return: Success message from the server.
|
||||
@@ -454,8 +429,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_custom_styles(self, css: str) -> str:
|
||||
"""
|
||||
Set custom CSS styles for the web interface.
|
||||
"""Set custom CSS styles for the web interface.
|
||||
|
||||
:param css: The CSS string.
|
||||
:return: Success message from the server.
|
||||
@@ -465,8 +439,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_custom_javascript(self, js: str) -> str:
|
||||
"""
|
||||
Set custom JavaScript for the web interface.
|
||||
"""Set custom JavaScript for the web interface.
|
||||
|
||||
:param js: The JavaScript string.
|
||||
:return: Success message from the server.
|
||||
@@ -476,8 +449,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_socket_host_override(self, host: str) -> str:
|
||||
"""
|
||||
Set the WebSocket host override.
|
||||
"""Set the WebSocket host override.
|
||||
|
||||
:param host: The WebSocket host override value.
|
||||
:return: Success message from the server.
|
||||
@@ -487,8 +459,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_ffmpeg_path(self, path: str) -> str:
|
||||
"""
|
||||
Set the path to the ffmpeg binary.
|
||||
"""Set the path to the ffmpeg binary.
|
||||
|
||||
:param path: The ffmpeg binary path.
|
||||
:return: Success message from the server.
|
||||
@@ -498,8 +469,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_video_serving_endpoint(self, endpoint: str) -> str:
|
||||
"""
|
||||
Set the video serving endpoint (e.g., for CDN).
|
||||
"""Set the video serving endpoint (e.g., for CDN).
|
||||
|
||||
:param endpoint: The video serving endpoint URL.
|
||||
:return: Success message from the server.
|
||||
@@ -509,8 +479,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_video_codec(self, codec: str) -> str:
|
||||
"""
|
||||
Set the video codec.
|
||||
"""Set the video codec.
|
||||
|
||||
:param codec: The video codec name.
|
||||
:return: Success message from the server.
|
||||
@@ -520,8 +489,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_chat_disabled(self, disabled: bool) -> str:
|
||||
"""
|
||||
Enable or disable the chat.
|
||||
"""Enable or disable the chat.
|
||||
|
||||
:param disabled: True to disable chat, False to enable it.
|
||||
:return: Success message from the server.
|
||||
@@ -531,8 +499,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_chat_join_messages_enabled(self, enabled: bool) -> str:
|
||||
"""
|
||||
Enable or disable chat join messages.
|
||||
"""Enable or disable chat join messages.
|
||||
|
||||
:param enabled: True to show join messages, False to hide them.
|
||||
:return: Success message from the server.
|
||||
@@ -542,8 +509,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_chat_established_mode(self, enabled: bool) -> str:
|
||||
"""
|
||||
Enable or disable established user mode for chat.
|
||||
"""Enable or disable established user mode for chat.
|
||||
|
||||
:param enabled: True to enable established mode.
|
||||
:return: Success message from the server.
|
||||
@@ -555,8 +521,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_chat_spam_protection(self, enabled: bool) -> str:
|
||||
"""
|
||||
Enable or disable chat spam protection.
|
||||
"""Enable or disable chat spam protection.
|
||||
|
||||
:param enabled: True to enable spam protection.
|
||||
:return: Success message from the server.
|
||||
@@ -568,8 +533,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_chat_slur_filter(self, enabled: bool) -> str:
|
||||
"""
|
||||
Enable or disable the chat slur filter.
|
||||
"""Enable or disable the chat slur filter.
|
||||
|
||||
:param enabled: True to enable the slur filter.
|
||||
:return: Success message from the server.
|
||||
@@ -579,8 +543,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_nsfw(self, nsfw: bool) -> str:
|
||||
"""
|
||||
Set the NSFW flag for the server.
|
||||
"""Set the NSFW flag for the server.
|
||||
|
||||
:param nsfw: True to mark the server as NSFW.
|
||||
:return: Success message from the server.
|
||||
@@ -588,8 +551,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
return await self._set_config_value("/api/admin/config/nsfw", nsfw, "NSFW flag")
|
||||
|
||||
async def set_directory_enabled(self, enabled: bool) -> str:
|
||||
"""
|
||||
Enable or disable listing in the Owncast directory.
|
||||
"""Enable or disable listing in the Owncast directory.
|
||||
|
||||
:param enabled: True to enable directory listing.
|
||||
:return: Success message from the server.
|
||||
@@ -599,8 +561,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_hide_viewer_count(self, hide: bool) -> str:
|
||||
"""
|
||||
Show or hide the viewer count.
|
||||
"""Show or hide the viewer count.
|
||||
|
||||
:param hide: True to hide the viewer count.
|
||||
:return: Success message from the server.
|
||||
@@ -610,8 +571,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_disable_search_indexing(self, disabled: bool) -> str:
|
||||
"""
|
||||
Enable or disable search engine indexing.
|
||||
"""Enable or disable search engine indexing.
|
||||
|
||||
:param disabled: True to disable search indexing.
|
||||
:return: Success message from the server.
|
||||
@@ -623,8 +583,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_forbidden_usernames(self, names: list[str]) -> str:
|
||||
"""
|
||||
Set the list of forbidden usernames.
|
||||
"""Set the list of forbidden usernames.
|
||||
|
||||
:param names: List of forbidden username strings.
|
||||
:return: Success message from the server.
|
||||
@@ -634,8 +593,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_suggested_usernames(self, names: list[str]) -> str:
|
||||
"""
|
||||
Set the list of suggested usernames for new viewers.
|
||||
"""Set the list of suggested usernames for new viewers.
|
||||
|
||||
:param names: List of suggested username strings.
|
||||
:return: Success message from the server.
|
||||
@@ -645,8 +603,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_tags(self, tags: list[str]) -> str:
|
||||
"""
|
||||
Set the server tags.
|
||||
"""Set the server tags.
|
||||
|
||||
:param tags: List of tag strings.
|
||||
:return: Success message from the server.
|
||||
@@ -654,8 +611,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
return await self._set_config_value("/api/admin/config/tags", tags, "tags")
|
||||
|
||||
async def set_federation_blocked_domains(self, domains: list[str]) -> str:
|
||||
"""
|
||||
Set the list of blocked federation domains.
|
||||
"""Set the list of blocked federation domains.
|
||||
|
||||
:param domains: List of domain strings to block.
|
||||
:return: Success message from the server.
|
||||
@@ -667,8 +623,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_stream_keys(self, keys: list[StreamKey]) -> str:
|
||||
"""
|
||||
Set the stream keys.
|
||||
"""Set the stream keys.
|
||||
|
||||
:param keys: List of :class:`StreamKey` objects.
|
||||
:return: Success message from the server.
|
||||
@@ -680,8 +635,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_video_variants(self, variants: list[VideoVariant]) -> str:
|
||||
"""
|
||||
Set the video output variants (quality levels).
|
||||
"""Set the video output variants (quality levels).
|
||||
|
||||
:param variants: List of :class:`VideoVariant` objects.
|
||||
:return: Success message from the server.
|
||||
@@ -693,8 +647,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_social_handles(self, handles: list[SocialHandle]) -> str:
|
||||
"""
|
||||
Set the social media handles displayed on the page.
|
||||
"""Set the social media handles displayed on the page.
|
||||
|
||||
:param handles: List of :class:`SocialHandle` objects.
|
||||
:return: Success message from the server.
|
||||
@@ -706,8 +659,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_external_actions(self, actions: list[ExternalAction]) -> str:
|
||||
"""
|
||||
Set the external actions (buttons/links in the player).
|
||||
"""Set the external actions (buttons/links in the player).
|
||||
|
||||
:param actions: List of :class:`ExternalAction` objects.
|
||||
:return: Success message from the server.
|
||||
@@ -727,8 +679,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
bucket: str,
|
||||
region: str,
|
||||
) -> str:
|
||||
"""
|
||||
Set the S3 storage configuration.
|
||||
"""Set the S3 storage configuration.
|
||||
|
||||
:param enabled: Whether S3 storage is enabled.
|
||||
:param endpoint: The S3 endpoint URL.
|
||||
@@ -757,8 +708,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
webhook: str,
|
||||
go_live_message: str,
|
||||
) -> str:
|
||||
"""
|
||||
Set the Discord notification configuration.
|
||||
"""Set the Discord notification configuration.
|
||||
|
||||
:param enabled: Whether Discord notifications are enabled.
|
||||
:param webhook: The Discord webhook URL.
|
||||
@@ -780,8 +730,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
enabled: bool,
|
||||
go_live_message: str,
|
||||
) -> str:
|
||||
"""
|
||||
Set the browser notification configuration.
|
||||
"""Set the browser notification configuration.
|
||||
|
||||
:param enabled: Whether browser notifications are enabled.
|
||||
:param go_live_message: The message shown when going live.
|
||||
@@ -797,8 +746,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_color_variables(self, variables: dict[str, Any]) -> str:
|
||||
"""
|
||||
Set the custom color variables for the web interface.
|
||||
"""Set the custom color variables for the web interface.
|
||||
|
||||
:param variables: Color variables dict.
|
||||
:return: Success message from the server.
|
||||
@@ -808,8 +756,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_stream_latency(self, level: int) -> str:
|
||||
"""
|
||||
Set the stream latency level.
|
||||
"""Set the stream latency level.
|
||||
|
||||
:param level: Latency level value.
|
||||
:return: Success message from the server.
|
||||
@@ -819,8 +766,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_rtmp_port(self, port: int) -> str:
|
||||
"""
|
||||
Set the RTMP server port.
|
||||
"""Set the RTMP server port.
|
||||
|
||||
:param port: The RTMP port number.
|
||||
:return: Success message from the server.
|
||||
@@ -830,8 +776,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_web_server_port(self, port: int) -> str:
|
||||
"""
|
||||
Set the web server port.
|
||||
"""Set the web server port.
|
||||
|
||||
:param port: The web server port number.
|
||||
:return: Success message from the server.
|
||||
@@ -841,8 +786,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_web_server_ip(self, ip: str) -> str:
|
||||
"""
|
||||
Set the web server bind IP address.
|
||||
"""Set the web server bind IP address.
|
||||
|
||||
:param ip: The IP address to bind to.
|
||||
:return: Success message from the server.
|
||||
@@ -852,8 +796,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_federation_enabled(self, enabled: bool) -> str:
|
||||
"""
|
||||
Enable or disable federation (ActivityPub).
|
||||
"""Enable or disable federation (ActivityPub).
|
||||
|
||||
:param enabled: True to enable federation.
|
||||
:return: Success message from the server.
|
||||
@@ -863,8 +806,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_federation_username(self, name: str) -> str:
|
||||
"""
|
||||
Set the federation (ActivityPub) username.
|
||||
"""Set the federation (ActivityPub) username.
|
||||
|
||||
:param name: The federation username.
|
||||
:return: Success message from the server.
|
||||
@@ -874,8 +816,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_federation_go_live_message(self, message: str) -> str:
|
||||
"""
|
||||
Set the message sent to followers when going live.
|
||||
"""Set the message sent to followers when going live.
|
||||
|
||||
:param message: The go-live notification message.
|
||||
:return: Success message from the server.
|
||||
@@ -887,8 +828,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def set_logo(self, base64_data_url: str) -> str:
|
||||
"""
|
||||
Set the server logo from a base64 data URL.
|
||||
"""Set the server logo from a base64 data URL.
|
||||
|
||||
:param base64_data_url: Data URL string
|
||||
(e.g., ``data:image/png;base64,iVBOR...``).
|
||||
@@ -899,8 +839,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def upload_emoji(self, name: str, data_base64: str) -> str:
|
||||
"""
|
||||
Upload a custom emoji.
|
||||
"""Upload a custom emoji.
|
||||
|
||||
:param name: The emoji name.
|
||||
:param data_base64: Base64-encoded image data for the emoji.
|
||||
@@ -912,8 +851,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def delete_emoji(self, name: str) -> str:
|
||||
"""
|
||||
Delete a custom emoji.
|
||||
"""Delete a custom emoji.
|
||||
|
||||
:param name: The emoji name to delete.
|
||||
:return: Success message from the server.
|
||||
@@ -922,16 +860,14 @@ class OwncastAdminClient(OwncastClient):
|
||||
return await self._post("/api/admin/emoji/delete", {"name": name})
|
||||
|
||||
async def get_webhooks(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get all registered webhooks.
|
||||
"""Get all registered webhooks.
|
||||
|
||||
:return: Webhook list as returned by the Owncast API.
|
||||
"""
|
||||
return list(await self._get("/api/admin/webhooks"))
|
||||
|
||||
async def create_webhook(self, url: str, events: list[str]) -> str:
|
||||
"""
|
||||
Create a new webhook.
|
||||
"""Create a new webhook.
|
||||
|
||||
:param url: The URL to send webhook events to.
|
||||
:param events: List of event type strings to subscribe to.
|
||||
@@ -943,8 +879,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def delete_webhook(self, webhook_id: int) -> str:
|
||||
"""
|
||||
Delete a webhook.
|
||||
"""Delete a webhook.
|
||||
|
||||
:param webhook_id: The ID of the webhook to delete.
|
||||
:return: Success message from the server.
|
||||
@@ -953,16 +888,14 @@ class OwncastAdminClient(OwncastClient):
|
||||
return await self._post("/api/admin/webhooks/delete", {"id": webhook_id})
|
||||
|
||||
async def get_access_tokens(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get all access tokens.
|
||||
"""Get all access tokens.
|
||||
|
||||
:return: Token list as returned by the Owncast API.
|
||||
"""
|
||||
return list(await self._get("/api/admin/accesstokens"))
|
||||
|
||||
async def create_access_token(self, name: str, scopes: list[str]) -> str:
|
||||
"""
|
||||
Create a new access token.
|
||||
"""Create a new access token.
|
||||
|
||||
:param name: Display name for the token.
|
||||
:param scopes: List of permission scope strings.
|
||||
@@ -974,8 +907,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def delete_access_token(self, token: str) -> str:
|
||||
"""
|
||||
Delete an access token.
|
||||
"""Delete an access token.
|
||||
|
||||
:param token: The token string to delete.
|
||||
:return: Success message from the server.
|
||||
@@ -985,8 +917,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
return await self._post("/api/admin/accesstokens/delete", {"token": token})
|
||||
|
||||
async def get_followers(self, offset: int = 0, limit: int = 25) -> dict[str, Any]:
|
||||
"""
|
||||
Get a paginated list of followers.
|
||||
"""Get a paginated list of followers.
|
||||
|
||||
:param offset: Number of followers to skip.
|
||||
:param limit: Maximum number of followers to return.
|
||||
@@ -999,24 +930,21 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def get_pending_follow_requests(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get pending follow requests.
|
||||
"""Get pending follow requests.
|
||||
|
||||
:return: Follower request list as returned by the Owncast API.
|
||||
"""
|
||||
return list(await self._get("/api/admin/followers/pending"))
|
||||
|
||||
async def get_blocked_followers(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get blocked and rejected followers.
|
||||
"""Get blocked and rejected followers.
|
||||
|
||||
:return: Follower list as returned by the Owncast API.
|
||||
"""
|
||||
return list(await self._get("/api/admin/followers/blocked"))
|
||||
|
||||
async def approve_follower(self, actor_iri: str, approved: bool) -> str:
|
||||
"""
|
||||
Approve or reject a follow request.
|
||||
"""Approve or reject a follow request.
|
||||
|
||||
:param actor_iri: The ActivityPub actor IRI of the follower.
|
||||
:param approved: True to approve, False to reject.
|
||||
@@ -1030,8 +958,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
)
|
||||
|
||||
async def send_federated_message(self, message: str) -> str:
|
||||
"""
|
||||
Send a message to all followers via federation.
|
||||
"""Send a message to all followers via federation.
|
||||
|
||||
:param message: The message text to send.
|
||||
:return: Success message from the server.
|
||||
@@ -1040,24 +967,21 @@ class OwncastAdminClient(OwncastClient):
|
||||
return await self._post("/api/admin/federation/send", {"value": message})
|
||||
|
||||
async def get_logs(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get server logs.
|
||||
"""Get server logs.
|
||||
|
||||
:return: Log entry list as returned by the Owncast API.
|
||||
"""
|
||||
return list(await self._get("/api/admin/logs"))
|
||||
|
||||
async def get_warnings(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get server warning and error logs.
|
||||
"""Get server warning and error logs.
|
||||
|
||||
:return: Log entry list as returned by the Owncast API.
|
||||
"""
|
||||
return list(await self._get("/api/admin/logs/warnings"))
|
||||
|
||||
async def get_playback_metrics(self) -> dict[str, Any]:
|
||||
"""
|
||||
Get playback quality metrics.
|
||||
"""Get playback quality metrics.
|
||||
|
||||
:return: Playback metrics as returned by the Owncast API.
|
||||
"""
|
||||
@@ -1066,8 +990,7 @@ class OwncastAdminClient(OwncastClient):
|
||||
async def _set_config_value(
|
||||
self, endpoint: str, value: Any, description: str
|
||||
) -> str:
|
||||
"""
|
||||
Set a configuration value via POST with a ``{"value": ...}`` body.
|
||||
"""Set a configuration value via POST with a ``{"value": ...}`` body.
|
||||
|
||||
:param endpoint: The config API endpoint path.
|
||||
:param value: The value to set.
|
||||
|
||||
@@ -52,8 +52,7 @@ class OwncastError(Exception):
|
||||
"""Raised when an Owncast API request fails."""
|
||||
|
||||
def __init__(self, status: int, message: str):
|
||||
"""
|
||||
Initialize the error.
|
||||
"""Initialize the error.
|
||||
|
||||
:param status: HTTP status code from the failed request, or 0 if the
|
||||
request failed due to a connection error before receiving a response.
|
||||
@@ -72,8 +71,7 @@ class OwncastClient:
|
||||
"""
|
||||
|
||||
def __init__(self, base_url: str, access_token: str, http_client: HttpClient):
|
||||
"""
|
||||
Initialize the Owncast client.
|
||||
"""Initialize the Owncast client.
|
||||
|
||||
:param base_url: The Owncast server URL (e.g., "https://stream.logal.dev").
|
||||
:param access_token: API access token from Owncast admin settings.
|
||||
@@ -94,8 +92,7 @@ class OwncastClient:
|
||||
return self._base_url
|
||||
|
||||
async def get_status(self) -> dict[str, Any]:
|
||||
"""
|
||||
Get the public server status.
|
||||
"""Get the public server status.
|
||||
|
||||
This is a public endpoint that does not require authentication.
|
||||
Returns server info including version, online status, and viewer count.
|
||||
@@ -106,8 +103,7 @@ class OwncastClient:
|
||||
return dict(await self._get("/api/status"))
|
||||
|
||||
async def send_message(self, body: str) -> str:
|
||||
"""
|
||||
Send a chat message visible to all viewers.
|
||||
"""Send a chat message visible to all viewers.
|
||||
|
||||
:param body: The message text (supports markdown).
|
||||
:return: Success message from the server.
|
||||
@@ -116,8 +112,7 @@ class OwncastClient:
|
||||
return await self._post("/api/integrations/chat/send", {"body": body})
|
||||
|
||||
async def send_system_message(self, body: str) -> str:
|
||||
"""
|
||||
Send a system message visible to all viewers.
|
||||
"""Send a system message visible to all viewers.
|
||||
|
||||
System messages are styled differently from regular chat (typically
|
||||
italicized or dimmed) and are used for announcements or notifications.
|
||||
@@ -129,8 +124,7 @@ class OwncastClient:
|
||||
return await self._post("/api/integrations/chat/system", {"body": body})
|
||||
|
||||
async def send_action(self, body: str) -> str:
|
||||
"""
|
||||
Send an action message (like IRC /me).
|
||||
"""Send an action message (like IRC /me).
|
||||
|
||||
Action messages display as "*BotName does something*" and are used
|
||||
for describing actions rather than speech.
|
||||
@@ -142,8 +136,7 @@ class OwncastClient:
|
||||
return await self._post("/api/integrations/chat/action", {"body": body})
|
||||
|
||||
async def send_system_message_to_client(self, client_id: int, body: str) -> str:
|
||||
"""
|
||||
Send a private system message to a specific viewer.
|
||||
"""Send a private system message to a specific viewer.
|
||||
|
||||
The message is only visible to the targeted client, useful for
|
||||
welcome messages or private notifications.
|
||||
@@ -160,8 +153,7 @@ class OwncastClient:
|
||||
async def set_message_visibility(
|
||||
self, message_ids: list[str], visible: bool
|
||||
) -> str:
|
||||
"""
|
||||
Hide or show chat messages (moderation).
|
||||
"""Hide or show chat messages (moderation).
|
||||
|
||||
Hidden messages are removed from the chat display for all viewers.
|
||||
This is typically used for moderation purposes.
|
||||
@@ -179,8 +171,7 @@ class OwncastClient:
|
||||
)
|
||||
|
||||
async def get_chat_history(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Fetch recent chat messages.
|
||||
"""Fetch recent chat messages.
|
||||
|
||||
:return: List of recent chat message objects with user info and content.
|
||||
"""
|
||||
@@ -188,8 +179,7 @@ class OwncastClient:
|
||||
return list(await self._get("/api/integrations/chat"))
|
||||
|
||||
async def get_connected_clients(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get list of currently connected viewers.
|
||||
"""Get list of currently connected viewers.
|
||||
|
||||
:return: List of connected client objects with user info and connection details.
|
||||
"""
|
||||
@@ -197,8 +187,7 @@ class OwncastClient:
|
||||
return list(await self._get("/api/integrations/clients"))
|
||||
|
||||
async def set_stream_title(self, title: str) -> str:
|
||||
"""
|
||||
Update the stream title.
|
||||
"""Update the stream title.
|
||||
|
||||
:param title: The new stream title.
|
||||
:return: Success message from the server.
|
||||
@@ -207,8 +196,7 @@ class OwncastClient:
|
||||
return await self._post("/api/integrations/streamtitle", {"value": title})
|
||||
|
||||
async def _post(self, endpoint: str, data: dict[str, Any] | None = None) -> str:
|
||||
"""
|
||||
Send a POST request to the Owncast API.
|
||||
"""Send a POST request to the Owncast API.
|
||||
|
||||
Owncast POST endpoints return ``{"success": true, "message": "..."}``.
|
||||
This method validates the response and returns just the message string.
|
||||
@@ -270,8 +258,7 @@ class OwncastClient:
|
||||
raise OwncastError(0, str(e)) from e
|
||||
|
||||
async def _get(self, endpoint: str, params: dict[str, Any] | None = None) -> Any:
|
||||
"""
|
||||
Send a GET request to the Owncast API.
|
||||
"""Send a GET request to the Owncast API.
|
||||
|
||||
:param endpoint: The API endpoint path.
|
||||
:param params: Optional query parameters.
|
||||
|
||||
@@ -62,8 +62,7 @@ def on_route(
|
||||
*,
|
||||
methods: list[str] | None = None,
|
||||
) -> Callable[[RouteHandler], RouteHandler]:
|
||||
"""
|
||||
Decorator to register an HTTP route handler.
|
||||
"""Register an HTTP route handler.
|
||||
|
||||
Routes are namespaced under /owlbot/<module_name>/<path>.
|
||||
|
||||
|
||||
+13
-24
@@ -34,8 +34,7 @@ class StorageError(Exception):
|
||||
|
||||
|
||||
class ModuleStorage:
|
||||
"""
|
||||
Module-scoped async SQLite storage backed by a lazy connection pool.
|
||||
"""Module-scoped async SQLite storage backed by a lazy connection pool.
|
||||
|
||||
Each module gets its own isolated database file. Connections are created
|
||||
lazily and pooled up to ``pool_size``. WAL mode is enabled so that
|
||||
@@ -54,8 +53,7 @@ class ModuleStorage:
|
||||
"""
|
||||
|
||||
def __init__(self, storage_dir: Path, module_name: str, pool_size: int = 4):
|
||||
"""
|
||||
Initialize the storage API.
|
||||
"""Initialize the storage API.
|
||||
|
||||
:param storage_dir: Directory where module databases are stored.
|
||||
:param module_name: Name of the module this storage belongs to.
|
||||
@@ -92,8 +90,7 @@ class ModuleStorage:
|
||||
|
||||
@asynccontextmanager
|
||||
async def transaction(self) -> AsyncIterator[ModuleStorage]:
|
||||
"""
|
||||
Context manager for explicit transaction control within a handler.
|
||||
"""Context manager for explicit transaction control within a handler.
|
||||
|
||||
Use this when you need multiple operations to succeed or fail together
|
||||
within a single handler. Commits on success, rolls back on exception.
|
||||
@@ -105,6 +102,7 @@ class ModuleStorage:
|
||||
# Both committed together, or both rolled back on error
|
||||
|
||||
:return: This ModuleStorage instance.
|
||||
|
||||
"""
|
||||
self._logger.debug("Explicit transaction started.")
|
||||
try:
|
||||
@@ -119,8 +117,7 @@ class ModuleStorage:
|
||||
sql: str,
|
||||
parameters: tuple[Any, ...] | dict[str, Any] = (),
|
||||
) -> aiosqlite.Cursor:
|
||||
"""
|
||||
Execute a SQL statement.
|
||||
"""Execute a SQL statement.
|
||||
|
||||
:param sql: SQL statement (use ? or :name for parameters).
|
||||
:param parameters: Query parameters (tuple for ?, dict for :name).
|
||||
@@ -140,8 +137,7 @@ class ModuleStorage:
|
||||
sql: str,
|
||||
parameters: list[tuple[Any, ...]] | list[dict[str, Any]],
|
||||
) -> aiosqlite.Cursor:
|
||||
"""
|
||||
Execute a SQL statement with multiple parameter sets.
|
||||
"""Execute a SQL statement with multiple parameter sets.
|
||||
|
||||
Useful for batch inserts/updates.
|
||||
|
||||
@@ -166,8 +162,7 @@ class ModuleStorage:
|
||||
sql: str,
|
||||
parameters: tuple[Any, ...] | dict[str, Any] = (),
|
||||
) -> aiosqlite.Row | None:
|
||||
"""
|
||||
Execute a query and fetch one row.
|
||||
"""Execute a query and fetch one row.
|
||||
|
||||
:param sql: SELECT statement.
|
||||
:param parameters: Query parameters.
|
||||
@@ -189,8 +184,7 @@ class ModuleStorage:
|
||||
sql: str,
|
||||
parameters: tuple[Any, ...] | dict[str, Any] = (),
|
||||
) -> list[aiosqlite.Row]:
|
||||
"""
|
||||
Execute a query and fetch all rows.
|
||||
"""Execute a query and fetch all rows.
|
||||
|
||||
:param sql: SELECT statement.
|
||||
:param parameters: Query parameters.
|
||||
@@ -212,8 +206,7 @@ class ModuleStorage:
|
||||
sql: str,
|
||||
parameters: tuple[Any, ...] | dict[str, Any] = (),
|
||||
) -> Any | None:
|
||||
"""
|
||||
Execute a query and fetch a single value.
|
||||
"""Execute a query and fetch a single value.
|
||||
|
||||
:param sql: SELECT statement returning one column.
|
||||
:param parameters: Query parameters.
|
||||
@@ -260,8 +253,7 @@ class ModuleStorage:
|
||||
|
||||
@asynccontextmanager
|
||||
async def _connection(self) -> AsyncIterator[aiosqlite.Connection]:
|
||||
"""
|
||||
Async context manager that provides a connection.
|
||||
"""Async context manager that provides a connection.
|
||||
|
||||
If already inside a ``_checkout``, yields the checked-out connection
|
||||
without releasing it. Otherwise acquires a standalone connection from
|
||||
@@ -285,8 +277,7 @@ class ModuleStorage:
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkout(self) -> AsyncIterator[None]:
|
||||
"""
|
||||
Check out a connection from the pool for the duration of a handler.
|
||||
"""Check out a connection from the pool for the duration of a handler.
|
||||
|
||||
Sets a ContextVar so that all storage operations within the handler
|
||||
reuse the same connection.
|
||||
@@ -300,8 +291,7 @@ class ModuleStorage:
|
||||
self._release(conn)
|
||||
|
||||
async def _commit(self) -> None:
|
||||
"""
|
||||
Commit the current transaction (internal use by bot).
|
||||
"""Commit the current transaction (internal use by bot).
|
||||
|
||||
Called automatically after each handler completes successfully.
|
||||
"""
|
||||
@@ -311,8 +301,7 @@ class ModuleStorage:
|
||||
self._logger.debug("Transaction committed.")
|
||||
|
||||
async def _rollback(self) -> None:
|
||||
"""
|
||||
Rollback the current transaction (internal use by bot).
|
||||
"""Rollback the current transaction (internal use by bot).
|
||||
|
||||
Called automatically if a handler throws an exception.
|
||||
"""
|
||||
|
||||
+2
-4
@@ -47,8 +47,7 @@ class Owlbot:
|
||||
overrides: dict[str, Any] | None = None,
|
||||
skip_api_check: bool = False,
|
||||
):
|
||||
"""
|
||||
Initialize Owlbot.
|
||||
"""Initialize Owlbot.
|
||||
|
||||
:param config_path: Path to the YAML config file.
|
||||
:param overrides: CLI overrides passed to the config manager.
|
||||
@@ -161,8 +160,7 @@ class Owlbot:
|
||||
logger.info("Owlbot shutdown complete.")
|
||||
|
||||
async def _check_api_accessibility(self) -> None:
|
||||
"""
|
||||
Verify that the Owncast APIs are reachable before proceeding with startup.
|
||||
"""Verify that the Owncast APIs are reachable before proceeding with startup.
|
||||
|
||||
Calls a non-transformative endpoint on each configured client to confirm
|
||||
the server is accessible and credentials are valid. Raises
|
||||
|
||||
@@ -56,8 +56,7 @@ __all__ = [
|
||||
|
||||
@on_setup
|
||||
async def setup(ctx: ModuleContext) -> None:
|
||||
"""
|
||||
Initialize the custom_commands module.
|
||||
"""Initialize the custom_commands module.
|
||||
|
||||
Creates the database schema and loads existing commands from the database.
|
||||
|
||||
|
||||
@@ -30,8 +30,7 @@ from .placeholders import DEFAULT_MAX_DEPTH, process_placeholders
|
||||
async def get_aliases_for_command(
|
||||
storage: ModuleStorage, command_name: str
|
||||
) -> list[str]:
|
||||
"""
|
||||
Fetch all aliases for a command from the database.
|
||||
"""Fetch all aliases for a command from the database.
|
||||
|
||||
:param storage: The module storage instance.
|
||||
:param command_name: The canonical command name.
|
||||
@@ -47,8 +46,7 @@ async def get_aliases_for_command(
|
||||
async def resolve_command_name(
|
||||
storage: ModuleStorage, name: str
|
||||
) -> aiosqlite.Row | None:
|
||||
"""
|
||||
Resolve a command name or alias to the full command row.
|
||||
"""Resolve a command name or alias to the full command row.
|
||||
|
||||
Checks the commands table first, then falls back to the aliases table.
|
||||
Returns the command's name, requires_moderator, and cooldown columns.
|
||||
@@ -70,8 +68,7 @@ async def resolve_command_name(
|
||||
|
||||
|
||||
async def custom_command_handler(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Shared handler for all custom commands.
|
||||
"""Shared handler for all custom commands.
|
||||
|
||||
Looks up the command in the database, increments use count,
|
||||
processes placeholders, and sends the response.
|
||||
@@ -112,8 +109,7 @@ def reregister_command(
|
||||
requires_moderator: bool,
|
||||
cooldown: int,
|
||||
) -> None:
|
||||
"""
|
||||
Unregister and re-register a custom command with updated settings.
|
||||
"""Unregister and re-register a custom command with updated settings.
|
||||
|
||||
Both registry operations are synchronous, so no other coroutine can observe
|
||||
the intermediate unregistered state.
|
||||
|
||||
@@ -67,8 +67,7 @@ async def _resolve_or_error(ctx: CommandContext, raw_name: str) -> aiosqlite.Row
|
||||
|
||||
@on_command("addcommand", aliases=["addcmd"], requires_moderator=True)
|
||||
async def addcommand(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Create a new custom command.
|
||||
"""Create a new custom command.
|
||||
|
||||
Usage: !addcommand !name response text
|
||||
|
||||
@@ -134,8 +133,7 @@ async def addcommand(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("editcommand", aliases=["editcmd"], requires_moderator=True)
|
||||
async def editcommand(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Edit an existing custom command's response.
|
||||
"""Edit an existing custom command's response.
|
||||
|
||||
Usage: !editcommand !name new response
|
||||
|
||||
@@ -175,8 +173,7 @@ async def editcommand(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("deletecommand", aliases=["delcmd"], requires_moderator=True)
|
||||
async def deletecommand(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Delete a custom command.
|
||||
"""Delete a custom command.
|
||||
|
||||
Usage: !deletecommand !name
|
||||
|
||||
@@ -215,8 +212,7 @@ async def deletecommand(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("commandmodonly", aliases=["cmdmodonly"], requires_moderator=True)
|
||||
async def commandmodonly(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Toggle moderator-only access for a custom command.
|
||||
"""Toggle moderator-only access for a custom command.
|
||||
|
||||
Usage: !commandmodonly !name <on|off>
|
||||
|
||||
@@ -255,8 +251,7 @@ async def commandmodonly(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("resetcommand", aliases=["resetcmd"], requires_moderator=True)
|
||||
async def resetcommand(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Reset a custom command's use counter to 0.
|
||||
"""Reset a custom command's use counter to 0.
|
||||
|
||||
Usage: !resetcommand !name
|
||||
|
||||
@@ -288,8 +283,7 @@ async def resetcommand(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("editcounter", aliases=["editcount"], requires_moderator=True)
|
||||
async def editcounter(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Set, increment, or decrement a named counter.
|
||||
"""Set, increment, or decrement a named counter.
|
||||
|
||||
Usage: !editcounter <name> <value>
|
||||
|
||||
@@ -351,8 +345,7 @@ async def editcounter(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("commandcooldown", aliases=["cmdcooldown"], requires_moderator=True)
|
||||
async def commandcooldown(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Set or disable a custom command's cooldown.
|
||||
"""Set or disable a custom command's cooldown.
|
||||
|
||||
Usage: !commandcooldown !name <seconds>
|
||||
|
||||
@@ -404,8 +397,7 @@ async def commandcooldown(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("addalias", requires_moderator=True)
|
||||
async def addalias(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Add an alias to an existing custom command.
|
||||
"""Add an alias to an existing custom command.
|
||||
|
||||
Usage: !addalias !command !alias
|
||||
|
||||
@@ -485,8 +477,7 @@ async def addalias(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("removealias", requires_moderator=True)
|
||||
async def removealias(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Remove an alias from a custom command.
|
||||
"""Remove an alias from a custom command.
|
||||
|
||||
Usage: !removealias !alias
|
||||
|
||||
@@ -535,8 +526,7 @@ async def removealias(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("listcommands", aliases=["listcmds"], cooldown=15)
|
||||
async def listcommands(ctx: CommandContext) -> None:
|
||||
"""
|
||||
List all custom commands.
|
||||
"""List all custom commands.
|
||||
|
||||
Sends a URL to the command list web page.
|
||||
Has a 15-second cooldown to prevent spam.
|
||||
|
||||
@@ -105,10 +105,10 @@ class PlaceholderContext:
|
||||
|
||||
|
||||
def _find_matching_close(template: str, pos: int) -> int:
|
||||
"""Find the position of the ``)`` that closes an escaped ``\\$(...)`` group.
|
||||
r"""Find the position of the ``)`` that closes an escaped ``\$(...)`` group.
|
||||
|
||||
Tracks nested ``$(`` / ``)`` pairs so that escaped groups containing inner
|
||||
placeholders (e.g. ``\\$(rand $(1) $(2))``) are consumed in their entirety.
|
||||
placeholders (e.g. ``\$(rand $(1) $(2))``) are consumed in their entirety.
|
||||
|
||||
:param template: The full template string.
|
||||
:param pos: The position immediately after the opening ``$(`` of the
|
||||
|
||||
@@ -30,8 +30,7 @@ _jinja_env = jinja2.Environment(
|
||||
|
||||
@on_route("/list", methods=["GET"])
|
||||
async def command_list_page(ctx: RouteContext) -> web.Response:
|
||||
"""
|
||||
Serve an HTML page listing all custom commands in a table.
|
||||
"""Serve an HTML page listing all custom commands in a table.
|
||||
|
||||
Columns: Command, Aliases, Response, Cooldown, Permissions.
|
||||
Accessible at /owlbot/custom_commands/list.
|
||||
|
||||
@@ -41,8 +41,7 @@ _jinja_env = jinja2.Environment(
|
||||
|
||||
@on_setup
|
||||
async def setup(ctx: ModuleContext) -> None:
|
||||
"""
|
||||
Initialize the quotes module.
|
||||
"""Initialize the quotes module.
|
||||
|
||||
Creates the database schema.
|
||||
|
||||
@@ -63,8 +62,7 @@ async def setup(ctx: ModuleContext) -> None:
|
||||
|
||||
@on_route("/list", methods=["GET"])
|
||||
async def quotes_list_page(ctx: RouteContext) -> web.Response:
|
||||
"""
|
||||
Serve an HTML page listing all quotes in a table.
|
||||
"""Serve an HTML page listing all quotes in a table.
|
||||
|
||||
Columns: #, Quote, Added By, Date Added.
|
||||
Accessible at /owlbot/quotes/list.
|
||||
@@ -94,8 +92,7 @@ async def quotes_list_page(ctx: RouteContext) -> web.Response:
|
||||
|
||||
@on_command("quote", aliases=["q"])
|
||||
async def quote_command(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Display a quote. Random if no argument, specific if an ID is given.
|
||||
"""Display a quote. Random if no argument, specific if an ID is given.
|
||||
|
||||
:param ctx: The command context.
|
||||
"""
|
||||
@@ -137,8 +134,7 @@ async def quote_command(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("addquote", requires_moderator=True)
|
||||
async def addquote_command(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Add a new quote to the database. Moderator only.
|
||||
"""Add a new quote to the database. Moderator only.
|
||||
|
||||
:param ctx: The command context.
|
||||
"""
|
||||
@@ -159,8 +155,7 @@ async def addquote_command(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("deletequote", aliases=["delquote"], requires_moderator=True)
|
||||
async def deletequote_command(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Delete a quote by ID. Moderator only.
|
||||
"""Delete a quote by ID. Moderator only.
|
||||
|
||||
:param ctx: The command context.
|
||||
"""
|
||||
@@ -190,8 +185,7 @@ async def deletequote_command(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("listquotes", cooldown=15)
|
||||
async def listquotes_command(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Send the URL to the quotes list web page.
|
||||
"""Send the URL to the quotes list web page.
|
||||
|
||||
:param ctx: The command context.
|
||||
"""
|
||||
|
||||
@@ -56,8 +56,7 @@ __all__ = [
|
||||
|
||||
@on_setup
|
||||
async def setup(ctx: ModuleContext) -> None:
|
||||
"""
|
||||
Initialize the timers module.
|
||||
"""Initialize the timers module.
|
||||
|
||||
Creates the database schema, initializes chat counters for enabled timers,
|
||||
and starts the background scheduler.
|
||||
@@ -97,8 +96,7 @@ async def setup(ctx: ModuleContext) -> None:
|
||||
|
||||
@on_teardown
|
||||
async def teardown(ctx: ModuleContext) -> None:
|
||||
"""
|
||||
Clean up the timers module.
|
||||
"""Clean up the timers module.
|
||||
|
||||
Stops the background scheduler task.
|
||||
|
||||
|
||||
@@ -32,8 +32,7 @@ from .scheduler import get_scheduler
|
||||
|
||||
@on_event(EventType.CHAT, priority=Priority.LOWEST)
|
||||
async def count_chat_message(ctx: EventContext[ChatEvent]) -> None:
|
||||
"""
|
||||
Count a chat message for all tracked timers.
|
||||
"""Count a chat message for all tracked timers.
|
||||
|
||||
Runs at lowest priority so all other CHAT handlers (moderation, etc.)
|
||||
execute first. Skips bot messages and hidden messages.
|
||||
@@ -60,8 +59,7 @@ async def count_chat_message(ctx: EventContext[ChatEvent]) -> None:
|
||||
|
||||
@on_event(EventType.VISIBILITY_UPDATE, priority=Priority.LOWEST)
|
||||
async def handle_visibility_update(ctx: EventContext[VisibilityUpdateEvent]) -> None:
|
||||
"""
|
||||
Remove hidden messages from chat counts.
|
||||
"""Remove hidden messages from chat counts.
|
||||
|
||||
Only handles the hide case. Un-hiding does not re-add messages because
|
||||
we cannot distinguish previously counted user messages from bot messages
|
||||
|
||||
@@ -33,8 +33,7 @@ _NAME_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]{0,31}$")
|
||||
|
||||
|
||||
async def _resolve_timer(ctx: CommandContext, identifier: str) -> aiosqlite.Row | None:
|
||||
"""
|
||||
Resolve a timer by numeric ID or name.
|
||||
"""Resolve a timer by numeric ID or name.
|
||||
|
||||
Tries parsing as an integer first, then falls back to a name lookup.
|
||||
|
||||
@@ -66,8 +65,7 @@ async def _resolve_timer(ctx: CommandContext, identifier: str) -> aiosqlite.Row
|
||||
|
||||
|
||||
def _timer_display(row: aiosqlite.Row) -> str:
|
||||
"""
|
||||
Format a timer's display identifier.
|
||||
"""Format a timer's display identifier.
|
||||
|
||||
:param row: Timer database row.
|
||||
:return: Display string like "timer_name (#3)" or "timer #3".
|
||||
@@ -79,8 +77,7 @@ def _timer_display(row: aiosqlite.Row) -> str:
|
||||
|
||||
@on_command("addtimer", requires_moderator=True)
|
||||
async def addtimer(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Create a new empty timer with an optional name.
|
||||
"""Create a new empty timer with an optional name.
|
||||
|
||||
Usage: !addtimer [name]
|
||||
|
||||
@@ -123,8 +120,7 @@ async def addtimer(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("settimermessage", requires_moderator=True)
|
||||
async def settimermessage(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Set the message text for a timer.
|
||||
"""Set the message text for a timer.
|
||||
|
||||
Usage: !settimermessage <id|name> <message>
|
||||
|
||||
@@ -164,8 +160,7 @@ async def settimermessage(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("settimerinterval", requires_moderator=True)
|
||||
async def settimerinterval(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Set the interval for a timer.
|
||||
"""Set the interval for a timer.
|
||||
|
||||
Accepts simple durations (15m, 1h30m) or cron expressions (*/15 * * * *).
|
||||
|
||||
@@ -212,8 +207,7 @@ async def settimerinterval(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("settimerlines", requires_moderator=True)
|
||||
async def settimerlines(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Set the minimum chat lines between timer firings.
|
||||
"""Set the minimum chat lines between timer firings.
|
||||
|
||||
Usage: !settimerlines <id|name> <count>
|
||||
|
||||
@@ -260,8 +254,7 @@ async def settimerlines(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("enabletimer", requires_moderator=True)
|
||||
async def enabletimer(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Enable a timer.
|
||||
"""Enable a timer.
|
||||
|
||||
Won't enable a timer that has no message set.
|
||||
|
||||
@@ -310,8 +303,7 @@ async def enabletimer(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("disabletimer", requires_moderator=True)
|
||||
async def disabletimer(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Disable a timer.
|
||||
"""Disable a timer.
|
||||
|
||||
Usage: !disabletimer <id|name>
|
||||
|
||||
@@ -351,8 +343,7 @@ async def disabletimer(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("deletetimer", requires_moderator=True)
|
||||
async def deletetimer(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Permanently delete a timer.
|
||||
"""Permanently delete a timer.
|
||||
|
||||
Usage: !deletetimer <id|name>
|
||||
|
||||
@@ -382,8 +373,7 @@ async def deletetimer(ctx: CommandContext) -> None:
|
||||
|
||||
@on_command("listtimers", requires_moderator=True, cooldown=15)
|
||||
async def listtimers(ctx: CommandContext) -> None:
|
||||
"""
|
||||
Send the URL to the timer list web page.
|
||||
"""Send the URL to the timer list web page.
|
||||
|
||||
Usage: !listtimers
|
||||
|
||||
|
||||
@@ -31,8 +31,7 @@ _jinja_env = jinja2.Environment(
|
||||
|
||||
@on_route("/list", methods=["GET"])
|
||||
async def timer_list_page(ctx: RouteContext) -> web.Response:
|
||||
"""
|
||||
Serve an HTML page listing all timers in a table.
|
||||
"""Serve an HTML page listing all timers in a table.
|
||||
|
||||
Columns: #, Name, Message, Interval, Min Lines, Status, Last Fired.
|
||||
Accessible at /owlbot/timers/list.
|
||||
|
||||
@@ -51,8 +51,7 @@ _MIN_CRON_MINUTES = 1
|
||||
|
||||
|
||||
def parse_interval(value: str) -> tuple[IntervalType, str]:
|
||||
"""
|
||||
Parse and validate an interval string.
|
||||
"""Parse and validate an interval string.
|
||||
|
||||
Simple durations (no spaces) are parsed with a regex. Cron expressions
|
||||
(contain spaces) are validated with cronsim. Returns the detected type
|
||||
@@ -72,8 +71,7 @@ def parse_interval(value: str) -> tuple[IntervalType, str]:
|
||||
|
||||
|
||||
def _parse_simple(value: str) -> tuple[IntervalType, str]:
|
||||
"""
|
||||
Parse and validate a simple duration string.
|
||||
"""Parse and validate a simple duration string.
|
||||
|
||||
:param value: Duration string like "30s", "5m", "1h30m".
|
||||
:return: Tuple of (IntervalType.SIMPLE, value).
|
||||
@@ -99,8 +97,7 @@ def _parse_simple(value: str) -> tuple[IntervalType, str]:
|
||||
|
||||
|
||||
def _parse_cron(value: str) -> tuple[IntervalType, str]:
|
||||
"""
|
||||
Validate a cron expression.
|
||||
"""Validate a cron expression.
|
||||
|
||||
:param value: Cron expression string (5 fields).
|
||||
:return: Tuple of (IntervalType.CRON, value).
|
||||
@@ -125,8 +122,7 @@ def _parse_cron(value: str) -> tuple[IntervalType, str]:
|
||||
|
||||
|
||||
def _duration_to_seconds(value: str) -> int:
|
||||
"""
|
||||
Convert a validated simple duration string to total seconds.
|
||||
"""Convert a validated simple duration string to total seconds.
|
||||
|
||||
:param value: A previously validated duration string.
|
||||
:return: Total seconds.
|
||||
@@ -141,8 +137,7 @@ def _duration_to_seconds(value: str) -> int:
|
||||
|
||||
|
||||
def _next_fire_time(row: aiosqlite.Row, now: datetime) -> datetime | None:
|
||||
"""
|
||||
Compute when a timer will next be time-due.
|
||||
"""Compute when a timer will next be time-due.
|
||||
|
||||
:param row: Database row with interval_type, interval_value, last_fired_at.
|
||||
:param now: Current UTC time.
|
||||
@@ -167,8 +162,7 @@ def _next_fire_time(row: aiosqlite.Row, now: datetime) -> datetime | None:
|
||||
|
||||
|
||||
def _is_timer_due(row: aiosqlite.Row, now: datetime) -> bool:
|
||||
"""
|
||||
Check whether a timer should fire based on its interval and last fire time.
|
||||
"""Check whether a timer should fire based on its interval and last fire time.
|
||||
|
||||
:param row: Database row with interval_type, interval_value, last_fired_at.
|
||||
:param now: Current UTC time.
|
||||
@@ -187,6 +181,7 @@ class TimerScheduler:
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize an empty scheduler with no running task."""
|
||||
self._task: asyncio.Task[None] | None = None
|
||||
self._counted_ids: dict[int, set[str]] = {}
|
||||
self._wake_event: asyncio.Event | None = None
|
||||
@@ -197,8 +192,7 @@ class TimerScheduler:
|
||||
return self._counted_ids
|
||||
|
||||
def init_counted_ids(self, timer_ids: list[int]) -> None:
|
||||
"""
|
||||
Initialize empty counter sets for the given timer IDs.
|
||||
"""Initialize empty counter sets for the given timer IDs.
|
||||
|
||||
Called during module setup to prepare tracking for enabled timers.
|
||||
|
||||
@@ -207,8 +201,7 @@ class TimerScheduler:
|
||||
self._counted_ids = {tid: set() for tid in timer_ids}
|
||||
|
||||
def start(self, ctx: ModuleContext) -> None:
|
||||
"""
|
||||
Start the background scheduler task.
|
||||
"""Start the background scheduler task.
|
||||
|
||||
:param ctx: The module context.
|
||||
"""
|
||||
@@ -219,8 +212,7 @@ class TimerScheduler:
|
||||
ctx.logger.debug("Timer scheduler started.")
|
||||
|
||||
async def stop(self, ctx: ModuleContext) -> None:
|
||||
"""
|
||||
Cancel the background scheduler task and wait for it to exit.
|
||||
"""Cancel the background scheduler task and wait for it to exit.
|
||||
|
||||
:param ctx: The module context.
|
||||
"""
|
||||
@@ -233,8 +225,7 @@ class TimerScheduler:
|
||||
ctx.logger.info("Timer scheduler stopped.")
|
||||
|
||||
def reschedule(self) -> None:
|
||||
"""
|
||||
Wake the scheduler so it recalculates the next fire time.
|
||||
"""Wake the scheduler so it recalculates the next fire time.
|
||||
|
||||
Called by timer management commands when timer state changes.
|
||||
"""
|
||||
@@ -242,8 +233,7 @@ class TimerScheduler:
|
||||
self._wake_event.set()
|
||||
|
||||
async def _scheduler_loop(self, ctx: ModuleContext, wake: asyncio.Event) -> None:
|
||||
"""
|
||||
Background loop that sleeps until the next timer is due and fires it.
|
||||
"""Background loop that sleeps until the next timer is due and fires it.
|
||||
|
||||
Uses an asyncio.Event to allow early wake-ups when timer state changes.
|
||||
|
||||
@@ -283,8 +273,7 @@ class TimerScheduler:
|
||||
ctx.logger.exception("Scheduler tick failed.")
|
||||
|
||||
async def _tick(self, ctx: ModuleContext) -> None:
|
||||
"""
|
||||
Single scheduler tick: query enabled timers and fire any that are due.
|
||||
"""Single scheduler tick: query enabled timers and fire any that are due.
|
||||
|
||||
:param ctx: The module context.
|
||||
"""
|
||||
@@ -342,8 +331,7 @@ class TimerScheduler:
|
||||
|
||||
|
||||
async def _compute_next_delay(ctx: ModuleContext) -> tuple[float | None, str | None]:
|
||||
"""
|
||||
Query enabled timers and return seconds until the soonest one is time-due.
|
||||
"""Query enabled timers and return seconds until the soonest one is time-due.
|
||||
|
||||
:param ctx: The module context.
|
||||
:return: Tuple of (seconds until next timer, display name of that timer),
|
||||
@@ -394,8 +382,7 @@ _scheduler: TimerScheduler | None = None
|
||||
|
||||
|
||||
def get_scheduler() -> TimerScheduler:
|
||||
"""
|
||||
Return the active scheduler instance.
|
||||
"""Return the active scheduler instance.
|
||||
|
||||
:return: The active TimerScheduler.
|
||||
:raises RuntimeError: If the scheduler has not been initialized.
|
||||
@@ -406,8 +393,7 @@ def get_scheduler() -> TimerScheduler:
|
||||
|
||||
|
||||
def set_scheduler(scheduler: TimerScheduler) -> None:
|
||||
"""
|
||||
Set the active scheduler instance.
|
||||
"""Set the active scheduler instance.
|
||||
|
||||
:param scheduler: The TimerScheduler to install.
|
||||
"""
|
||||
|
||||
@@ -48,8 +48,7 @@ type WebhookCallback = Callable[[EventType, Event], Coroutine[Any, Any, None]]
|
||||
|
||||
|
||||
class HttpServer:
|
||||
"""
|
||||
HTTP server for Owlbot.
|
||||
"""HTTP server for Owlbot.
|
||||
|
||||
Manages the aiohttp Application, built-in routes (webhook),
|
||||
and module-registered routes via the RouteDispatcher.
|
||||
@@ -61,8 +60,7 @@ class HttpServer:
|
||||
event_dispatch: WebhookCallback,
|
||||
route_dispatcher: RouteDispatcher,
|
||||
):
|
||||
"""
|
||||
Initialize the web server.
|
||||
"""Initialize the web server.
|
||||
|
||||
:param config: Bot configuration.
|
||||
:param event_dispatch: Async callback to dispatch webhook events.
|
||||
@@ -94,8 +92,7 @@ class HttpServer:
|
||||
logger.debug("HttpServer initialized.")
|
||||
|
||||
async def start(self, host: str, port: int) -> None:
|
||||
"""
|
||||
Start the HTTP server.
|
||||
"""Start the HTTP server.
|
||||
|
||||
:param host: Address to bind to.
|
||||
:param port: Port to bind to.
|
||||
|
||||
+14
-28
@@ -54,8 +54,7 @@ class ModuleLoader:
|
||||
config: Config,
|
||||
http_client: HttpClient,
|
||||
):
|
||||
"""
|
||||
Initialize the module loader.
|
||||
"""Initialize the module loader.
|
||||
|
||||
:param modules_dir: Path to the user modules directory.
|
||||
:param config: Configuration object for checking module enable/disable state.
|
||||
@@ -105,8 +104,7 @@ class ModuleLoader:
|
||||
)
|
||||
|
||||
def get_module_context(self, module_name: str) -> ModuleContext | None:
|
||||
"""
|
||||
Look up a module's context by name.
|
||||
"""Look up a module's context by name.
|
||||
|
||||
:param module_name: The module name.
|
||||
:return: The ModuleContext if the module is loaded, None otherwise.
|
||||
@@ -128,8 +126,7 @@ class ModuleLoader:
|
||||
return ctx
|
||||
|
||||
def discover_module_names(self) -> list[str]:
|
||||
"""
|
||||
Discover all loadable modules from both built-in and user directories.
|
||||
"""Discover all loadable modules from both built-in and user directories.
|
||||
|
||||
Built-in modules ship with the package. User modules are discovered
|
||||
from the configured modules directory. If a user module has the same
|
||||
@@ -160,8 +157,7 @@ class ModuleLoader:
|
||||
return modules
|
||||
|
||||
def _discover_user_module_names(self) -> set[str]:
|
||||
"""
|
||||
Scan the user modules directory for loadable modules.
|
||||
"""Scan the user modules directory for loadable modules.
|
||||
|
||||
Supports both single-file modules (``name.py``) and package modules
|
||||
(``name/__init__.py``). Files and directories starting with underscore
|
||||
@@ -197,8 +193,7 @@ class ModuleLoader:
|
||||
return found
|
||||
|
||||
async def load_all_modules(self) -> list[str]:
|
||||
"""
|
||||
Discover and load all enabled modules using two-phase loading.
|
||||
"""Discover and load all enabled modules using two-phase loading.
|
||||
|
||||
**Phase 1:** Import every module, create contexts, and register all
|
||||
decorated handlers (``@on_event``, ``@on_command``, ``@on_route``).
|
||||
@@ -255,8 +250,7 @@ class ModuleLoader:
|
||||
return loaded
|
||||
|
||||
async def load_module(self, module_name: str, *, _run_setup: bool = True) -> None:
|
||||
"""
|
||||
Load a single module by name.
|
||||
"""Load a single module by name.
|
||||
|
||||
If both a package and single-file form exist for the same name,
|
||||
the package form is used. Checks if the module is enabled in
|
||||
@@ -354,8 +348,7 @@ class ModuleLoader:
|
||||
raise ModuleLoadError(f"Failed to load module '{module_name}': {e}") from e
|
||||
|
||||
async def unload_module(self, module_name: str) -> bool:
|
||||
"""
|
||||
Unload a module, calling its teardown and cleaning up all state.
|
||||
"""Unload a module, calling its teardown and cleaning up all state.
|
||||
|
||||
:param module_name: The module to unload.
|
||||
:return: True if module was unloaded, False if not found.
|
||||
@@ -397,8 +390,7 @@ class ModuleLoader:
|
||||
return True
|
||||
|
||||
async def unload_all_modules(self) -> None:
|
||||
"""
|
||||
Unload all modules, calling teardown and cleaning up all state.
|
||||
"""Unload all modules, calling teardown and cleaning up all state.
|
||||
|
||||
Called during bot shutdown to allow modules to clean up resources.
|
||||
"""
|
||||
@@ -411,8 +403,7 @@ class ModuleLoader:
|
||||
logger.info("All module unload complete.")
|
||||
|
||||
async def _run_module_setup(self, module_name: str) -> None:
|
||||
"""
|
||||
Run a module's ``@on_setup`` hooks if any are defined.
|
||||
"""Run a module's ``@on_setup`` hooks if any are defined.
|
||||
|
||||
All setup handlers run inside a single storage transaction that is
|
||||
committed on success. If any handler fails, the transaction is
|
||||
@@ -449,8 +440,7 @@ class ModuleLoader:
|
||||
) from e
|
||||
|
||||
def _resolve_module_path(self, module_name: str) -> Path:
|
||||
"""
|
||||
Resolve the filesystem path for a module by name.
|
||||
"""Resolve the filesystem path for a module by name.
|
||||
|
||||
User modules are checked first (package form, then single-file).
|
||||
If no user module is found and the name is a built-in, the built-in
|
||||
@@ -478,8 +468,7 @@ class ModuleLoader:
|
||||
|
||||
@staticmethod
|
||||
def _resolve_builtin_module_path(module_name: str) -> Path:
|
||||
"""
|
||||
Locate a built-in module's ``__init__.py`` inside the package.
|
||||
"""Locate a built-in module's ``__init__.py`` inside the package.
|
||||
|
||||
:param module_name: Name of the built-in module.
|
||||
:return: Path to the module's ``__init__.py``.
|
||||
@@ -493,8 +482,7 @@ class ModuleLoader:
|
||||
)
|
||||
|
||||
def _register_module_handlers(self, module: ModuleType, module_name: str) -> None:
|
||||
"""
|
||||
Scan a module for decorated handlers and register them.
|
||||
"""Scan a module for decorated handlers and register them.
|
||||
|
||||
Delegates to each registry's ``register_from_module()`` method,
|
||||
which knows how to find its own decorator markers.
|
||||
@@ -510,8 +498,7 @@ class ModuleLoader:
|
||||
def _collect_lifecycle_handlers(
|
||||
module: ModuleType, marker: str
|
||||
) -> list[LifecycleHandler]:
|
||||
"""
|
||||
Collect callables from a module that have a given marker attribute.
|
||||
"""Collect callables from a module that have a given marker attribute.
|
||||
|
||||
Scans ``vars(module)`` for callable objects where ``getattr(obj, marker)``
|
||||
is truthy. Used to find ``@on_setup`` (marker ``"_owlbot_setup"``) and
|
||||
@@ -528,8 +515,7 @@ class ModuleLoader:
|
||||
]
|
||||
|
||||
def _cleanup_module(self, module_name: str) -> None:
|
||||
"""
|
||||
Remove all state associated with a module.
|
||||
"""Remove all state associated with a module.
|
||||
|
||||
Removes the top-level module entry and any submodule entries
|
||||
(for package-style modules) from ``sys.modules``.
|
||||
|
||||
@@ -41,16 +41,14 @@ logger = logging.getLogger("owlbot.commands")
|
||||
|
||||
|
||||
class CommandRegistry:
|
||||
"""
|
||||
Holds all registered commands for a bot instance.
|
||||
"""Holds all registered commands for a bot instance.
|
||||
|
||||
Instance-scoped to enable proper dependency injection and allow multiple
|
||||
bot instances to coexist without sharing state.
|
||||
"""
|
||||
|
||||
def __init__(self, prefix: str = "!") -> None:
|
||||
"""
|
||||
Initialize the command registry.
|
||||
"""Initialize the command registry.
|
||||
|
||||
:param prefix: Command prefix character (e.g., "!" for "!ping").
|
||||
"""
|
||||
@@ -76,8 +74,7 @@ class CommandRegistry:
|
||||
cooldown: int | float = 0,
|
||||
module_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Register a command handler.
|
||||
"""Register a command handler.
|
||||
|
||||
:param name: Primary command name (case-insensitive).
|
||||
:param handler: Async function to handle the command.
|
||||
@@ -123,8 +120,7 @@ class CommandRegistry:
|
||||
)
|
||||
|
||||
def unregister(self, name: str) -> bool:
|
||||
"""
|
||||
Unregister a command and all its aliases.
|
||||
"""Unregister a command and all its aliases.
|
||||
|
||||
:param name: The primary command name or any alias.
|
||||
:return: True if command was found and removed, False otherwise.
|
||||
@@ -149,8 +145,7 @@ class CommandRegistry:
|
||||
return True
|
||||
|
||||
def get(self, trigger: str) -> CommandInfo | None:
|
||||
"""
|
||||
Look up a command by name or alias.
|
||||
"""Look up a command by name or alias.
|
||||
|
||||
:param trigger: Command name or alias (case-insensitive).
|
||||
:return: CommandInfo if found, None otherwise.
|
||||
@@ -162,8 +157,7 @@ class CommandRegistry:
|
||||
return self._commands.get(primary)
|
||||
|
||||
def exists(self, trigger: str) -> bool:
|
||||
"""
|
||||
Check if a command is registered.
|
||||
"""Check if a command is registered.
|
||||
|
||||
:param trigger: Command name or alias (case-insensitive).
|
||||
:return: True if the command exists, False otherwise.
|
||||
@@ -171,16 +165,14 @@ class CommandRegistry:
|
||||
return trigger.lower() in self._aliases
|
||||
|
||||
def get_all(self) -> dict[str, CommandInfo]:
|
||||
"""
|
||||
Get all registered commands.
|
||||
"""Get all registered commands.
|
||||
|
||||
:return: Dict mapping primary command names to CommandInfo.
|
||||
"""
|
||||
return self._commands.copy()
|
||||
|
||||
def unregister_by_module(self, module_name: str) -> int:
|
||||
"""
|
||||
Remove all commands registered by a specific module.
|
||||
"""Remove all commands registered by a specific module.
|
||||
|
||||
:param module_name: The module whose commands should be removed.
|
||||
:return: Number of commands removed.
|
||||
@@ -197,8 +189,7 @@ class CommandRegistry:
|
||||
return len(to_remove)
|
||||
|
||||
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
||||
"""
|
||||
Scan a Python module for @on_command-decorated functions and register them.
|
||||
"""Scan a Python module for @on_command-decorated functions and register them.
|
||||
|
||||
Looks for functions with the ``_owlbot_command`` attribute set by
|
||||
the ``@on_command`` decorator and registers each one.
|
||||
@@ -222,8 +213,7 @@ class CommandRegistry:
|
||||
)
|
||||
|
||||
def parse(self, message: str) -> tuple[str, str] | None:
|
||||
"""
|
||||
Parse a message to extract command and arguments.
|
||||
"""Parse a message to extract command and arguments.
|
||||
|
||||
:param message: The chat message body.
|
||||
:return: Tuple of (command_name, args_string), or None if not a command.
|
||||
@@ -245,8 +235,7 @@ class CommandRegistry:
|
||||
|
||||
|
||||
class CommandDispatcher:
|
||||
"""
|
||||
Dispatches chat events to registered command handlers.
|
||||
"""Dispatches chat events to registered command handlers.
|
||||
|
||||
Parses messages, checks authentication/moderator requirements,
|
||||
and calls the appropriate command handler.
|
||||
@@ -260,8 +249,7 @@ class CommandDispatcher:
|
||||
loaded_modules: set[str],
|
||||
command_prefix: str = "!",
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the command dispatcher.
|
||||
"""Initialize the command dispatcher.
|
||||
|
||||
Creates and owns a :class:`CommandRegistry` internally.
|
||||
|
||||
@@ -296,8 +284,7 @@ class CommandDispatcher:
|
||||
*,
|
||||
aliases: list[str] | tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Register a built-in command handler.
|
||||
"""Register a built-in command handler.
|
||||
|
||||
Built-in commands use a simpler handler signature (event, owncast_client)
|
||||
and don't require module infrastructure.
|
||||
@@ -328,8 +315,7 @@ class CommandDispatcher:
|
||||
cooldown: int | float = 0,
|
||||
module_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Register a command handler.
|
||||
"""Register a command handler.
|
||||
|
||||
Delegates to the internal CommandRegistry.
|
||||
|
||||
@@ -355,8 +341,7 @@ class CommandDispatcher:
|
||||
self._cooldown_tracker.pop(name.lower(), None)
|
||||
|
||||
def unregister(self, name: str) -> bool:
|
||||
"""
|
||||
Unregister a command and all its aliases.
|
||||
"""Unregister a command and all its aliases.
|
||||
|
||||
Delegates to the internal CommandRegistry.
|
||||
|
||||
@@ -371,8 +356,7 @@ class CommandDispatcher:
|
||||
return result
|
||||
|
||||
def get(self, trigger: str) -> CommandInfo | None:
|
||||
"""
|
||||
Look up a command by name or alias.
|
||||
"""Look up a command by name or alias.
|
||||
|
||||
Delegates to the internal CommandRegistry.
|
||||
|
||||
@@ -382,8 +366,7 @@ class CommandDispatcher:
|
||||
return self._command_registry.get(trigger)
|
||||
|
||||
def exists(self, trigger: str) -> bool:
|
||||
"""
|
||||
Check if a command is registered.
|
||||
"""Check if a command is registered.
|
||||
|
||||
Delegates to the internal CommandRegistry.
|
||||
|
||||
@@ -393,8 +376,7 @@ class CommandDispatcher:
|
||||
return self._command_registry.exists(trigger)
|
||||
|
||||
def get_by_module(self, module_name: str) -> dict[str, CommandInfo]:
|
||||
"""
|
||||
Get all commands registered by a specific module.
|
||||
"""Get all commands registered by a specific module.
|
||||
|
||||
:param module_name: The module whose commands to return.
|
||||
:return: Dict mapping primary command names to CommandInfo for that module.
|
||||
@@ -406,8 +388,7 @@ class CommandDispatcher:
|
||||
}
|
||||
|
||||
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
||||
"""
|
||||
Scan a Python module for @on_command-decorated functions and register them.
|
||||
"""Scan a Python module for @on_command-decorated functions and register them.
|
||||
|
||||
Delegates to the internal CommandRegistry.
|
||||
|
||||
@@ -417,8 +398,7 @@ class CommandDispatcher:
|
||||
self._command_registry.register_from_module(module, module_name)
|
||||
|
||||
def unregister_by_module(self, module_name: str) -> int:
|
||||
"""
|
||||
Remove all commands registered by a specific module.
|
||||
"""Remove all commands registered by a specific module.
|
||||
|
||||
Delegates to the internal CommandRegistry.
|
||||
|
||||
@@ -432,8 +412,7 @@ class CommandDispatcher:
|
||||
return self._command_registry.unregister_by_module(module_name)
|
||||
|
||||
async def dispatch(self, event: ChatEvent) -> None:
|
||||
"""
|
||||
Dispatch a chat event to the appropriate command handler if it's a command.
|
||||
"""Dispatch a chat event to the appropriate command handler if it's a command.
|
||||
|
||||
:param event: The chat event to check for commands.
|
||||
"""
|
||||
@@ -605,8 +584,7 @@ class CommandDispatcher:
|
||||
|
||||
|
||||
class ModuleCommands:
|
||||
"""
|
||||
Module-scoped wrapper around CommandDispatcher.
|
||||
"""Module-scoped wrapper around CommandDispatcher.
|
||||
|
||||
This wrapper auto-supplies the module name for registration operations,
|
||||
so modules don't need to pass their own name back into the API.
|
||||
@@ -614,8 +592,7 @@ class ModuleCommands:
|
||||
"""
|
||||
|
||||
def __init__(self, dispatcher: CommandDispatcher, module_name: str) -> None:
|
||||
"""
|
||||
Initialize the module-scoped command wrapper.
|
||||
"""Initialize the module-scoped command wrapper.
|
||||
|
||||
:param dispatcher: The CommandDispatcher that owns the command registry.
|
||||
:param module_name: The name of the module using this wrapper.
|
||||
@@ -643,8 +620,7 @@ class ModuleCommands:
|
||||
requires_moderator: bool = False,
|
||||
cooldown: int | float = 0,
|
||||
) -> None:
|
||||
"""
|
||||
Register a command handler for this module.
|
||||
"""Register a command handler for this module.
|
||||
|
||||
The module name is automatically supplied.
|
||||
|
||||
@@ -667,8 +643,7 @@ class ModuleCommands:
|
||||
)
|
||||
|
||||
def unregister(self, name: str) -> bool:
|
||||
"""
|
||||
Unregister a command and all its aliases.
|
||||
"""Unregister a command and all its aliases.
|
||||
|
||||
Only commands registered by this module can be unregistered.
|
||||
|
||||
@@ -682,8 +657,7 @@ class ModuleCommands:
|
||||
return self._dispatcher.unregister(name)
|
||||
|
||||
def get(self, trigger: str) -> CommandInfo | None:
|
||||
"""
|
||||
Look up a command by name or alias within this module's registrations.
|
||||
"""Look up a command by name or alias within this module's registrations.
|
||||
|
||||
:param trigger: Command name or alias (case-insensitive).
|
||||
:return: CommandInfo if found and owned by this module, None otherwise.
|
||||
@@ -694,8 +668,7 @@ class ModuleCommands:
|
||||
return info
|
||||
|
||||
def exists(self, trigger: str) -> bool:
|
||||
"""
|
||||
Check if a command is registered across all modules.
|
||||
"""Check if a command is registered across all modules.
|
||||
|
||||
:param trigger: Command name or alias (case-insensitive).
|
||||
:return: True if the command exists, False otherwise.
|
||||
|
||||
+23
-46
@@ -49,8 +49,7 @@ logger = logging.getLogger("owlbot.events")
|
||||
|
||||
|
||||
class EventRegistry:
|
||||
"""
|
||||
Holds all registered event handlers for a bot instance.
|
||||
"""Holds all registered event handlers for a bot instance.
|
||||
|
||||
Instance-scoped to enable proper dependency injection and allow multiple
|
||||
bot instances to coexist without sharing state.
|
||||
@@ -69,8 +68,7 @@ class EventRegistry:
|
||||
module_name: str,
|
||||
priority: int = Priority.NORMAL,
|
||||
) -> None:
|
||||
"""
|
||||
Register a handler for the given event types.
|
||||
"""Register a handler for the given event types.
|
||||
|
||||
Called by the module loader after scanning for decorated functions.
|
||||
|
||||
@@ -97,8 +95,7 @@ class EventRegistry:
|
||||
)
|
||||
|
||||
def unregister(self, handler: EventHandler) -> bool:
|
||||
"""
|
||||
Unregister a handler from all event types it is registered for.
|
||||
"""Unregister a handler from all event types it is registered for.
|
||||
|
||||
:param handler: The handler function to unregister.
|
||||
:return: True if handler was found and removed, False otherwise.
|
||||
@@ -126,8 +123,7 @@ class EventRegistry:
|
||||
return False
|
||||
|
||||
def get(self, event_type: EventType) -> list[HandlerEntry]:
|
||||
"""
|
||||
Get all handlers registered for a specific event type.
|
||||
"""Get all handlers registered for a specific event type.
|
||||
|
||||
:param event_type: The event type to look up.
|
||||
:return: List of (handler, module_name, priority) tuples.
|
||||
@@ -135,8 +131,7 @@ class EventRegistry:
|
||||
return self._handlers.get(event_type.value, [])
|
||||
|
||||
def get_all(self) -> EventHandlerMap:
|
||||
"""
|
||||
Get a copy of the entire handler registry.
|
||||
"""Get a copy of the entire handler registry.
|
||||
|
||||
:return: Dict mapping event type values to handler lists.
|
||||
"""
|
||||
@@ -145,8 +140,7 @@ class EventRegistry:
|
||||
}
|
||||
|
||||
def get_handler_module(self, handler: EventHandler) -> str | None:
|
||||
"""
|
||||
Look up which module registered a given handler.
|
||||
"""Look up which module registered a given handler.
|
||||
|
||||
:param handler: The handler function to look up.
|
||||
:return: The module name if found, None otherwise.
|
||||
@@ -158,8 +152,7 @@ class EventRegistry:
|
||||
return None
|
||||
|
||||
def unregister_by_module(self, module_name: str) -> int:
|
||||
"""
|
||||
Remove all handlers registered by a specific module.
|
||||
"""Remove all handlers registered by a specific module.
|
||||
|
||||
:param module_name: The module whose handlers should be removed.
|
||||
:return: Number of handlers removed.
|
||||
@@ -181,8 +174,7 @@ class EventRegistry:
|
||||
return len(seen)
|
||||
|
||||
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
||||
"""
|
||||
Scan a Python module for @on_event-decorated functions and register them.
|
||||
"""Scan a Python module for @on_event-decorated functions and register them.
|
||||
|
||||
Looks for functions with the ``_owlbot_event`` attribute set by
|
||||
the ``@on_event`` decorator and registers each one.
|
||||
@@ -201,8 +193,7 @@ class EventRegistry:
|
||||
|
||||
|
||||
class EventDispatcher:
|
||||
"""
|
||||
Dispatches events to registered handlers sequentially by priority.
|
||||
"""Dispatches events to registered handlers sequentially by priority.
|
||||
|
||||
After all event handlers complete, command dispatch is triggered for
|
||||
CHAT events (via the injected command_dispatch callback).
|
||||
@@ -214,8 +205,7 @@ class EventDispatcher:
|
||||
get_module_context: Callable[[str], ModuleContext],
|
||||
handler_timeout: float,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the event dispatcher.
|
||||
"""Initialize the event dispatcher.
|
||||
|
||||
Creates and owns a :class:`EventRegistry` internally.
|
||||
|
||||
@@ -237,8 +227,7 @@ class EventDispatcher:
|
||||
module_name: str,
|
||||
priority: int = Priority.NORMAL,
|
||||
) -> None:
|
||||
"""
|
||||
Register a handler for the given event types.
|
||||
"""Register a handler for the given event types.
|
||||
|
||||
A single handler can respond to multiple event types; the registry
|
||||
stores a separate entry per event type. This is the shared entry
|
||||
@@ -255,8 +244,7 @@ class EventDispatcher:
|
||||
self._handler_registry.register(handler, event_types, module_name, priority)
|
||||
|
||||
def unregister(self, handler: EventHandler) -> bool:
|
||||
"""
|
||||
Unregister a handler from all event types it is registered for.
|
||||
"""Unregister a handler from all event types it is registered for.
|
||||
|
||||
Unlike commands (looked up by name string), event handlers are
|
||||
identified by object identity. A handler registered for multiple
|
||||
@@ -268,8 +256,7 @@ class EventDispatcher:
|
||||
return self._handler_registry.unregister(handler)
|
||||
|
||||
def get_by_module(self, module_name: str) -> EventHandlerMap:
|
||||
"""
|
||||
Get all handlers registered by a specific module, grouped by event type.
|
||||
"""Get all handlers registered by a specific module, grouped by event type.
|
||||
|
||||
The registry stores handlers grouped by event type, not by module,
|
||||
so this filters across all event types to collect a single module's
|
||||
@@ -288,8 +275,7 @@ class EventDispatcher:
|
||||
return result
|
||||
|
||||
def get_handler_module(self, handler: EventHandler) -> str | None:
|
||||
"""
|
||||
Reverse-lookup which module registered a given handler.
|
||||
"""Reverse-lookup which module registered a given handler.
|
||||
|
||||
Scans all event types since handlers are stored by event type,
|
||||
not by module.
|
||||
@@ -300,8 +286,7 @@ class EventDispatcher:
|
||||
return self._handler_registry.get_handler_module(handler)
|
||||
|
||||
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
||||
"""
|
||||
Scan a Python module for ``@on_event``-decorated functions and register them.
|
||||
"""Scan a Python module for ``@on_event``-decorated functions and register them.
|
||||
|
||||
This is the import-phase entry point: the module loader calls it once
|
||||
per module. Decorator attributes are read here and passed as explicit
|
||||
@@ -314,8 +299,7 @@ class EventDispatcher:
|
||||
self._handler_registry.register_from_module(module, module_name)
|
||||
|
||||
def unregister_by_module(self, module_name: str) -> int:
|
||||
"""
|
||||
Remove all handlers registered by a specific module.
|
||||
"""Remove all handlers registered by a specific module.
|
||||
|
||||
Used during module teardown to clean up all of a module's handlers
|
||||
in one call, regardless of which event types they were registered for.
|
||||
@@ -326,8 +310,7 @@ class EventDispatcher:
|
||||
return self._handler_registry.unregister_by_module(module_name)
|
||||
|
||||
async def dispatch(self, event_type: EventType, event: Event) -> None:
|
||||
"""
|
||||
Dispatch an event to handlers sequentially by priority, then to commands.
|
||||
"""Dispatch an event to handlers sequentially by priority, then to commands.
|
||||
|
||||
:param event_type: The type of event to dispatch.
|
||||
:param event: The parsed event instance.
|
||||
@@ -391,8 +374,7 @@ class EventDispatcher:
|
||||
module_name: str,
|
||||
propagation: PropagationState,
|
||||
) -> None:
|
||||
"""
|
||||
Call a single handler with timeout enforcement and transaction management.
|
||||
"""Call a single handler with timeout enforcement and transaction management.
|
||||
|
||||
:param handler: The handler function to call.
|
||||
:param event: The event to pass to the handler.
|
||||
@@ -444,8 +426,7 @@ class EventDispatcher:
|
||||
|
||||
|
||||
class ModuleEvents:
|
||||
"""
|
||||
Module-scoped wrapper around EventDispatcher.
|
||||
"""Module-scoped wrapper around EventDispatcher.
|
||||
|
||||
This wrapper auto-supplies the module name for registration operations,
|
||||
so modules don't need to pass their own name back into the API.
|
||||
@@ -453,8 +434,7 @@ class ModuleEvents:
|
||||
"""
|
||||
|
||||
def __init__(self, dispatcher: EventDispatcher, module_name: str) -> None:
|
||||
"""
|
||||
Initialize the module-scoped handler wrapper.
|
||||
"""Initialize the module-scoped handler wrapper.
|
||||
|
||||
:param dispatcher: The EventDispatcher that owns the handler registry.
|
||||
:param module_name: The name of the module using this wrapper.
|
||||
@@ -473,8 +453,7 @@ class ModuleEvents:
|
||||
event_types: tuple[EventType, ...],
|
||||
priority: int = Priority.NORMAL,
|
||||
) -> None:
|
||||
"""
|
||||
Register an event handler for this module.
|
||||
"""Register an event handler for this module.
|
||||
|
||||
The module name is automatically supplied.
|
||||
|
||||
@@ -491,8 +470,7 @@ class ModuleEvents:
|
||||
)
|
||||
|
||||
def unregister(self, handler: EventHandler) -> bool:
|
||||
"""
|
||||
Unregister a handler from all event types it is registered for.
|
||||
"""Unregister a handler from all event types it is registered for.
|
||||
|
||||
Only handlers registered by this module can be unregistered.
|
||||
|
||||
@@ -505,8 +483,7 @@ class ModuleEvents:
|
||||
return self._dispatcher.unregister(handler)
|
||||
|
||||
def get(self, event_type: EventType) -> list[HandlerEntry]:
|
||||
"""
|
||||
Get handlers registered by this module for a specific event type.
|
||||
"""Get handlers registered by this module for a specific event type.
|
||||
|
||||
:param event_type: The event type to look up.
|
||||
:return: List of (handler, module_name, priority) tuples for this module only.
|
||||
|
||||
+27
-54
@@ -39,8 +39,7 @@ logger = logging.getLogger("owlbot.web")
|
||||
|
||||
|
||||
class RouteRegistry:
|
||||
"""
|
||||
Holds all registered routes for a bot instance.
|
||||
"""Holds all registered routes for a bot instance.
|
||||
|
||||
Routes are namespaced by module to prevent conflicts. Supports path
|
||||
patterns using aiohttp's ``{name}`` and ``{name:regex}`` syntax via
|
||||
@@ -64,8 +63,7 @@ class RouteRegistry:
|
||||
methods: list[str] | None = None,
|
||||
module_name: str,
|
||||
) -> RouteInfo:
|
||||
"""
|
||||
Register a route handler.
|
||||
"""Register a route handler.
|
||||
|
||||
:param path: URL path relative to module namespace. Supports
|
||||
``{name}`` and ``{name:regex}`` patterns.
|
||||
@@ -108,8 +106,7 @@ class RouteRegistry:
|
||||
return info
|
||||
|
||||
def unregister(self, full_path: str) -> bool:
|
||||
"""
|
||||
Unregister a route by its full path.
|
||||
"""Unregister a route by its full path.
|
||||
|
||||
:param full_path: The full route path including namespace.
|
||||
:return: True if route was found and removed, False otherwise.
|
||||
@@ -134,8 +131,7 @@ class RouteRegistry:
|
||||
return False
|
||||
|
||||
def get(self, full_path: str) -> RouteInfo | None:
|
||||
"""
|
||||
Look up a route by its full path (exact match on registered pattern).
|
||||
"""Look up a route by its full path (exact match on registered pattern).
|
||||
|
||||
:param full_path: The full route path including namespace.
|
||||
:return: RouteInfo if found, None otherwise.
|
||||
@@ -146,8 +142,7 @@ class RouteRegistry:
|
||||
return None
|
||||
|
||||
def match(self, full_path: str) -> tuple[RouteInfo, dict[str, str]] | None:
|
||||
"""
|
||||
Match a request path against registered routes.
|
||||
"""Match a request path against registered routes.
|
||||
|
||||
Scans routes in registration order (first match wins). Uses
|
||||
``DynamicResource._match()`` for both plain and parameterized paths.
|
||||
@@ -162,16 +157,14 @@ class RouteRegistry:
|
||||
return None
|
||||
|
||||
def get_all(self) -> dict[str, RouteInfo]:
|
||||
"""
|
||||
Get all registered routes.
|
||||
"""Get all registered routes.
|
||||
|
||||
:return: Dict mapping full paths to RouteInfo.
|
||||
"""
|
||||
return {info.full_path: info for _, info in self._routes}
|
||||
|
||||
def get_by_module(self, module_name: str) -> list[RouteInfo]:
|
||||
"""
|
||||
Get all routes registered by a specific module.
|
||||
"""Get all routes registered by a specific module.
|
||||
|
||||
:param module_name: The module name.
|
||||
:return: List of RouteInfo for that module.
|
||||
@@ -180,8 +173,7 @@ class RouteRegistry:
|
||||
return [info for _, info in self._routes if info.full_path in paths]
|
||||
|
||||
def unregister_by_module(self, module_name: str) -> int:
|
||||
"""
|
||||
Remove all routes registered by a specific module.
|
||||
"""Remove all routes registered by a specific module.
|
||||
|
||||
:param module_name: The module whose routes should be removed.
|
||||
:return: Number of routes removed.
|
||||
@@ -202,8 +194,7 @@ class RouteRegistry:
|
||||
return count
|
||||
|
||||
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
||||
"""
|
||||
Scan a Python module for @on_route-decorated functions and register them.
|
||||
"""Scan a Python module for @on_route-decorated functions and register them.
|
||||
|
||||
Looks for functions with the ``_owlbot_route`` attribute set by
|
||||
the ``@on_route`` decorator and registers each one.
|
||||
@@ -225,8 +216,7 @@ class RouteRegistry:
|
||||
|
||||
|
||||
class RouteDispatcher:
|
||||
"""
|
||||
Dispatches HTTP requests to registered module route handlers.
|
||||
"""Dispatches HTTP requests to registered module route handlers.
|
||||
|
||||
Looks up routes in the RouteRegistry, validates methods, creates
|
||||
RouteContext, and calls the handler with timeout and transaction management.
|
||||
@@ -237,8 +227,7 @@ class RouteDispatcher:
|
||||
get_module_context: Callable[[str], ModuleContext],
|
||||
handler_timeout: float,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the route dispatcher.
|
||||
"""Initialize the route dispatcher.
|
||||
|
||||
Creates and owns a :class:`RouteRegistry` internally.
|
||||
|
||||
@@ -258,8 +247,7 @@ class RouteDispatcher:
|
||||
methods: list[str] | None = None,
|
||||
module_name: str,
|
||||
) -> RouteInfo:
|
||||
"""
|
||||
Register a route handler.
|
||||
"""Register a route handler.
|
||||
|
||||
Delegates to the internal RouteRegistry.
|
||||
|
||||
@@ -278,8 +266,7 @@ class RouteDispatcher:
|
||||
)
|
||||
|
||||
def unregister(self, full_path: str) -> bool:
|
||||
"""
|
||||
Unregister a route by its full path.
|
||||
"""Unregister a route by its full path.
|
||||
|
||||
Delegates to the internal RouteRegistry.
|
||||
|
||||
@@ -289,8 +276,7 @@ class RouteDispatcher:
|
||||
return self._route_registry.unregister(full_path)
|
||||
|
||||
def get(self, full_path: str) -> RouteInfo | None:
|
||||
"""
|
||||
Look up a route by its full path.
|
||||
"""Look up a route by its full path.
|
||||
|
||||
Delegates to the internal RouteRegistry.
|
||||
|
||||
@@ -300,8 +286,7 @@ class RouteDispatcher:
|
||||
return self._route_registry.get(full_path)
|
||||
|
||||
def get_by_module(self, module_name: str) -> list[RouteInfo]:
|
||||
"""
|
||||
Get all routes registered by a specific module.
|
||||
"""Get all routes registered by a specific module.
|
||||
|
||||
Delegates to the internal RouteRegistry.
|
||||
|
||||
@@ -311,8 +296,7 @@ class RouteDispatcher:
|
||||
return self._route_registry.get_by_module(module_name)
|
||||
|
||||
def register_from_module(self, module: ModuleType, module_name: str) -> None:
|
||||
"""
|
||||
Scan a Python module for @on_route-decorated functions and register them.
|
||||
"""Scan a Python module for @on_route-decorated functions and register them.
|
||||
|
||||
Delegates to the internal RouteRegistry.
|
||||
|
||||
@@ -322,8 +306,7 @@ class RouteDispatcher:
|
||||
self._route_registry.register_from_module(module, module_name)
|
||||
|
||||
def unregister_by_module(self, module_name: str) -> int:
|
||||
"""
|
||||
Remove all routes registered by a specific module.
|
||||
"""Remove all routes registered by a specific module.
|
||||
|
||||
Delegates to the internal RouteRegistry.
|
||||
|
||||
@@ -333,8 +316,7 @@ class RouteDispatcher:
|
||||
return self._route_registry.unregister_by_module(module_name)
|
||||
|
||||
async def dispatch(self, request: web.Request) -> web.StreamResponse:
|
||||
"""
|
||||
Dispatch an HTTP request to the appropriate module route handler.
|
||||
"""Dispatch an HTTP request to the appropriate module route handler.
|
||||
|
||||
Extracts module_name and path from the URL, matches it against
|
||||
registered routes (supporting path patterns), validates the HTTP
|
||||
@@ -374,8 +356,7 @@ class RouteDispatcher:
|
||||
route_info: RouteInfo,
|
||||
match_info: dict[str, str] | None = None,
|
||||
) -> web.StreamResponse:
|
||||
"""
|
||||
Handle an HTTP request to a module-registered route.
|
||||
"""Handle an HTTP request to a module-registered route.
|
||||
|
||||
:param request: The aiohttp request object.
|
||||
:param route_info: Information about the registered route.
|
||||
@@ -447,8 +428,7 @@ class RouteDispatcher:
|
||||
|
||||
|
||||
class ModuleRoutes:
|
||||
"""
|
||||
Module-scoped wrapper around RouteDispatcher.
|
||||
"""Module-scoped wrapper around RouteDispatcher.
|
||||
|
||||
This wrapper auto-supplies the module name for route operations,
|
||||
so modules don't need to know the internal routing namespace.
|
||||
@@ -458,8 +438,7 @@ class ModuleRoutes:
|
||||
def __init__(
|
||||
self, dispatcher: RouteDispatcher, module_name: str, public_base_url: str
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the module-scoped routes wrapper.
|
||||
"""Initialize the module-scoped routes wrapper.
|
||||
|
||||
:param dispatcher: The RouteDispatcher that owns the route registry.
|
||||
:param module_name: The name of the module using this wrapper.
|
||||
@@ -475,8 +454,7 @@ class ModuleRoutes:
|
||||
return self._dispatcher.get_by_module(self._module_name)
|
||||
|
||||
def url_for(self, path: str) -> str:
|
||||
"""
|
||||
Build a public URL for a route registered by this module.
|
||||
"""Build a public URL for a route registered by this module.
|
||||
|
||||
:param path: The route path (e.g., "/list").
|
||||
:return: Full public URL (e.g., "http://host/owlbot/quotes/list").
|
||||
@@ -490,8 +468,7 @@ class ModuleRoutes:
|
||||
*,
|
||||
methods: list[str] | None = None,
|
||||
) -> RouteInfo:
|
||||
"""
|
||||
Register a route handler for this module.
|
||||
"""Register a route handler for this module.
|
||||
|
||||
The module name is automatically supplied.
|
||||
|
||||
@@ -509,8 +486,7 @@ class ModuleRoutes:
|
||||
)
|
||||
|
||||
def unregister(self, path: str) -> bool:
|
||||
"""
|
||||
Unregister a route by its relative path.
|
||||
"""Unregister a route by its relative path.
|
||||
|
||||
:param path: Relative route path (e.g., "/stats").
|
||||
:return: True if route was found and removed, False otherwise.
|
||||
@@ -518,8 +494,7 @@ class ModuleRoutes:
|
||||
return self._dispatcher.unregister(self._full_path(path))
|
||||
|
||||
def get(self, path: str) -> RouteInfo | None:
|
||||
"""
|
||||
Look up a route by its relative path.
|
||||
"""Look up a route by its relative path.
|
||||
|
||||
:param path: Relative route path (e.g., "/stats").
|
||||
:return: RouteInfo if found, None otherwise.
|
||||
@@ -527,8 +502,7 @@ class ModuleRoutes:
|
||||
return self._dispatcher.get(self._full_path(path))
|
||||
|
||||
def exists(self, path: str) -> bool:
|
||||
"""
|
||||
Check if a route is registered at the given relative path.
|
||||
"""Check if a route is registered at the given relative path.
|
||||
|
||||
:param path: Relative route path (e.g., "/stats").
|
||||
:return: True if the route exists, False otherwise.
|
||||
@@ -536,8 +510,7 @@ class ModuleRoutes:
|
||||
return self.get(path) is not None
|
||||
|
||||
def _full_path(self, path: str) -> str:
|
||||
"""
|
||||
Normalize a relative path into the full namespaced path.
|
||||
"""Normalize a relative path into the full namespaced path.
|
||||
|
||||
:param path: Relative route path (e.g., "/list" or "list").
|
||||
:return: Full path (e.g., "/owlbot/quotes/list").
|
||||
|
||||
@@ -77,6 +77,11 @@ select = [
|
||||
"SIM", # flake8-simplify
|
||||
"TCH", # flake8-type-checking
|
||||
"RUF", # Ruff-specific rules
|
||||
"D", # pydocstyle
|
||||
]
|
||||
ignore = [
|
||||
"D203", # incompatible with D211 (no blank line before class docstring)
|
||||
"D213", # incompatible with D212 (summary on first line)
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Shared test fixtures for the Owlbot test suite."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -27,5 +29,6 @@ if TYPE_CHECKING:
|
||||
|
||||
@pytest.fixture
|
||||
async def storage(tmp_path: Path) -> AsyncIterator[ModuleStorage]:
|
||||
"""Yield an open ModuleStorage backed by a temporary directory."""
|
||||
async with ModuleStorage(tmp_path, "test_module") as s:
|
||||
yield s
|
||||
|
||||
Reference in New Issue
Block a user