"""API Schemas for guild domain."""
from __future__ import annotations
from enum import Enum
from uuid import UUID # noqa: TC003
from pydantic import Field
from byte_api.lib.schema import CamelizedBaseModel
__all__ = (
"AllowedUserSetting",
"AllowedUsersConfigSchema",
"BaseGuildSetting",
"ForumConfigSchema",
"ForumSetting",
"GitHubConfigSchema",
"GitHubConfigSetting",
"GuildCreate",
"GuildModelSetting",
"GuildSchema",
"GuildUpdate",
"SOTagsConfigSchema",
"SOTagsSetting",
"UpdateableGuildSetting",
"UpdateableGuildSettingEnum",
)
[docs]
class GitHubConfigSchema(CamelizedBaseModel):
"""Schema for validating GitHub configuration."""
guild_id: UUID
discussion_sync: bool
github_organization: str | None
github_repository: str | None
[docs]
class AllowedUsersConfigSchema(CamelizedBaseModel):
"""Schema for validating allowed users for certain admin actions within a guild."""
guild_id: UUID
user_id: UUID
[docs]
class ForumConfigSchema(CamelizedBaseModel):
"""Schema for validating forum configuration."""
guild_id: UUID
help_forum: bool = Field(title="Help Forum", description="Is the help forum enabled.")
help_forum_category: str
help_thread_auto_close: bool
help_thread_auto_close_days: int
help_thread_notify: bool
help_thread_notify_roles: list[int]
help_thread_notify_days: int
help_thread_sync: bool
showcase_forum: bool
showcase_forum_category: str
showcase_thread_auto_close: bool
showcase_thread_auto_close_days: int
[docs]
class GuildSchema(CamelizedBaseModel):
"""Schema representing an existing guild."""
internal_id: UUID = Field(title="Internal ID", description="The internal database record ID.", alias="id")
guild_id: int = Field(title="Guild ID", description="The guild ID.")
guild_name: str = Field(title="Name", description="The guild name.")
prefix: str | None = Field(title="Prefix", description="The prefix for the guild.")
help_channel_id: int | None = Field(title="Help Channel ID", description="The channel ID for the help channel.")
sync_label: str | None = Field(
title="Sync Label", description="The forum label to use for GitHub discussion syncs."
)
issue_linking: bool | None = Field(title="Issue Linking", description="Is issue linking enabled.")
comment_linking: bool | None = Field(title="Comment Linking", description="Is comment linking enabled.")
pep_linking: bool | None = Field(title="PEP Linking", description="Is PEP linking enabled.")
github_config: GitHubConfigSchema | None = Field(
title="GitHub Config", description="The GitHub configuration for the guild."
)
sotags_configs: list[SOTagsConfigSchema] = Field(
title="StackOverflow Tags Configs", description="The StackOverflow tags configuration for the guild."
)
allowed_users: list[AllowedUsersConfigSchema] = Field(
title="Allowed Users", description="The allowed users configuration for the guild."
)
forum_config: ForumConfigSchema | None = Field(
title="Forum Config", description="The forum configuration for the guild."
)
[docs]
class GuildCreate(CamelizedBaseModel):
"""Schema representing a guild create request.
.. todo:: Add owner ID
"""
guild_id: int = Field(title="Guild ID", description="The guild ID.", alias="id")
name: str = Field(title="Name", description="The guild name.")
[docs]
class GuildUpdate(CamelizedBaseModel):
"""Schema representing a guild update request."""
guild_id: int = Field(title="Guild ID", description="The guild ID.", alias="id")
prefix: str | None = Field(title="Prefix", description="The prefix for the guild.")
help_channel_id: int | None = Field(title="Help Channel ID", description="The channel ID for the help channel.")
sync_label: str | None = Field(
title="Sync Label", description="The forum label to use for GitHub discussion syncs."
)
issue_linking: bool | None = Field(title="Issue Linking", description="Is issue linking enabled.")
comment_linking: bool | None = Field(title="Comment Linking", description="Is comment linking enabled.")
pep_linking: bool | None = Field(title="PEP Linking", description="Is PEP linking enabled.")
[docs]
class BaseGuildSetting(CamelizedBaseModel):
"""Base class for guild settings providing common serialization behavior."""
[docs]
class GuildModelSetting(BaseGuildSetting):
"""Core guild model settings."""
prefix: str = Field(title="Prefix", description="The prefix for the guild.")
help_channel_id: int = Field(title="Help Channel ID", description="The channel ID for the help forum.")
showcase_channel_id: int = Field(title="Showcase Channel ID", description="The channel ID for the showcase forum.")
sync_label: str = Field(title="Sync Label", description="The forum label to use for GitHub discussion syncs.")
issue_linking: bool = Field(title="Issue Linking", description="Is issue linking enabled.")
comment_linking: bool = Field(title="Comment Linking", description="Is comment linking enabled.")
pep_linking: bool = Field(title="PEP Linking", description="Is PEP linking enabled.")
[docs]
class GitHubConfigSetting(BaseGuildSetting):
"""GitHub-related settings for a guild."""
discussion_sync: bool = Field(title="Discussion Sync", description="Is GitHub discussion sync enabled.")
github_organization: str = Field(title="GitHub Organization", description="The GitHub organization to sync.")
github_repository: str = Field(title="GitHub Repository", description="The GitHub repository to sync.")
[docs]
class AllowedUserSetting(BaseGuildSetting):
"""User permissions configuration."""
allowed_user_id: int = Field(title="User ID", description="The user or role ID to allow.")
[docs]
class ForumSetting(BaseGuildSetting):
"""Forum configuration settings."""
help_forum: bool = Field(title="Help Forum", description="Is the help forum enabled.")
help_forum_category: str = Field(title="Help Forum Category", description="The help forum category.")
help_thread_auto_close: bool = Field(
title="Help Thread Auto Close", description="Is the help thread auto close enabled."
)
help_thread_auto_close_days: int = Field(
title="Help Thread Auto Close Days", description="The days to auto close help threads after inactivity."
)
help_thread_notify: bool = Field(
title="Help Thread Notify", description="Whether to notify roles for unresponded help threads."
)
help_thread_notify_roles: list[int] = Field(
title="Help Thread Notify Roles", description="The roles to notify for unresponded help threads."
)
help_thread_notify_days: int = Field(
title="Help Thread Notify Days", description="The days to notify `notify_roles` after not receiving a response."
)
help_thread_sync: bool = Field(
title="Help Thread Sync", description="Is the help thread GitHub discussions sync enabled."
)
showcase_forum: bool = Field(title="Showcase Forum", description="Is the showcase forum enabled.")
showcase_forum_category: str = Field(title="Showcase Forum Category", description="The showcase forum category.")
showcase_thread_auto_close: bool = Field(
title="Showcase Thread Auto Close", description="Is the showcase thread auto close enabled."
)
showcase_thread_auto_close_days: int = Field(
title="Showcase Thread Auto Close Days", description="The days to auto close showcase threads after inactivity."
)
[docs]
class UpdateableGuildSetting(
GuildModelSetting,
GitHubConfigSetting,
SOTagsSetting,
AllowedUserSetting,
ForumSetting,
):
"""Allowed settings that admins can update for their guild.
This is a composite class that combines all focused setting classes
for backwards compatibility with the existing API.
Important:
All parent setting models must keep distinct field names and
compatible model_config. Introducing overlapping field names
may lead to subtle Pydantic MRO issues.
"""
def _assert_distinct_updateable_guild_setting_fields() -> None:
"""Validate that composed setting models have no overlapping fields.
Multiple inheritance of Pydantic models relies on a stable MRO; if
two parents introduce the same field name, behavior becomes
order-dependent and fragile. This guard raises at import time
if overlapping fields are detected.
"""
parents = (
GuildModelSetting,
GitHubConfigSetting,
SOTagsSetting,
AllowedUserSetting,
ForumSetting,
)
seen: dict[str, str] = {}
overlaps: dict[str, list[str]] = {}
for parent in parents:
for field_name in parent.model_fields:
if field_name in seen:
overlaps.setdefault(field_name, [seen[field_name]]).append(parent.__name__)
else:
seen[field_name] = parent.__name__
if overlaps:
details = ", ".join(f"{name}: {', '.join(sorted(owners))}" for name, owners in sorted(overlaps.items()))
msg = f"UpdateableGuildSetting parent models must not define overlapping fields. Overlapping: {details}"
raise RuntimeError(msg)
_assert_distinct_updateable_guild_setting_fields()
UpdateableGuildSettingEnum = Enum( # type: ignore[misc]
"UpdateableGuildSettingEnum",
{field_name.upper(): field_name for field_name in UpdateableGuildSetting.model_fields},
)