diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e48d07b..8cf5cb9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: mixed-line-ending - id: trailing-whitespace - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: "v0.2.2" + rev: "v0.3.2" hooks: - id: ruff args: ["--fix"] @@ -88,7 +88,7 @@ repos: # discord-py, # ] - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.351 + rev: v1.1.353 hooks: - id: pyright additional_dependencies: @@ -142,6 +142,7 @@ repos: asyncpg-stubs, polyfactory, discord-py, + types-python-dateutil, ] - repo: https://github.com/sphinx-contrib/sphinx-lint rev: "v0.9.1" diff --git a/docs/conf.py b/docs/conf.py index 5bc74c6..dcaed6c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,5 @@ """Sphinx configuration.""" + from __future__ import annotations import importlib.metadata diff --git a/pdm.lock b/pdm.lock index 934b060..cd0e778 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "docs", "lint", "test"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.1" -content_hash = "sha256:7098b3193b937e135dd887069755130b9528dc064cdd068424cf78340e091281" +content_hash = "sha256:85851593eb18cf99411fefc596f8644a2b15988a4e0b6467aaaa5a3b4c50aeb8" [[package]] name = "accessible-pygments" @@ -655,7 +655,7 @@ files = [ name = "domdf-python-tools" version = "3.8.0.post2" requires_python = ">=3.6" -summary = "Helpful functions for Python 🐍 🛠️" +summary = "Helpful functions for Python‚ÄÇüêç‚ÄÇüõ†Ô∏è" groups = ["docs"] dependencies = [ "natsort>=7.0.1", @@ -782,31 +782,35 @@ files = [ [[package]] name = "githubkit" -version = "0.11.1" +version = "0.11.2" requires_python = ">=3.8,<4.0" -git = "https://github.com/yanyongyu/githubkit.git" -revision = "5d88fdc2349496a696d000ea31fef30efcae5e55" summary = "GitHub SDK for Python" groups = ["default"] dependencies = [ - "hishel<0.0.22,>=0.0.21", + "hishel<=0.0.24,>=0.0.21", "httpx<1.0.0,>=0.23.0", "pydantic!=2.5.0,!=2.5.1,<3.0.0,>=1.9.1", "typing-extensions<5.0.0,>=4.3.0", ] +files = [ + {file = "githubkit-0.11.2-py3-none-any.whl", hash = "sha256:d6776c667e37e0a120c003fa0a4c4c9a03c99a87a8a1767d3b67a87e09567933"}, + {file = "githubkit-0.11.2.tar.gz", hash = "sha256:2192cd30f32424d5ac8b104f3fc601c69ff1d4f63e2934eff3873953afca961b"}, +] [[package]] name = "githubkit" -version = "0.11.1" +version = "0.11.2" extras = ["auth-app"] requires_python = ">=3.8,<4.0" -git = "https://github.com/yanyongyu/githubkit.git" -revision = "5d88fdc2349496a696d000ea31fef30efcae5e55" summary = "GitHub SDK for Python" groups = ["default"] dependencies = [ "PyJWT[crypto]<3.0.0,>=2.4.0", - "githubkit @ git+https://github.com/yanyongyu/githubkit.git@5d88fdc2349496a696d000ea31fef30efcae5e55", + "githubkit==0.11.2", +] +files = [ + {file = "githubkit-0.11.2-py3-none-any.whl", hash = "sha256:d6776c667e37e0a120c003fa0a4c4c9a03c99a87a8a1767d3b67a87e09567933"}, + {file = "githubkit-0.11.2.tar.gz", hash = "sha256:2192cd30f32424d5ac8b104f3fc601c69ff1d4f63e2934eff3873953afca961b"}, ] [[package]] @@ -2216,7 +2220,7 @@ files = [ name = "sphinx-toolbox" version = "3.5.0" requires_python = ">=3.7" -summary = "Box of handy tools for Sphinx 🧰 📔" +summary = "Box of handy tools for Sphinx 🧰 📔" groups = ["docs"] dependencies = [ "apeye>=0.4.0", diff --git a/pyproject.toml b/pyproject.toml index 29d5529..571004a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,11 @@ dependencies = [ "advanced-alchemy>=0.6.1", "certifi>=2023.11.17", "asyncpg>=0.29.0", - "githubkit[auth-app] @ git+https://github.com/yanyongyu/githubkit.git", "PyJWT>=2.8.0", "alembic>=1.13.0", "pre-commit>=3.6.2", "ruff>=0.1.7", + "githubkit[auth-app]>=0.11.2", ] requires-python = ">=3.11,<4.0" readme = "README.md" @@ -82,7 +82,7 @@ changelog = "git cliff -c pyproject.toml -o docs/changelog.rst" # TODO: Move more from makefile here ci = { composite = ["lint", "test"] } -[tool.pdm.dev-dependencies] +[project.optional-dependencies] test = [ "pytest>=7.4.3", "coverage>=7.3.2", diff --git a/src/__init__.py b/src/__init__.py index f9b6856..538e852 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,4 +1,5 @@ """Byte Bot.""" + from __future__ import annotations from rich import get_console diff --git a/src/__main__.py b/src/__main__.py index 2a7da61..ab78994 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -1,4 +1,5 @@ """Application Entrypoint.""" + from __future__ import annotations __all__ = ("run_cli",) diff --git a/src/app.py b/src/app.py index 0a9bd6a..0707eea 100644 --- a/src/app.py +++ b/src/app.py @@ -1,4 +1,5 @@ """ASGI application factory.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/src/byte/__init__.py b/src/byte/__init__.py index e242444..44b0719 100644 --- a/src/byte/__init__.py +++ b/src/byte/__init__.py @@ -1,4 +1,5 @@ """Byte Bot Bot.""" + from __future__ import annotations from byte import bot, lib, plugins, views diff --git a/src/byte/bot.py b/src/byte/bot.py index 456cd59..6d33e3d 100644 --- a/src/byte/bot.py +++ b/src/byte/bot.py @@ -1,4 +1,5 @@ """Byte Bot.""" + from __future__ import annotations import contextlib diff --git a/src/byte/lib/__init__.py b/src/byte/lib/__init__.py index 28cfd13..412b7bf 100644 --- a/src/byte/lib/__init__.py +++ b/src/byte/lib/__init__.py @@ -1,10 +1,11 @@ """Byte library module.""" -from byte.lib import common, log, settings, utils +from byte.lib import common, log, settings, types, utils __all__ = [ "settings", "utils", "log", "common", + "types", ] diff --git a/src/byte/lib/checks.py b/src/byte/lib/checks.py new file mode 100644 index 0000000..e95a28d --- /dev/null +++ b/src/byte/lib/checks.py @@ -0,0 +1,65 @@ +""":doc:`Checks ` for Byte.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from discord.ext.commands import CheckFailure, Context, check + +from byte.lib import settings + +if TYPE_CHECKING: + from collections.abc import Callable + + from discord.ext.commands._types import Check + +__all__ = ("is_byte_dev", "is_guild_admin") + + +def is_guild_admin() -> Callable[[Context], Check]: + """Check if the user is a guild admin. + + Returns: + A check function. + """ + + async def predicate(ctx: Context) -> bool: + """Check if the user is a guild admin. + + Args: + ctx: Context object. + + Returns: + True if the user is a guild admin, False otherwise. + """ + if not (member := ctx.guild.get_member(ctx.author.id)): + msg = "Member not found in the guild." + raise CheckFailure(msg) + return member.guild_permissions.administrator + + return check(predicate) + + +def is_byte_dev() -> Callable[[Context], Check]: + """Determines if the user is a Byte developer or owner. + + Returns: + A check function. + """ + + async def predicate(ctx: Context) -> bool: + """Check if the user is a Byte developer or owner. + + Args: + ctx: Context object. + + Returns: + True if the user is a Byte developer or owner, False otherwise. + """ + return ( + await ctx.bot.is_owner(ctx.author) + or ctx.author.id == settings.discord.DEV_USER_ID + or any(role.name == "byte-dev" for role in ctx.author.roles) + ) + + return check(predicate) diff --git a/src/byte/lib/common/__init__.py b/src/byte/lib/common/__init__.py index 5a7f255..4c23612 100644 --- a/src/byte/lib/common/__init__.py +++ b/src/byte/lib/common/__init__.py @@ -3,11 +3,55 @@ .. todo:: temporary, these are not multi-guild friendly. """ -from byte.lib.common import assets, colors, guilds, links +from typing import Any + +from byte.lib.common import assets, colors, guilds, links, mention __all__ = ( "assets", "colors", "guilds", "links", + "config_options", + "mention", ) + +config_options: list[dict[str, Any]] = [ + { + "label": "Server Settings", + "description": "Configure overall server settings", + "sub_settings": [ + {"label": "Prefix", "field": "prefix", "data_type": "String"}, + {"label": "Help Channel ID", "field": "help_channel_id", "data_type": "Integer"}, + {"label": "Sync Label", "field": "sync_label", "data_type": "String"}, + {"label": "Issue Linking", "field": "issue_linking", "data_type": "True/False"}, + {"label": "Comment Linking", "field": "comment_linking", "data_type": "True/False"}, + {"label": "PEP Linking", "field": "pep_linking", "data_type": "True/False"}, + ], + }, + { + "label": "GitHub Settings", + "description": "Configure GitHub settings", + "sub_settings": [ + {"label": "Discussion Sync", "field": "discussion_sync", "data_type": "True/False"}, + {"label": "GitHub Organization", "field": "github_organization", "data_type": "String"}, + {"label": "GitHub Repository", "field": "github_repository", "data_type": "String"}, + ], + }, + { + "label": "StackOverflow Settings", + "description": "Configure StackOverflow settings", + "sub_settings": [ + {"label": "Tag Name", "field": "tag_name", "data_type": "Comma-Separated String"}, + ], + }, + { + "label": "Allowed Users", + "description": "Configure allowed users", + "sub_settings": [ + {"label": "User ID", "field": "user_id", "data_type": "Integer"}, + ], + }, + # Forum Settings: Configure help and showcase forum settings + # Byte Settings: Configure meta-level Byte features +] diff --git a/src/byte/lib/common/assets.py b/src/byte/lib/common/assets.py index d478841..ea5e52d 100644 --- a/src/byte/lib/common/assets.py +++ b/src/byte/lib/common/assets.py @@ -2,6 +2,7 @@ .. todo:: temporary, these are not multi-guild friendly. """ + from typing import Final # --- Assets diff --git a/src/byte/lib/common/colors.py b/src/byte/lib/common/colors.py index cfa5795..622f0bb 100644 --- a/src/byte/lib/common/colors.py +++ b/src/byte/lib/common/colors.py @@ -2,6 +2,7 @@ .. todo:: temporary, these are not multi-guild friendly. """ + from typing import Final # --- Colors diff --git a/src/byte/lib/common/guilds.py b/src/byte/lib/common/guilds.py index cee58ae..6f05964 100644 --- a/src/byte/lib/common/guilds.py +++ b/src/byte/lib/common/guilds.py @@ -2,6 +2,7 @@ .. todo:: temporary, these are not multi-guild friendly. """ + from typing import Final # --- Channel IDs diff --git a/src/byte/lib/common/links.py b/src/byte/lib/common/links.py index a1f0d64..28eb128 100644 --- a/src/byte/lib/common/links.py +++ b/src/byte/lib/common/links.py @@ -2,6 +2,7 @@ .. todo:: temporary, these are not multi-guild friendly. """ + from typing import Final # --- Links diff --git a/src/byte/lib/common/mention.py b/src/byte/lib/common/mention.py new file mode 100644 index 0000000..f81c6ba --- /dev/null +++ b/src/byte/lib/common/mention.py @@ -0,0 +1,129 @@ +"""Helper functions for mentioning users, roles, and channels.""" + +from __future__ import annotations + +__all__ = ( + "mention_channel", + "mention_custom_emoji", + "mention_custom_emoji_animated", + "mention_guild_navigation", + "mention_role", + "mention_slash_command", + "mention_timestamp", + "mention_user", + "mention_user_nickname", +) + + +def mention_user(user_id: int) -> str: + """Mention a user by ID. + + Args: + user_id: The unique identifier for the user. + + Returns: + A formatted string that mentions the user. + """ + return f"<@{user_id}>" + + +def mention_user_nickname(user_id: int) -> str: + """Mention a user by ID with a nickname. + + Args: + user_id: The unique identifier for the user. + + Returns: + A formatted string that mentions the user with a nickname. + """ + return f"<@!{user_id}>" + + +def mention_channel(channel_id: int) -> str: + """Mention a channel by ID. + + Args: + channel_id: The unique identifier for the channel. + + Returns: + A formatted string that mentions the channel. + """ + return f"<#{channel_id}>" + + +def mention_role(role_id: int) -> str: + """Mention a role by ID. + + Args: + role_id: The unique identifier for the role. + + Returns: + A formatted string that mentions the role. + """ + return f"<@&{role_id}>" + + +def mention_slash_command(name: str, command_id: int) -> str: + """Mention a slash command by name and ID. + + Args: + name: The name of the slash command. + command_id: The unique identifier for the slash command. + + Returns: + A formatted string that mentions the slash command. + """ + return f"" + + +def mention_custom_emoji(name: str, emoji_id: int) -> str: + """Mention a custom emoji by name and ID. + + Args: + name: The name of the emoji. + emoji_id: The unique identifier for the emoji. + + Returns: + A formatted string that mentions the custom emoji. + """ + return f"<:{name}:{emoji_id}>" + + +def mention_custom_emoji_animated(name: str, emoji_id: int) -> str: + """Mention an animated custom emoji by name and ID. + + Args: + name: The name of the animated emoji. + emoji_id: The unique identifier for the animated emoji. + + Returns: + A formatted string that mentions the animated custom emoji. + """ + return f"" + + +def mention_timestamp(timestamp: int, style: str = "") -> str: + """Mention a timestamp, optionally with a style. + + Args: + timestamp: The Unix timestamp to format. + style: An optional string representing the timestamp style. + (Default `` ``, valid styles: ``t``, ``T``, ``d``, ``D``, ``f``, ``F``, ``R``) + + Returns: + A formatted string that represents the timestamp. + """ + return f"" if style else f"" + + +def mention_guild_navigation(guild_nav_type: str, guild_element_id: int) -> str: + """Mention a guild navigation element by type and ID. + + Args: + guild_nav_type: The type of the guild navigation element. + guild_element_id: The unique identifier for the element. + + Returns: + A formatted string that mentions the guild navigation element. + """ + return f"<{guild_element_id}:{guild_nav_type}>" diff --git a/src/byte/lib/log.py b/src/byte/lib/log.py index b42849e..c1373e3 100644 --- a/src/byte/lib/log.py +++ b/src/byte/lib/log.py @@ -1,4 +1,5 @@ """Centralized logging configuration.""" + import logging import logging.config import logging.handlers @@ -72,6 +73,16 @@ def setup_logging() -> None: "handlers": ["console", "file"], "propagate": False, }, + "httpcore": { + "level": settings.log.HTTP_CORE_LEVEL, + "handlers": ["console", "file"], + "propagate": False, + }, + "httpx": { + "level": settings.log.HTTPX_LEVEL, + "handlers": ["console", "file"], + "propagate": False, + }, "websockets": { "level": settings.log.WEBSOCKETS_LEVEL, "handlers": ["console", "file"], diff --git a/src/byte/lib/settings.py b/src/byte/lib/settings.py index e70cdbc..1dc0985 100644 --- a/src/byte/lib/settings.py +++ b/src/byte/lib/settings.py @@ -1,4 +1,5 @@ """Project Settings.""" + from __future__ import annotations import os @@ -106,6 +107,10 @@ class LogSettings(BaseSettings): """Sets the log level for the websockets library.""" ASYNCIO_LEVEL: int = 20 """Sets the log level for the asyncio library.""" + HTTP_CORE_LEVEL: int = 20 + """Sets the log level for the httpcore library. (Used in cert. validation)""" + HTTPX_LEVEL: int = 30 + """Sets the log level for the httpx library.""" FORMAT: str = "[[ %(asctime)s ]] - [[ %(name)s ]] - [[ %(levelname)s ]] - %(message)s" """Log format string.""" FILE: Path = BASE_DIR / "logs" / "byte.log" diff --git a/src/byte/lib/types/__init__.py b/src/byte/lib/types/__init__.py new file mode 100644 index 0000000..7e42cd8 --- /dev/null +++ b/src/byte/lib/types/__init__.py @@ -0,0 +1,5 @@ +"""Types and similar facilities used throughout library code.""" + +from byte.lib.types import astral, python + +__all__ = ("astral", "python") diff --git a/src/byte/lib/types/astral.py b/src/byte/lib/types/astral.py new file mode 100644 index 0000000..cf0ee23 --- /dev/null +++ b/src/byte/lib/types/astral.py @@ -0,0 +1,32 @@ +"""Types for Astral views and plugins.""" + +from __future__ import annotations + +from typing import TypedDict + +__all__ = ("BaseRuffRule", "FormattedRuffRule", "RuffRule") + + +class BaseRuffRule(TypedDict): + """Base Ruff rule data.""" + + name: str + summary: str + fix: str + explanation: str + + +class RuffRule(BaseRuffRule): + """Ruff rule data.""" + + code: str + linter: str + message_formats: list[str] + preview: bool + + +class FormattedRuffRule(BaseRuffRule): + """Formatted Ruff rule data.""" + + rule_link: str + rule_anchor_link: str diff --git a/src/byte/lib/types/python.py b/src/byte/lib/types/python.py new file mode 100644 index 0000000..f9c3123 --- /dev/null +++ b/src/byte/lib/types/python.py @@ -0,0 +1,77 @@ +"""Types for Python related views and plugins.""" + +from __future__ import annotations + +from enum import StrEnum +from typing import TYPE_CHECKING, TypedDict + +if TYPE_CHECKING: + from datetime import datetime + +__all__ = ("PEP", "PEPHistoryItem", "PEPStatus", "PEPType") + + +class PEPType(StrEnum): + """Type of PEP. + + Based off of `PEP Types in PEP1 `_. + """ + + I = "Informational" # noqa: E741 + P = "Process" + S = "Standards Track" + + +class PEPStatus(StrEnum): + """Status of a PEP. + + .. note:: ``Active`` and ``Accepted`` both traditionally use ``A``, + but are differentiated here for clarity. + + Based off of `PEP Status in PEP1 `_. + """ + + A = "Active" + AA = "Accepted" + D = "Deferred" + __ = "Draft" + F = "Final" + P = "Provisional" + R = "Rejected" + S = "Superseded" + W = "Withdrawn" + + +class PEPHistoryItem(TypedDict, total=False): + """PEP history item. + + Sometimes these include a list of ``datetime`` objects, + other times they are a list of datetime and str + because they contain a date and an rST link. + """ + + date: str + link: str + + +class PEP(TypedDict): + """PEP data. + + Based off of the `PEPS API `_. + """ + + number: int + title: str + authors: list[str] | str + discussions_to: str + status: PEPStatus + type: PEPType + topic: str + created: datetime + python_version: list[float] | float + post_history: list[str] + resolution: str | None + requires: str | None + replaces: str | None + superseded_by: str | None + url: str diff --git a/src/byte/lib/utils.py b/src/byte/lib/utils.py index 4f18d76..8e60f75 100644 --- a/src/byte/lib/utils.py +++ b/src/byte/lib/utils.py @@ -1,196 +1,42 @@ """Byte utilities.""" + from __future__ import annotations +import datetime as dt import json import re import subprocess from datetime import UTC, datetime -from enum import StrEnum from itertools import islice -from typing import TYPE_CHECKING, TypedDict, TypeVar +from typing import TYPE_CHECKING, TypeVar import httpx from anyio import run_process -from discord.ext import commands from ruff.__main__ import find_ruff_bin # type: ignore[import-untyped] -from byte.lib import settings from byte.lib.common.links import pastebin +from byte.lib.types.python import PEP, PEPStatus, PEPType if TYPE_CHECKING: from collections.abc import Iterable - from typing import Any - from discord.ext.commands import Context - from discord.ext.commands._types import Check + from byte.lib.types.astral import FormattedRuffRule, RuffRule __all__ = ( - "BaseRuffRule", - "RuffRule", - "FormattedRuffRule", - "PEP", - "PEPType", - "PEPStatus", - "PEPHistoryItem", - "is_guild_admin", - "is_byte_dev", - "linker", - "mention_user", - "mention_user_nickname", - "mention_channel", - "mention_role", - "mention_slash_command", - "mention_custom_emoji", - "mention_custom_emoji_animated", - "mention_timestamp", - "mention_guild_navigation", + "chunk_sequence", + "format_resolution_link", "format_ruff_rule", - "query_all_ruff_rules", - "run_ruff_format", + "get_next_friday", + "linker", "paste", - "chunk_sequence", "query_all_peps", + "query_all_ruff_rules", + "run_ruff_format", ) _T = TypeVar("_T") -class BaseRuffRule(TypedDict): - """Base Ruff rule data.""" - - name: str - summary: str - fix: str - explanation: str - - -class RuffRule(BaseRuffRule): - """Ruff rule data.""" - - code: str - linter: str - message_formats: list[str] - preview: bool - - -class FormattedRuffRule(BaseRuffRule): - """Formatted Ruff rule data.""" - - rule_link: str - rule_anchor_link: str - - -class PEPType(StrEnum): - """Type of PEP. - - Based off of `PEP Types in PEP1 `_. - """ - - I = "Informational" # noqa: E741 - P = "Process" - S = "Standards Track" - - -class PEPStatus(StrEnum): - """Status of a PEP. - - .. note:: ``Active`` and ``Accepted`` both traditionally use ``A``, - but are differentiated here for clarity. - - Based off of `PEP Status in PEP1 `_. - """ - - A = "Active" - AA = "Accepted" - D = "Deferred" - __ = "Draft" - F = "Final" - P = "Provisional" - R = "Rejected" - S = "Superseded" - W = "Withdrawn" - - -class PEPHistoryItem(TypedDict, total=False): - """PEP history item. - - Sometimes these include a list of ``datetime`` objects, - other times they are a list of datetime and str - because they contain a date and an rST link. - """ - - date: str - link: str - - -class PEP(TypedDict): - """PEP data. - - Based off of the `PEPS API `_. - """ - - number: int - title: str - authors: list[str] | str - discussions_to: str - status: PEPStatus - type: PEPType - topic: str - created: datetime - python_version: list[float] | float - post_history: list[str] - resolution: str | None - requires: str | None - replaces: str | None - superseded_by: str | None - url: str - - -def is_guild_admin() -> Check[Any]: - """Check if the user is a guild admin. - - Returns: - A check function. - """ - - async def predicate(ctx: Context) -> bool: - """Check if the user is a guild admin. - - Args: - ctx: Context object. - - Returns: - True if the user is a guild admin, False otherwise. - """ - return ctx.author.guild_permissions.administrator - - return commands.check(predicate) - - -def is_byte_dev() -> Check[Any]: - """Check if the user is a Byte developer. - - Returns: - A check function. - """ - - async def predicate(ctx: Context) -> bool: - """Check if the user is a Byte Dev or Owner. - - Args: - ctx: Context object. - - Returns: - True if the user is a Byte Dev or Owner, False otherwise. - """ - if await ctx.bot.is_owner(ctx.author) or ctx.author.id == settings.discord.DEV_USER_ID: - return True - - return any(role.name == "byte-dev" for role in ctx.author.roles) # type: ignore[reportAttributeAccessIssue] - - return commands.check(predicate) - - def linker(title: str, link: str, show_embed: bool = False) -> str: """Create a Markdown link, optionally with an embed. @@ -205,120 +51,6 @@ def linker(title: str, link: str, show_embed: bool = False) -> str: return f"[{title}]({link})" if show_embed else f"[{title}](<{link}>)" -def mention_user(user_id: int) -> str: - """Mention a user by ID. - - Args: - user_id: The unique identifier for the user. - - Returns: - A formatted string that mentions the user. - """ - return f"<@{user_id}>" - - -def mention_user_nickname(user_id: int) -> str: - """Mention a user by ID with a nickname. - - Args: - user_id: The unique identifier for the user. - - Returns: - A formatted string that mentions the user with a nickname. - """ - return f"<@!{user_id}>" - - -def mention_channel(channel_id: int) -> str: - """Mention a channel by ID. - - Args: - channel_id: The unique identifier for the channel. - - Returns: - A formatted string that mentions the channel. - """ - return f"<#{channel_id}>" - - -def mention_role(role_id: int) -> str: - """Mention a role by ID. - - Args: - role_id: The unique identifier for the role. - - Returns: - A formatted string that mentions the role. - """ - return f"<@&{role_id}>" - - -def mention_slash_command(name: str, command_id: int) -> str: - """Mention a slash command by name and ID. - - Args: - name: The name of the slash command. - command_id: The unique identifier for the slash command. - - Returns: - A formatted string that mentions the slash command. - """ - return f"" - - -def mention_custom_emoji(name: str, emoji_id: int) -> str: - """Mention a custom emoji by name and ID. - - Args: - name: The name of the emoji. - emoji_id: The unique identifier for the emoji. - - Returns: - A formatted string that mentions the custom emoji. - """ - return f"<:{name}:{emoji_id}>" - - -def mention_custom_emoji_animated(name: str, emoji_id: int) -> str: - """Mention an animated custom emoji by name and ID. - - Args: - name: The name of the animated emoji. - emoji_id: The unique identifier for the animated emoji. - - Returns: - A formatted string that mentions the animated custom emoji. - """ - return f"" - - -def mention_timestamp(timestamp: int, style: str = "") -> str: - """Mention a timestamp, optionally with a style. - - Args: - timestamp: The Unix timestamp to format. - style: An optional string representing the timestamp style. - (Default `` ``, valid styles: ``t``, ``T``, ``d``, ``D``, ``f``, ``F``, ``R``) - - Returns: - A formatted string that represents the timestamp. - """ - return f"" if style else f"" - - -def mention_guild_navigation(guild_nav_type: str, guild_element_id: int) -> str: - """Mention a guild navigation element by type and ID. - - Args: - guild_nav_type: The type of the guild navigation element. - guild_element_id: The unique identifier for the element. - - Returns: - A formatted string that mentions the guild navigation element. - """ - return f"<{guild_element_id}:{guild_nav_type}>" - - def format_ruff_rule(rule_data: RuffRule) -> FormattedRuffRule: """Format ruff rule data for embed-friendly output and append rule link. @@ -467,3 +199,25 @@ async def query_all_peps() -> list[PEP]: } for pep_info in data.values() ] + + +def get_next_friday(now: datetime, delay: int | None = None) -> tuple[datetime, datetime]: + """Calculate the next Friday from ``now``. + + If ``delay``, calculate the Friday for ``delay`` weeks from now. + + Args: + now: The current date and time. + delay: The number of weeks to delay the calculation. + + Returns: + datetime: The next Friday, optionally for the week after next. + """ + days_ahead = 4 - now.weekday() + if days_ahead < 0: + days_ahead += 7 + if delay: + days_ahead += 7 * delay + start_dt = (now + dt.timedelta(days=days_ahead)).replace(hour=11, minute=0, second=0, microsecond=0) + end_dt = start_dt + dt.timedelta(hours=1) + return start_dt, end_dt diff --git a/src/byte/plugins/admin.py b/src/byte/plugins/admin.py index ac80a6a..a84e3bc 100644 --- a/src/byte/plugins/admin.py +++ b/src/byte/plugins/admin.py @@ -2,15 +2,16 @@ .. todo:: add an unload cog command. """ + from discord import Interaction from discord.app_commands import command as app_command from discord.ext import commands from discord.ext.commands import Bot, Cog, Context, command, group, is_owner -from byte.lib.utils import is_byte_dev - __all__ = ("AdminCommands", "setup") +from byte.lib.checks import is_byte_dev + class AdminCommands(Cog): """Admin command cog.""" diff --git a/src/byte/plugins/astral.py b/src/byte/plugins/astral.py index 5fb9f1e..89894f6 100644 --- a/src/byte/plugins/astral.py +++ b/src/byte/plugins/astral.py @@ -1,4 +1,5 @@ """Plugins for Astral Inc. related software, including Ruff, uv, etc.""" + from __future__ import annotations from typing import TYPE_CHECKING @@ -14,7 +15,7 @@ from byte.views.astral import RuffView if TYPE_CHECKING: - from byte.lib.utils import RuffRule + from byte.lib.types.astral import RuffRule __all__ = ("Astral", "setup") @@ -32,10 +33,10 @@ def __init__(self, bot: Bot, rules: list[RuffRule]) -> None: async def _rule_autocomplete(self, _: Interaction, current_rule: str) -> list[Choice[str]]: # TODO: this can and should be made faster, rn this is slow, slow like the maintainer return [ - Choice(name=f'{code} - {rule["name"]}', value=code) - for code, rule in self._rules.items() - if current_rule.lower() in code.lower() - ][:25] + Choice(name=f'{code} - {rule["name"]}', value=code) + for code, rule in self._rules.items() + if current_rule.lower() in code.lower() + ][:25] @app_command(name="ruff") @autocomplete(rule=_rule_autocomplete) diff --git a/src/byte/plugins/config.py b/src/byte/plugins/config.py new file mode 100644 index 0000000..5dfc7e3 --- /dev/null +++ b/src/byte/plugins/config.py @@ -0,0 +1,69 @@ +"""Plugins for guild admins to configure Byte and its features.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from discord.app_commands import Choice, autocomplete +from discord.app_commands import command as app_command +from discord.ext.commands import Bot, Cog + +from byte.lib.common import config_options +from byte.views.config import ConfigView + +if TYPE_CHECKING: + from discord import Interaction + +__all__ = ("Config", "setup") + + +class Config(Cog): + """Config cog.""" + + def __init__(self, bot: Bot) -> None: + """Initialize cog.""" + self.bot = bot + self.__cog_name__ = "Config Commands" + self.config_options = config_options + + async def _config_autocomplete(self, interaction: Interaction, current: str) -> list[Choice[str]]: # noqa: ARG002 + """Autocomplete config for the config dropdown (up?) slash command.""" + return [ + Choice(name=f"{option['label']} - {option['description']}", value=option["label"]) + for option in config_options + if current.lower() in option["label"].lower() + ][:25] + + @app_command(name="config") + @autocomplete(setting=_config_autocomplete) + async def config_rule(self, interaction: Interaction, setting: str | None = None) -> None: + """Slash command to configure Byte. + + Args: + interaction: Interaction object. + setting: The setting to configure. + """ + if setting: + if selected_option := next( + (option for option in config_options if option["label"] == setting), + None, + ): + view = ConfigView(preselected=selected_option["label"]) + await interaction.response.send_message( + f"Configure {selected_option['label']}:", + view=view, + ephemeral=True, + ) + else: + await interaction.response.send_message( + f"Invalid setting: {setting}. Please select a valid setting.", + ephemeral=True, + ) + else: + view = ConfigView() + await interaction.response.send_message("Select a configuration option:", view=view, ephemeral=True) + + +async def setup(bot: Bot) -> None: + """Set up the config cog.""" + await bot.add_cog(Config(bot)) diff --git a/src/byte/plugins/custom/litestar.py b/src/byte/plugins/custom/litestar.py index c205ab4..efe458c 100644 --- a/src/byte/plugins/custom/litestar.py +++ b/src/byte/plugins/custom/litestar.py @@ -1,11 +1,18 @@ """Custom plugins for the Litestar Discord.""" + from __future__ import annotations -from discord import Embed +import datetime + +from dateutil.zoneinfo import gettz +from discord import Embed, EntityType, Interaction, Object, PrivacyLevel +from discord.app_commands import command as app_command from discord.ext.commands import Bot, Cog, Context, command, group, is_owner +from byte.lib.checks import is_byte_dev from byte.lib.common.colors import litestar_yellow -from byte.lib.utils import is_byte_dev, mention_role, mention_user +from byte.lib.common.mention import mention_role, mention_user +from byte.lib.utils import get_next_friday __all__ = ("LitestarCommands", "setup") @@ -85,6 +92,50 @@ async def apply_role_embed(self, ctx: Context[Bot]) -> None: await ctx.send(embed=embed) + @app_command( + name="schedule-office-hours", + description="Schedule Office Hours event for the upcoming or the week after next Friday.", + ) + async def schedule_office_hours(self, interaction: Interaction, delay: int | None = None) -> None: + """Schedule Office Hours event for the upcoming or ``delay`` weeks after next Friday. + + Args: + interaction: Interaction object. + delay: Optional. Number of weeks to delay the event. + """ + now_cst = datetime.datetime.now(gettz("America/Chicago")) + start_dt, end_dt = get_next_friday(now_cst, delay) + existing_events = interaction.guild.scheduled_events + + for event in existing_events: + if ( + event.name == "Office Hours" + and event.start_time.astimezone(gettz("America/Chicago")).date() == start_dt.date() + ): + await interaction.response.send_message( + "An Office Hours event is already scheduled for that day.", ephemeral=True + ) + return + + await interaction.guild.create_scheduled_event( + name="Office Hours", + start_time=start_dt, + end_time=end_dt, + description="Join us for our weekly office hours!", + entity_type=EntityType.stage_instance, + privacy_level=PrivacyLevel.guild_only, + reason=f"Scheduled by {interaction.user} via /schedule-office-hours", + channel=Object(id=1215926860144443502), + ) + + formatted_date = f"" + start_time_formatted = f"" + end_time_formatted = f"" + + await interaction.response.send_message( + f"Office Hours event scheduled: {formatted_date} from {start_time_formatted} - {end_time_formatted}." + ) + async def setup(bot: Bot) -> None: """Add cog to bot. diff --git a/src/byte/plugins/events.py b/src/byte/plugins/events.py index feec474..2a96621 100644 --- a/src/byte/plugins/events.py +++ b/src/byte/plugins/events.py @@ -1,4 +1,5 @@ """Plugins for events.""" + from threading import Thread from typing import cast diff --git a/src/byte/plugins/general.py b/src/byte/plugins/general.py index c034d71..17b155e 100644 --- a/src/byte/plugins/general.py +++ b/src/byte/plugins/general.py @@ -1,4 +1,5 @@ """General plugins to be used wherever.""" + from __future__ import annotations from discord import Embed, Interaction @@ -36,7 +37,7 @@ async def show_paste(self, interaction: Interaction) -> None: embed.add_field( name="Syntax Highlighting", value="You can also use backticks to format your code. Read about it in the " - f"{linker('Discord Markdown Guide', markdown_guide)}.", + f"{linker('Discord Markdown Guide', markdown_guide)}.", ) embed.set_thumbnail(url=litestar_logo_yellow) diff --git a/src/byte/plugins/github.py b/src/byte/plugins/github.py index 680018e..4033e03 100644 --- a/src/byte/plugins/github.py +++ b/src/byte/plugins/github.py @@ -1,4 +1,5 @@ """Plugins for GitHub interactions.""" + from __future__ import annotations from typing import Self diff --git a/src/byte/plugins/python.py b/src/byte/plugins/python.py index 900da80..b9b97ea 100644 --- a/src/byte/plugins/python.py +++ b/src/byte/plugins/python.py @@ -1,4 +1,5 @@ """Plugins for Python related things, including PyPI, PEPs, etc.""" + from __future__ import annotations from discord import Embed, Interaction diff --git a/src/byte/plugins/testing.py b/src/byte/plugins/testing.py index b258db8..81734e1 100644 --- a/src/byte/plugins/testing.py +++ b/src/byte/plugins/testing.py @@ -1,4 +1,5 @@ """Plugins for testing purposes.""" + from discord.ext.commands import Bot, Cog, Context, command, group diff --git a/src/byte/views/abstract_views.py b/src/byte/views/abstract_views.py index f51d4e2..333ae7a 100644 --- a/src/byte/views/abstract_views.py +++ b/src/byte/views/abstract_views.py @@ -1,25 +1,30 @@ """Inheritable views that include extra functionality for base Views classes.""" + from __future__ import annotations from copy import deepcopy -from typing import TYPE_CHECKING, Any, Literal, TypedDict +from typing import TYPE_CHECKING, Any, Literal, ParamSpec, TypedDict from discord import ButtonStyle, Colour, Embed, Interaction from discord.ui import Button, View, button if TYPE_CHECKING: from datetime import datetime - from typing import Self, NotRequired + from typing import NotRequired, Self from discord.ext.commands import Bot __all__ = ("ExtendedEmbed", "Field", "ButtonEmbedView") +P = ParamSpec("P") + class ButtonEmbedView(View): """Base view including common buttons.""" - def __init__(self, author: int, bot: Bot, original_embed: Embed, minified_embed: Embed, *args, **kwargs) -> None: + def __init__( + self, author: int, bot: Bot, original_embed: Embed, minified_embed: Embed, *args: P.args, **kwargs: P.kwargs + ) -> None: """Initialize the view. Args: diff --git a/src/byte/views/astral.py b/src/byte/views/astral.py index 2e77494..47dc3ca 100644 --- a/src/byte/views/astral.py +++ b/src/byte/views/astral.py @@ -1,4 +1,5 @@ """Discord UI views used in Astral commands.""" + from __future__ import annotations from byte.lib.log import get_logger diff --git a/src/byte/views/config.py b/src/byte/views/config.py new file mode 100644 index 0000000..492dd27 --- /dev/null +++ b/src/byte/views/config.py @@ -0,0 +1,263 @@ +"""Discord UI views used in Byte config commands.""" + +from __future__ import annotations + +from typing import Any + +from discord import ButtonStyle, Interaction, SelectOption, TextStyle +from discord.ui import Button, Modal, Select, TextInput, View + +from byte.lib.common import config_options +from byte.lib.log import get_logger + +__all__ = ("ConfigView",) + +logger = get_logger() + + +class FinishButton(Button): + """Finish button.""" + + def __init__(self) -> None: + """Initialize button.""" + super().__init__(style=ButtonStyle.success, label="Finished") + + async def callback(self, interaction: Interaction) -> None: + """Callback for button. + + Args: + interaction: Interaction object. + """ + await interaction.response.send_message("Configuration complete!", ephemeral=True) + self.view.stop() + + +class BackButton(Button): + """Back button.""" + + def __init__(self) -> None: + """Initialize button.""" + super().__init__(style=ButtonStyle.secondary, label="Back") + + async def callback(self, interaction: Interaction) -> None: + """Callback for button. + + Args: + interaction: Interaction object. + """ + view = ConfigView() + await interaction.response.edit_message(content="Select a configuration option:", view=view) + + +class CancelButton(Button): + """Cancel button.""" + + def __init__(self) -> None: + """Initialize button.""" + super().__init__(style=ButtonStyle.danger, label="Cancel") + + async def callback(self, interaction: Interaction) -> None: + """Callback for button. + + Args: + interaction: Interaction object. + """ + await interaction.response.send_message("Configuration cancelled.", ephemeral=True) + self.view.stop() + + +class ConfigSelect(Select): + """Configuration select dropdown menu.""" + + def __init__(self, preselected: str | None = None) -> None: + """Initialize select. + + Args: + preselected: Preselected option, if given. + """ + options = [SelectOption(label=option["label"], description=option["description"]) for option in config_options] + super().__init__(placeholder="Choose a setting...", min_values=1, max_values=1, options=options) + + if preselected: + for option in options: + if option.label == preselected: + option.default = True + break + + async def callback(self, interaction: Interaction) -> None: + """Callback for select. + + Args: + interaction: Interaction object. + """ + selected_option = next(option for option in config_options if option["label"] == self.values[0]) + if "sub_settings" in selected_option: + view = ConfigKeyView(selected_option) + await interaction.response.edit_message(content=f"Select a key for {selected_option['label']}:", view=view) + else: + modal = ConfigModal(title=f"Configure {selected_option['label']}") + await interaction.response.send_modal(modal) + + +class ConfigKeySelect(Select): + """Configuration key select dropdown menu.""" + + def __init__(self, option: dict[str, Any]) -> None: + """Initialize select. + + Args: + option: The selected configuration option. + """ + self.option = option + options = [ + SelectOption(label=sub_setting["label"], description=sub_setting.get("description", "")) + for sub_setting in option["sub_settings"] + ] + super().__init__(placeholder="Choose a key...", min_values=1, max_values=1, options=options) + + async def callback(self, interaction: Interaction) -> None: + """Callback for select. + + Args: + interaction: Interaction object. + """ + selected_sub_setting = self.values[0] + selected_sub_setting = next( + sub_setting for sub_setting in self.option["sub_settings"] if sub_setting["label"] == selected_sub_setting + ) + modal = ConfigModal( + title=f"{self.option['label']} - {selected_sub_setting['label']}", + sub_setting=selected_sub_setting, + option=self.option, + ) + await interaction.response.send_modal(modal) + + +class ConfigView(View): + """Configuration view.""" + + def __init__(self, preselected: str | None = None) -> None: + """Initialize view. + + Args: + preselected: Preselected option, if given. + """ + super().__init__(timeout=None) + self.add_item(ConfigSelect(preselected)) + self.add_item(FinishButton()) + self.add_item(CancelButton()) + + +class ConfigKeyView(View): + """Configuration key view.""" + + def __init__(self, option: dict[str, Any]) -> None: + """Initialize view. + + Args: + option: The selected configuration option. + """ + super().__init__(timeout=None) + self.add_item(ConfigKeySelect(option)) + self.add_item(BackButton()) + self.add_item(CancelButton()) + + +class ConfigModal(Modal): + """Configuration modal.""" + + def __init__( + self, + title: str, + sub_setting: dict[str, str] | None = None, + sub_settings: list[dict[str, str]] | None = None, + option: dict[str, Any] | None = None, + ) -> None: + """Initialize modal. + + Args: + title: Title of modal. + sub_setting: The selected sub-setting, if applicable. + sub_settings: List of sub-settings, if configuring all keys. + option: The selected configuration option, if applicable. + """ + super().__init__(title=title + "\n\n") + self.option = option + + if sub_settings: + for sub_setting in sub_settings: + self.add_item( + TextInput( + label=sub_setting["label"], + style=TextStyle.short, + custom_id=sub_setting["field"], + placeholder=f"Enter {sub_setting['label']} ({sub_setting['data_type']})", + required=True, + min_length=4 if sub_setting["data_type"] == "True/False" else 1, + max_length=5 + if sub_setting["data_type"] == "True/False" + else 100 + if sub_setting["data_type"] in ["String", "Integer"] + else 300 + if sub_setting["data_type"] == "Comma-separated list" + else None, + ) + ) + elif sub_setting: + self.add_item( + TextInput( + label=sub_setting["label"], + style=TextStyle.short, + placeholder=f"Enter {sub_setting['label']} ({sub_setting['data_type']})", + required=True, + min_length=4 if sub_setting["data_type"] == "True/False" else 1, + max_length=5 + if sub_setting["data_type"] == "True/False" + else 100 + if sub_setting["data_type"] in ["String", "Integer"] + else 300 + if sub_setting["data_type"] == "Comma-separated list" + else None, + ) + ) + else: + self.add_item( + TextInput( + label="Configuration Value", + style=TextStyle.short, + placeholder="Enter your configuration value...", + required=True, + ) + ) + + async def on_submit(self, interaction: Interaction) -> None: + """Handle modal submission. + + Args: + interaction: Interaction object. + """ + config_values = {item.custom_id: item.value for item in self.children if hasattr(item, "custom_id")} + await interaction.response.send_message(f"Configuration values received: {config_values}", ephemeral=True) + + if self.option: + view = ConfigKeyView(self.option) + await interaction.followup.send( + f"Select another key for {self.option['label']} or click 'Back' to return to the main menu.", + view=view, + ephemeral=True, + ) + else: + view = ConfigView() + await interaction.followup.send( + "Select another setting or click 'Finished' when done.", view=view, ephemeral=True + ) + + async def on_error(self, interaction: Interaction, error: Exception) -> None: + """Handle modal submission error. + + Args: + interaction: Interaction object. + error: Error object. + """ + await interaction.response.send_message("Oops! Something went wrong.", ephemeral=True) + logger.exception("Error occurred while processing config modal submission", exc_info=error) diff --git a/src/byte/views/python.py b/src/byte/views/python.py index 795514c..00989d5 100644 --- a/src/byte/views/python.py +++ b/src/byte/views/python.py @@ -1,4 +1,5 @@ """Discord UI views used in Python commands.""" + from __future__ import annotations from byte.lib.log import get_logger diff --git a/src/cli.py b/src/cli.py index 3792729..78534dc 100644 --- a/src/cli.py +++ b/src/cli.py @@ -1,4 +1,5 @@ """Project CLI.""" + from __future__ import annotations import multiprocessing diff --git a/src/server/__init__.py b/src/server/__init__.py index e2374c1..f3a871a 100644 --- a/src/server/__init__.py +++ b/src/server/__init__.py @@ -1,4 +1,5 @@ """Byte Bot Server.""" + from __future__ import annotations from server import domain, lib diff --git a/src/server/domain/__init__.py b/src/server/domain/__init__.py index 768c733..bea0a22 100644 --- a/src/server/domain/__init__.py +++ b/src/server/domain/__init__.py @@ -1,4 +1,5 @@ """Application Modules.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/src/server/domain/db/__init__.py b/src/server/domain/db/__init__.py index ea8a50c..c8a8fea 100644 --- a/src/server/domain/db/__init__.py +++ b/src/server/domain/db/__init__.py @@ -1,4 +1,5 @@ """Domain for database models.""" + from server.domain.db import models __all__ = ["models"] diff --git a/src/server/domain/db/models.py b/src/server/domain/db/models.py index f57e256..ecbe42f 100644 --- a/src/server/domain/db/models.py +++ b/src/server/domain/db/models.py @@ -1,4 +1,5 @@ """Shared models.""" + from __future__ import annotations from uuid import UUID # noqa: TCH003 diff --git a/src/server/domain/github/helpers.py b/src/server/domain/github/helpers.py index edf4bcc..fbab915 100644 --- a/src/server/domain/github/helpers.py +++ b/src/server/domain/github/helpers.py @@ -1,4 +1,5 @@ """Helper functions for use within the GitHub domain.""" + from __future__ import annotations from githubkit import AppInstallationAuthStrategy, GitHub # type: ignore[reportMissingImports] diff --git a/src/server/domain/guilds/controllers.py b/src/server/domain/guilds/controllers.py index 21d53f4..fca75d3 100644 --- a/src/server/domain/guilds/controllers.py +++ b/src/server/domain/guilds/controllers.py @@ -1,4 +1,5 @@ """Guild controller.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/src/server/domain/guilds/dependencies.py b/src/server/domain/guilds/dependencies.py index b84c34d..1d3ef92 100644 --- a/src/server/domain/guilds/dependencies.py +++ b/src/server/domain/guilds/dependencies.py @@ -1,4 +1,5 @@ """Dependencies for guilds.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/src/server/domain/guilds/helpers.py b/src/server/domain/guilds/helpers.py index 6d0ec14..e1edd15 100644 --- a/src/server/domain/guilds/helpers.py +++ b/src/server/domain/guilds/helpers.py @@ -1,4 +1,5 @@ """Helper functions to be used for interacting with Guild data.""" + from __future__ import annotations from server.domain.guilds.dependencies import provides_guilds_service diff --git a/src/server/domain/guilds/schemas.py b/src/server/domain/guilds/schemas.py index 89a2158..4110589 100644 --- a/src/server/domain/guilds/schemas.py +++ b/src/server/domain/guilds/schemas.py @@ -1,4 +1,5 @@ """API Schemas for guild domain.""" + from __future__ import annotations from uuid import UUID # noqa: TCH003 diff --git a/src/server/domain/guilds/services.py b/src/server/domain/guilds/services.py index c207966..96f1260 100644 --- a/src/server/domain/guilds/services.py +++ b/src/server/domain/guilds/services.py @@ -1,4 +1,5 @@ """Guild services.""" + from __future__ import annotations from typing import Any diff --git a/src/server/domain/guilds/urls.py b/src/server/domain/guilds/urls.py index 0ec7c1c..42989d9 100644 --- a/src/server/domain/guilds/urls.py +++ b/src/server/domain/guilds/urls.py @@ -1,4 +1,5 @@ """Guild URLs.""" + from __future__ import annotations from typing import Final diff --git a/src/server/domain/system/__init__.py b/src/server/domain/system/__init__.py index bde82a7..bf4e867 100644 --- a/src/server/domain/system/__init__.py +++ b/src/server/domain/system/__init__.py @@ -1,4 +1,5 @@ """System domain.""" + from __future__ import annotations from server.domain.system import controllers, dtos, helpers diff --git a/src/server/domain/system/controllers/__init__.py b/src/server/domain/system/controllers/__init__.py index dc0d429..43c936a 100644 --- a/src/server/domain/system/controllers/__init__.py +++ b/src/server/domain/system/controllers/__init__.py @@ -1,4 +1,5 @@ """System domain controllers.""" + from __future__ import annotations from server.domain.system.controllers import system diff --git a/src/server/domain/system/controllers/system.py b/src/server/domain/system/controllers/system.py index e411ee2..d4fd57d 100644 --- a/src/server/domain/system/controllers/system.py +++ b/src/server/domain/system/controllers/system.py @@ -1,4 +1,5 @@ """System Controller.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/src/server/domain/system/dtos.py b/src/server/domain/system/dtos.py index 469fa80..77f2c20 100644 --- a/src/server/domain/system/dtos.py +++ b/src/server/domain/system/dtos.py @@ -1,4 +1,5 @@ """System domain DTOS.""" + from dataclasses import dataclass from typing import Annotated, Literal diff --git a/src/server/domain/system/helpers.py b/src/server/domain/system/helpers.py index 2660dd9..abc93b8 100644 --- a/src/server/domain/system/helpers.py +++ b/src/server/domain/system/helpers.py @@ -1,4 +1,5 @@ """System domain helper functions.""" + from __future__ import annotations from time import time diff --git a/src/server/domain/urls.py b/src/server/domain/urls.py index 4de204c..810f0ba 100644 --- a/src/server/domain/urls.py +++ b/src/server/domain/urls.py @@ -1,4 +1,5 @@ """Domain URLs.""" + from __future__ import annotations from typing import Final diff --git a/src/server/domain/web/__init__.py b/src/server/domain/web/__init__.py index 7720fb5..772e8e6 100644 --- a/src/server/domain/web/__init__.py +++ b/src/server/domain/web/__init__.py @@ -1,4 +1,5 @@ """Web domain.""" + from __future__ import annotations from server.domain.web import controllers diff --git a/src/server/domain/web/controllers/__init__.py b/src/server/domain/web/controllers/__init__.py index 378db02..ead8ddf 100644 --- a/src/server/domain/web/controllers/__init__.py +++ b/src/server/domain/web/controllers/__init__.py @@ -1,4 +1,5 @@ """Web domain controllers.""" + from __future__ import annotations from server.domain.web.controllers import web diff --git a/src/server/domain/web/controllers/web.py b/src/server/domain/web/controllers/web.py index aa1a26a..80bd2b3 100644 --- a/src/server/domain/web/controllers/web.py +++ b/src/server/domain/web/controllers/web.py @@ -1,4 +1,5 @@ """Web Controller.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/src/server/lib/__init__.py b/src/server/lib/__init__.py index 7e6ba14..01a75a5 100644 --- a/src/server/lib/__init__.py +++ b/src/server/lib/__init__.py @@ -1,4 +1,5 @@ """Server Lib.""" + from __future__ import annotations from server.lib import ( diff --git a/src/server/lib/constants.py b/src/server/lib/constants.py index 84f29a7..6457f20 100644 --- a/src/server/lib/constants.py +++ b/src/server/lib/constants.py @@ -1,4 +1,5 @@ """Byte server constants.""" + from __future__ import annotations __all__ = [ diff --git a/src/server/lib/cors.py b/src/server/lib/cors.py index 02441eb..e050d18 100644 --- a/src/server/lib/cors.py +++ b/src/server/lib/cors.py @@ -1,4 +1,5 @@ """CORS config.""" + from litestar.config.cors import CORSConfig from server.lib import settings diff --git a/src/server/lib/db/__init__.py b/src/server/lib/db/__init__.py index 057fc56..2a6b238 100644 --- a/src/server/lib/db/__init__.py +++ b/src/server/lib/db/__init__.py @@ -1,4 +1,5 @@ """Core DB Package.""" + from __future__ import annotations from server.lib.db import orm diff --git a/src/server/lib/db/base.py b/src/server/lib/db/base.py index 7b87265..719bfec 100644 --- a/src/server/lib/db/base.py +++ b/src/server/lib/db/base.py @@ -1,4 +1,5 @@ """Database session and engine.""" + from __future__ import annotations from contextlib import asynccontextmanager diff --git a/src/server/lib/db/migrations/env.py b/src/server/lib/db/migrations/env.py index 06ecfdb..5e77ab9 100644 --- a/src/server/lib/db/migrations/env.py +++ b/src/server/lib/db/migrations/env.py @@ -1,4 +1,5 @@ """Alembic environment for migrations.""" + from __future__ import annotations import asyncio diff --git a/src/server/lib/db/migrations/versions/002_simplify_models.py b/src/server/lib/db/migrations/versions/002_simplify_models.py index a181f12..a4809d8 100644 --- a/src/server/lib/db/migrations/versions/002_simplify_models.py +++ b/src/server/lib/db/migrations/versions/002_simplify_models.py @@ -6,6 +6,7 @@ Create Date: 2023-12-18 03:20:32.171148+00:00 """ + from __future__ import annotations import warnings diff --git a/src/server/lib/db/migrations/versions/initial.py b/src/server/lib/db/migrations/versions/initial.py index b54c66b..7ffad26 100644 --- a/src/server/lib/db/migrations/versions/initial.py +++ b/src/server/lib/db/migrations/versions/initial.py @@ -4,6 +4,7 @@ Create Date: 2023-11-26 20:11:48.777676+00:00 """ + from __future__ import annotations import warnings diff --git a/src/server/lib/db/orm.py b/src/server/lib/db/orm.py index ab21a6d..1615d81 100644 --- a/src/server/lib/db/orm.py +++ b/src/server/lib/db/orm.py @@ -1,4 +1,5 @@ """Application ORM configuration.""" + from __future__ import annotations from typing import Any diff --git a/src/server/lib/dependencies.py b/src/server/lib/dependencies.py index af468c9..236f3d3 100644 --- a/src/server/lib/dependencies.py +++ b/src/server/lib/dependencies.py @@ -1,4 +1,5 @@ """Application dependency providers.""" + from __future__ import annotations from datetime import datetime diff --git a/src/server/lib/dto.py b/src/server/lib/dto.py index 34084c4..13ce0c9 100644 --- a/src/server/lib/dto.py +++ b/src/server/lib/dto.py @@ -1,4 +1,5 @@ """DTO Library layer module.""" + from __future__ import annotations from typing import TYPE_CHECKING, Literal, overload @@ -23,8 +24,7 @@ def config( rename_strategy: RenameStrategy | None = None, max_nested_depth: int | None = None, partial: bool | None = None, -) -> SQLAlchemyDTOConfig: - ... +) -> SQLAlchemyDTOConfig: ... @overload @@ -35,8 +35,7 @@ def config( rename_strategy: RenameStrategy | None = None, max_nested_depth: int | None = None, partial: bool | None = None, -) -> DTOConfig: - ... +) -> DTOConfig: ... # noinspection PyUnusedLocal diff --git a/src/server/lib/exceptions.py b/src/server/lib/exceptions.py index 8afb921..fa61ea2 100644 --- a/src/server/lib/exceptions.py +++ b/src/server/lib/exceptions.py @@ -3,6 +3,7 @@ Also, defines functions that translate service and repository exceptions into HTTP exceptions. """ + from __future__ import annotations import sys diff --git a/src/server/lib/log/controller.py b/src/server/lib/log/controller.py index 6e64f50..59a7d12 100644 --- a/src/server/lib/log/controller.py +++ b/src/server/lib/log/controller.py @@ -4,6 +4,7 @@ Adds a filter for health check route logs. """ + from __future__ import annotations import logging diff --git a/src/server/lib/log/utils.py b/src/server/lib/log/utils.py index 7e9a4dc..e6f5b1f 100644 --- a/src/server/lib/log/utils.py +++ b/src/server/lib/log/utils.py @@ -7,6 +7,7 @@ :class:`EventFilter` - A structlog processor that removes keys from the log event if they exist. """ + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/src/server/lib/openapi.py b/src/server/lib/openapi.py index 7f97d5d..1c65362 100644 --- a/src/server/lib/openapi.py +++ b/src/server/lib/openapi.py @@ -1,4 +1,5 @@ """OpenAPI Config.""" + from __future__ import annotations from litestar.openapi.config import OpenAPIConfig diff --git a/src/server/lib/repository.py b/src/server/lib/repository.py index 308595e..d822436 100644 --- a/src/server/lib/repository.py +++ b/src/server/lib/repository.py @@ -1,4 +1,5 @@ """Repository module.""" + from __future__ import annotations import random diff --git a/src/server/lib/schema.py b/src/server/lib/schema.py index 6855eb8..233608a 100644 --- a/src/server/lib/schema.py +++ b/src/server/lib/schema.py @@ -1,4 +1,5 @@ """Schema.""" + from __future__ import annotations from pydantic import BaseModel as _BaseModel diff --git a/src/server/lib/serialization.py b/src/server/lib/serialization.py index fb3163c..820f553 100644 --- a/src/server/lib/serialization.py +++ b/src/server/lib/serialization.py @@ -1,4 +1,5 @@ """Serialization Helpers.""" + from __future__ import annotations import datetime diff --git a/src/server/lib/service.py b/src/server/lib/service.py index bf9f5c6..882b0c9 100644 --- a/src/server/lib/service.py +++ b/src/server/lib/service.py @@ -3,6 +3,7 @@ RepositoryService object is generic on the domain model type, which should be an SQLAlchemy model. """ + from __future__ import annotations import contextlib @@ -41,8 +42,7 @@ class SQLAlchemyAsyncRepositoryService(_SQLAlchemyAsyncRepositoryService[ModelT] """ @overload - def to_dto(self, data: ModelT) -> ModelT: - ... + def to_dto(self, data: ModelT) -> ModelT: ... @overload def to_dto( @@ -50,8 +50,7 @@ def to_dto( data: Sequence[ModelT], total: int | None = None, *filters: FilterTypes | ColumnElement[bool], - ) -> OffsetPagination[ModelT]: - ... + ) -> OffsetPagination[ModelT]: ... def to_dto( self, @@ -82,8 +81,7 @@ def to_dto( ) @overload - def to_schema(self, dto: type[ModelDTOT], data: ModelT) -> ModelDTOT: - ... + def to_schema(self, dto: type[ModelDTOT], data: ModelT) -> ModelDTOT: ... @overload def to_schema( @@ -92,8 +90,7 @@ def to_schema( data: Sequence[ModelT], total: int | None = None, *filters: FilterTypes, - ) -> OffsetPagination[ModelDTOT]: - ... + ) -> OffsetPagination[ModelDTOT]: ... def to_schema( self, diff --git a/src/server/lib/settings.py b/src/server/lib/settings.py index c873408..d8828bf 100644 --- a/src/server/lib/settings.py +++ b/src/server/lib/settings.py @@ -1,4 +1,5 @@ """Project Settings.""" + from __future__ import annotations import base64 diff --git a/src/server/lib/static_files.py b/src/server/lib/static_files.py index 524bb4b..c0dd33f 100644 --- a/src/server/lib/static_files.py +++ b/src/server/lib/static_files.py @@ -1,4 +1,5 @@ """Static files configuration.""" + from __future__ import annotations from pathlib import Path diff --git a/src/server/lib/template.py b/src/server/lib/template.py index f623233..2287430 100644 --- a/src/server/lib/template.py +++ b/src/server/lib/template.py @@ -2,6 +2,7 @@ See TemplateSettings for configuration. """ + from __future__ import annotations from litestar.template.config import TemplateConfig diff --git a/src/server/lib/types.py b/src/server/lib/types.py index 5850f5a..6fdfd07 100644 --- a/src/server/lib/types.py +++ b/src/server/lib/types.py @@ -1,4 +1,5 @@ """Library module for type definitions to be used in the application.""" + from __future__ import annotations from typing import TYPE_CHECKING, Any, Literal, TypeAlias, TypeVar diff --git a/src/utils.py b/src/utils.py index 3159fa0..cab0a9a 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,4 +1,5 @@ """General utility functions.""" + from __future__ import annotations import base64 diff --git a/tools/build_docs.py b/tools/build_docs.py index f15122d..c274b51 100644 --- a/tools/build_docs.py +++ b/tools/build_docs.py @@ -1,4 +1,5 @@ """Builds the documentation and copies it to the output directory.""" + from __future__ import annotations import argparse