diff --git a/royalnet/__init__.py b/royalnet/__init__.py index 3a1111c2..26bc2da9 100644 --- a/royalnet/__init__.py +++ b/royalnet/__init__.py @@ -1,14 +1,5 @@ -from . import alchemy, bard, commands, constellation, herald, backpack, serf, utils, version +from .version import semantic -__version__ = version.semantic +__version__ = semantic -__all__ = [ - "alchemy", - "bard", - "commands", - "constellation", - "herald", - "serf", - "utils", - "backpack", -] +__all__ = [] diff --git a/royalnet/__main__.py b/royalnet/__main__.py index 11024b86..5c451ca5 100644 --- a/royalnet/__main__.py +++ b/royalnet/__main__.py @@ -1,11 +1,13 @@ import click import multiprocessing -import royalnet.constellation as rc -import royalnet.serf as rs -import royalnet.utils as ru -import royalnet.herald as rh import toml import logging +import royalnet.constellation as rc +import royalnet.serf.telegram as rst +import royalnet.serf.discord as rsd +import royalnet.serf.matrix as rsm +import royalnet.utils as ru +import royalnet.herald as rh try: import coloredlogs @@ -60,7 +62,7 @@ def run(config_filename: str): telegram_process = None if "Telegram" in config["Serfs"] and config["Serfs"]["Telegram"]["enabled"]: telegram_process = multiprocessing.Process(name="Serf.Telegram", - target=rs.telegram.TelegramSerf.run_process, + target=rst.TelegramSerf.run_process, daemon=True, kwargs={ "alchemy_cfg": config["Alchemy"], @@ -78,7 +80,7 @@ def run(config_filename: str): discord_process = None if "Discord" in config["Serfs"] and config["Serfs"]["Discord"]["enabled"]: discord_process = multiprocessing.Process(name="Serf.Discord", - target=rs.discord.DiscordSerf.run_process, + target=rsd.DiscordSerf.run_process, daemon=True, kwargs={ "alchemy_cfg": config["Alchemy"], @@ -96,7 +98,7 @@ def run(config_filename: str): matrix_process = None if "Matrix" in config["Serfs"] and config["Serfs"]["Matrix"]["enabled"]: matrix_process = multiprocessing.Process(name="Serf.Matrix", - target=rs.matrix.MatrixSerf.run_process, + target=rsm.MatrixSerf.run_process, daemon=True, kwargs={ "alchemy_cfg": config["Alchemy"], diff --git a/royalnet/alchemy/README.md b/royalnet/alchemy/README.md new file mode 100644 index 00000000..65f364c4 --- /dev/null +++ b/royalnet/alchemy/README.md @@ -0,0 +1,15 @@ +# `royalnet.alchemy` + +The subpackage providing all functions and classes related to databases and tables. + +It requires either the `alchemy_easy` or the `alchemy_hard` extras to be installed. + +You can install `alchemy_easy` with: +``` +pip install royalnet[alchemy_easy] +``` +To install `alchemy_hard`, refer to the [`psycopg2`](https://pypi.org/project/psycopg2/) installation instructions, +then run: +``` +pip install royalnet[alchemy_hard] +``` \ No newline at end of file diff --git a/royalnet/alchemy/__init__.py b/royalnet/alchemy/__init__.py index c0f8070f..6a238f38 100644 --- a/royalnet/alchemy/__init__.py +++ b/royalnet/alchemy/__init__.py @@ -1,4 +1,17 @@ -"""Relational database classes and methods.""" +"""The subpackage providing all functions and classes related to databases and tables. + +It requires either the ``alchemy_easy`` or the ``alchemy_hard`` extras to be installed. + +You can install ``alchemy_easy`` with: :: + + pip install royalnet[alchemy_easy] + +To install ``alchemy_hard``, refer to the `psycopg2 }`_ installation instructions, +then run: :: + + pip install royalnet[alchemy_hard] + +""" from .alchemy import Alchemy from .table_dfs import table_dfs @@ -8,5 +21,5 @@ __all__ = [ "Alchemy", "table_dfs", "AlchemyException", - "TableNotFoundError" + "TableNotFoundError", ] diff --git a/royalnet/alchemy/alchemy.py b/royalnet/alchemy/alchemy.py index 65af1414..27704632 100644 --- a/royalnet/alchemy/alchemy.py +++ b/royalnet/alchemy/alchemy.py @@ -2,21 +2,12 @@ from typing import Set, Dict, Union from contextlib import contextmanager, asynccontextmanager from royalnet.utils import asyncify from royalnet.alchemy.errors import TableNotFoundError - -try: - from sqlalchemy import create_engine - from sqlalchemy.engine import Engine - from sqlalchemy.schema import Table - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy.ext.declarative.api import DeclarativeMeta - from sqlalchemy.orm import sessionmaker -except ImportError: - create_engine = None - Engine = None - Table = None - declarative_base = None - DeclarativeMeta = None - sessionmaker = None +from sqlalchemy import create_engine +from sqlalchemy.engine import Engine +from sqlalchemy.schema import Table +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.ext.declarative.api import DeclarativeMeta +from sqlalchemy.orm import sessionmaker class Alchemy: @@ -31,9 +22,6 @@ class Alchemy: tables: The :class:`set` of tables to be created and used in the selected database. Check the tables submodule for more details. """ - if create_engine is None: - raise ImportError("'alchemy' extra is not installed") - if database_uri.startswith("sqlite"): raise NotImplementedError("sqlite databases aren't supported, as they can't be used in multithreaded" " applications") diff --git a/royalnet/alchemy/table_dfs.py b/royalnet/alchemy/table_dfs.py index f9dbbdfa..28588018 100644 --- a/royalnet/alchemy/table_dfs.py +++ b/royalnet/alchemy/table_dfs.py @@ -1,9 +1,5 @@ -try: - from sqlalchemy.inspection import inspect - from sqlalchemy.schema import Table -except ImportError: - inspect = None - Table = None +from sqlalchemy.inspection import inspect +from sqlalchemy.schema import Table def table_dfs(starting_table: Table, ending_table: Table) -> tuple: @@ -11,9 +7,6 @@ def table_dfs(starting_table: Table, ending_table: Table) -> tuple: Returns: A :class:`tuple` containing the path, starting from the starting table and ending at the ending table.""" - if inspect is None: - raise ImportError("'alchemy' extra is not installed") - inspected = set() def search(_mapper, chain): diff --git a/royalnet/backpack/__init__.py b/royalnet/backpack/__init__.py index 0e9f6c05..a51af2ed 100644 --- a/royalnet/backpack/__init__.py +++ b/royalnet/backpack/__init__.py @@ -1,6 +1,4 @@ -"""A Pack that is imported by default by all Royalnet instances. - -Keep things here to a minimum!""" +"""A Pack that is imported by default by all Royalnet instances.""" from . import commands, tables, stars, events from .commands import available_commands diff --git a/royalnet/bard/README.md b/royalnet/bard/README.md new file mode 100644 index 00000000..2c64c054 --- /dev/null +++ b/royalnet/bard/README.md @@ -0,0 +1,10 @@ +# `royalnet.bard` + +The subpackage providing all classes related to music files. + +It requires the `bard` extra to be installed (the [`ffmpeg_python`](https://pypi.org/project/ffmpeg-python/), [`youtube_dl`](https://pypi.org/project/youtube_dl/) and [`eyed3`](https://pypi.org/project/eyeD3/) packages). + +You can install it with: +``` +pip install royalnet[bard] +``` diff --git a/royalnet/bard/__init__.py b/royalnet/bard/__init__.py index 5ead8635..1136c40c 100644 --- a/royalnet/bard/__init__.py +++ b/royalnet/bard/__init__.py @@ -1,18 +1,31 @@ +"""The subpackage providing all classes related to music files. + +It requires the ``bard`` extra to be installed (the :mod:`ffmpeg_python`, :mod:`youtube_dl` and :mod:`eyed3` packages). + +You can install it with: :: + + pip install royalnet[bard] + +""" + +try: + import ffmpeg + import youtube_dl + import eyed3 +except ImportError: + raise ImportError("The `bard` extra is not installed. Please install it with `pip install royalnet[bard]`.") + from .ytdlinfo import YtdlInfo from .ytdlfile import YtdlFile from .ytdlmp3 import YtdlMp3 -from .ytdldiscord import YtdlDiscord - -try: - from .fileaudiosource import FileAudioSource -except ImportError: - FileAudioSource = None - +from .errors import BardError, YtdlError, NotFoundError, MultipleFilesError __all__ = [ "YtdlInfo", "YtdlFile", "YtdlMp3", - "YtdlDiscord", - "FileAudioSource", + "BardError", + "YtdlError", + "NotFoundError", + "MultipleFilesError", ] diff --git a/royalnet/bard/discord/README.md b/royalnet/bard/discord/README.md new file mode 100644 index 00000000..32fb43dd --- /dev/null +++ b/royalnet/bard/discord/README.md @@ -0,0 +1,10 @@ +# `royalnet.bard.discord` + +The subpackage providing all functions and classes related to music playback on Discord. + +It requires the both the ``bard`` and ``discord`` extras to be installed. + +You can install them with: +``` + pip install royalnet[bard,discord] +``` \ No newline at end of file diff --git a/royalnet/bard/discord/__init__.py b/royalnet/bard/discord/__init__.py new file mode 100644 index 00000000..91a729ef --- /dev/null +++ b/royalnet/bard/discord/__init__.py @@ -0,0 +1,17 @@ +"""The subpackage providing all functions and classes related to music playback on Discord. + +It requires the both the ``bard`` and ``discord`` extras to be installed. + +You can install them with: :: + + pip install royalnet[bard,discord] + +""" + +from .ytdldiscord import YtdlDiscord +from .fileaudiosource import FileAudioSource + +__all__ = [ + "YtdlDiscord", + "FileAudioSource", +] diff --git a/royalnet/utils/fileaudiosource.py b/royalnet/bard/discord/fileaudiosource.py similarity index 95% rename from royalnet/utils/fileaudiosource.py rename to royalnet/bard/discord/fileaudiosource.py index 3be44d40..b8f4363e 100644 --- a/royalnet/utils/fileaudiosource.py +++ b/royalnet/bard/discord/fileaudiosource.py @@ -1,7 +1,4 @@ -try: - import discord -except ImportError: - discord = None +import discord class FileAudioSource(discord.AudioSource): diff --git a/royalnet/bard/ytdldiscord.py b/royalnet/bard/discord/ytdldiscord.py similarity index 90% rename from royalnet/bard/ytdldiscord.py rename to royalnet/bard/discord/ytdldiscord.py index 0311d316..02263483 100644 --- a/royalnet/bard/ytdldiscord.py +++ b/royalnet/bard/discord/ytdldiscord.py @@ -2,19 +2,12 @@ import typing import re import os import logging +import ffmpeg +import discord from contextlib import asynccontextmanager -from royalnet.utils import asyncify, MultiLock, FileAudioSource +from royalnet.utils import asyncify, MultiLock from royalnet.bard import YtdlInfo, YtdlFile - -try: - import ffmpeg -except ImportError: - ffmpeg = None - -try: - import discord -except ImportError: - discord = None +from .fileaudiosource import FileAudioSource log = logging.getLogger(__name__) @@ -44,8 +37,6 @@ class YtdlDiscord: async def convert_to_pcm(self) -> None: """Convert the file to pcm with :mod:`ffmpeg`.""" - if ffmpeg is None: - raise ImportError("'bard' extra is not installed") await self.ytdl_file.download_file() if self.pcm_filename is None: async with self.ytdl_file.lock.normal(): @@ -87,8 +78,6 @@ class YtdlDiscord: @asynccontextmanager async def spawn_audiosource(self): log.debug(f"Spawning audio_source for: {self}") - if FileAudioSource is None: - raise ImportError("'discord' extra is not installed") await self.convert_to_pcm() async with self.lock.normal(): with open(self.pcm_filename, "rb") as stream: @@ -97,8 +86,6 @@ class YtdlDiscord: def embed(self) -> "discord.Embed": """Return this info as a :py:class:`discord.Embed`.""" - if discord is None: - raise ImportError("'discord' extra is not installed") colors = { "youtube": 0xCC0000, "soundcloud": 0xFF5400, diff --git a/royalnet/bard/ytdlfile.py b/royalnet/bard/ytdlfile.py index 367f81a4..9427cab6 100644 --- a/royalnet/bard/ytdlfile.py +++ b/royalnet/bard/ytdlfile.py @@ -8,11 +8,7 @@ from royalnet.utils import * from asyncio import AbstractEventLoop, get_event_loop from .ytdlinfo import YtdlInfo from .errors import NotFoundError, MultipleFilesError - -try: - from youtube_dl import YoutubeDL -except ImportError: - YoutubeDL = None +from youtube_dl import YoutubeDL log = logging.getLogger(__name__) @@ -78,9 +74,6 @@ class YtdlFile: async def download_file(self) -> None: """Download the file.""" - if YoutubeDL is None: - raise ImportError("'bard' extra is not installed") - def download(): """Download function block to be asyncified.""" with YoutubeDL(self.ytdl_args) as ytdl: diff --git a/royalnet/bard/ytdlinfo.py b/royalnet/bard/ytdlinfo.py index ec4b0c97..6b1f7a54 100644 --- a/royalnet/bard/ytdlinfo.py +++ b/royalnet/bard/ytdlinfo.py @@ -1,14 +1,10 @@ -from asyncio import AbstractEventLoop, get_event_loop -from typing import Optional, Dict, List, Any -from datetime import datetime, timedelta +from typing import * +import asyncio as aio +import datetime import dateparser import logging -from royalnet.utils import ytdldateformat, asyncify - -try: - from youtube_dl import YoutubeDL -except ImportError: - YoutubeDL = None +import royalnet.utils as ru +import youtube_dl log = logging.getLogger(__name__) @@ -36,7 +32,7 @@ class YtdlInfo: self.uploader_url: Optional[str] = info.get("uploader_url") self.channel_id: Optional[str] = info.get("channel_id") self.channel_url: Optional[str] = info.get("channel_url") - self.upload_date: Optional[datetime] = dateparser.parse(ytdldateformat(info.get("upload_date"))) + self.upload_date: Optional[datetime.datetime] = dateparser.parse(ru.ytdldateformat(info.get("upload_date"))) self.license: Optional[str] = info.get("license") self.creator: Optional[...] = info.get("creator") self.title: Optional[str] = info.get("title") @@ -47,7 +43,7 @@ class YtdlInfo: self.tags: Optional[List[str]] = info.get("tags") self.subtitles: Optional[Dict[str, List[Dict[str, str]]]] = info.get("subtitles") self.automatic_captions: Optional[dict] = info.get("automatic_captions") - self.duration: Optional[timedelta] = timedelta(seconds=info.get("duration", 0)) + self.duration: Optional[datetime.timedelta] = datetime.timedelta(seconds=info.get("duration", 0)) self.age_limit: Optional[int] = info.get("age_limit") self.annotations: Optional[...] = info.get("annotations") self.chapters: Optional[...] = info.get("chapters") @@ -90,20 +86,17 @@ class YtdlInfo: self.album: Optional[str] = None @classmethod - async def from_url(cls, url, loop: Optional[AbstractEventLoop] = None, **ytdl_args) -> List["YtdlInfo"]: + async def from_url(cls, url, loop: Optional[aio.AbstractEventLoop] = None, **ytdl_args) -> List["YtdlInfo"]: """Fetch the info for an url through :class:`YoutubeDL`. Returns: A :class:`list` containing the infos for the requested videos.""" - if YoutubeDL is None: - raise ImportError("'bard' extra is not installed") - if loop is None: - loop: AbstractEventLoop = get_event_loop() + loop: aio.AbstractEventLoop = aio.get_event_loop() # So many redundant options! log.debug(f"Fetching info: {url}") - with YoutubeDL({**cls._default_ytdl_args, **ytdl_args}) as ytdl: - first_info = await asyncify(ytdl.extract_info, loop=loop, url=url, download=False) + with youtube_dl.YoutubeDL({**cls._default_ytdl_args, **ytdl_args}) as ytdl: + first_info = await ru.asyncify(ytdl.extract_info, loop=loop, url=url, download=False) # No video was found if first_info is None: return [] diff --git a/royalnet/bard/ytdlmp3.py b/royalnet/bard/ytdlmp3.py index b355ced8..7dfda4d4 100644 --- a/royalnet/bard/ytdlmp3.py +++ b/royalnet/bard/ytdlmp3.py @@ -1,22 +1,18 @@ -import typing +from typing import * import re import os -from royalnet.utils import asyncify, MultiLock +import ffmpeg +import royalnet.utils as ru from .ytdlinfo import YtdlInfo from .ytdlfile import YtdlFile -try: - import ffmpeg -except ImportError: - ffmpeg = None - class YtdlMp3: """A representation of a :class:`YtdlFile` conversion to mp3.""" def __init__(self, ytdl_file: YtdlFile): self.ytdl_file: YtdlFile = ytdl_file - self.mp3_filename: typing.Optional[str] = None - self.lock: MultiLock = MultiLock() + self.mp3_filename: Optional[str] = None + self.lock: ru.MultiLock = ru.MultiLock() def __repr__(self): if not self.ytdl_file.has_info: @@ -35,14 +31,12 @@ class YtdlMp3: async def convert_to_mp3(self) -> None: """Convert the file to mp3 with :mod:`ffmpeg`.""" - if ffmpeg is None: - raise ImportError("'bard' extra is not installed") await self.ytdl_file.download_file() if self.mp3_filename is None: async with self.ytdl_file.lock.normal(): destination_filename = re.sub(r"\.[^.]+$", ".mp3", self.ytdl_file.filename) async with self.lock.exclusive(): - await asyncify( + await ru.asyncify( ffmpeg.input(self.ytdl_file.filename) .output(destination_filename, format="mp3") .overwrite_output() @@ -58,7 +52,7 @@ class YtdlMp3: self.mp3_filename = None @classmethod - async def from_url(cls, url, **ytdl_args) -> typing.List["YtdlMp3"]: + async def from_url(cls, url, **ytdl_args) -> List["YtdlMp3"]: """Create a :class:`list` of :class:`YtdlMp3` from a URL.""" files = await YtdlFile.from_url(url, **ytdl_args) dfiles = [] @@ -68,6 +62,6 @@ class YtdlMp3: return dfiles @property - def info(self) -> typing.Optional[YtdlInfo]: + def info(self) -> Optional[YtdlInfo]: """Shortcut to get the :class:`YtdlInfo` of the object.""" return self.ytdl_file.info diff --git a/royalnet/commands/README.md b/royalnet/commands/README.md new file mode 100644 index 00000000..5026f992 --- /dev/null +++ b/royalnet/commands/README.md @@ -0,0 +1,3 @@ +# `royalnet.commands` + +The subpackage providing all classes related to Royalnet commands. diff --git a/royalnet/commands/__init__.py b/royalnet/commands/__init__.py index dd37703d..ca5f5785 100644 --- a/royalnet/commands/__init__.py +++ b/royalnet/commands/__init__.py @@ -1,15 +1,12 @@ +"""The subpackage providing all classes related to Royalnet commands.""" + from .commandinterface import CommandInterface from .command import Command from .commanddata import CommandData from .commandargs import CommandArgs from .event import Event -from .errors import CommandError, \ - InvalidInputError, \ - UnsupportedError, \ - ConfigurationError, \ - ExternalError, \ - UserError, \ - ProgramError +from .errors import \ + CommandError, InvalidInputError, UnsupportedError, ConfigurationError, ExternalError, UserError, ProgramError from .keyboardkey import KeyboardKey __all__ = [ diff --git a/royalnet/commands/command.py b/royalnet/commands/command.py index e91b0119..725af01f 100644 --- a/royalnet/commands/command.py +++ b/royalnet/commands/command.py @@ -1,4 +1,4 @@ -import typing +from typing import * from .commandinterface import CommandInterface from .commandargs import CommandArgs from .commanddata import CommandData @@ -11,7 +11,7 @@ class Command: Example: To be able to call ``/example`` on Telegram, the name should be ``"example"``.""" - aliases: typing.List[str] = [] + aliases: List[str] = [] """A list of possible aliases for a command. Example: diff --git a/royalnet/commands/commandargs.py b/royalnet/commands/commandargs.py index 5781c4bb..e5211c70 100644 --- a/royalnet/commands/commandargs.py +++ b/royalnet/commands/commandargs.py @@ -1,5 +1,5 @@ import re -from typing import Pattern, AnyStr, Optional, Sequence, Union +from typing import * from .errors import InvalidInputError diff --git a/royalnet/commands/commanddata.py b/royalnet/commands/commanddata.py index a3b448bd..93ee1213 100644 --- a/royalnet/commands/commanddata.py +++ b/royalnet/commands/commanddata.py @@ -1,11 +1,11 @@ +from typing import * import contextlib import logging import asyncio as aio -from typing import * -from sqlalchemy.orm.session import Session +import royalnet.utils as ru from .errors import UnsupportedError from .commandinterface import CommandInterface -import royalnet.utils as ru + if TYPE_CHECKING: from .keyboardkey import KeyboardKey @@ -14,26 +14,28 @@ log = logging.getLogger(__name__) class CommandData: def __init__(self, interface: CommandInterface, loop: aio.AbstractEventLoop): - self._interface: CommandInterface = interface self.loop: aio.AbstractEventLoop = loop + self._interface: CommandInterface = interface self._session = None + # TODO: make this asyncronous... somehow? @property def session(self): if self._session is None: if self._interface.alchemy is None: raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance") - # FIXME: this may take a while self._session = self._interface.alchemy.Session() return self._session async def session_commit(self): + """Asyncronously commit the :attr:`.session` of this object.""" if self._session: log.warning("Session had to be created to be committed") # noinspection PyUnresolvedReferences await ru.asyncify(self.session.commit) async def session_close(self): + """Asyncronously close the :attr:`.session` of this object.""" if self._session is not None: await ru.asyncify(self._session.close) diff --git a/royalnet/commands/commandinterface.py b/royalnet/commands/commandinterface.py index 175b440b..e0de7e77 100644 --- a/royalnet/commands/commandinterface.py +++ b/royalnet/commands/commandinterface.py @@ -1,6 +1,7 @@ from typing import * import asyncio as aio from .errors import UnsupportedError + if TYPE_CHECKING: from .event import Event from .command import Command diff --git a/royalnet/commands/event.py b/royalnet/commands/event.py index 761bccbf..e4309c7a 100644 --- a/royalnet/commands/event.py +++ b/royalnet/commands/event.py @@ -1,5 +1,7 @@ +import asyncio as aio from .commandinterface import CommandInterface from typing import TYPE_CHECKING + if TYPE_CHECKING: from serf import Serf @@ -16,7 +18,7 @@ class Event: """The :class:`CommandInterface` available to this :class:`Event`.""" @property - def serf(self): + def serf(self) -> Serf: """A shortcut for :attr:`.interface.serf`.""" return self.interface.serf @@ -26,12 +28,12 @@ class Event: return self.interface.alchemy @property - def loop(self): + def loop(self) -> aio.AbstractEventLoop: """A shortcut for :attr:`.interface.loop`.""" return self.interface.loop @property - def config(self): + def config(self) -> dict: """A shortcut for :attr:`.interface.config`.""" return self.interface.config diff --git a/royalnet/constellation/README.md b/royalnet/constellation/README.md index c14c3480..f5a59f2a 100644 --- a/royalnet/constellation/README.md +++ b/royalnet/constellation/README.md @@ -1,11 +1,17 @@ # `royalnet.constellation` -The part of `royalnet` that handles the webserver and webpages. +The subpackage providing all functions and classes that handle the webserver and the webpages. -It uses many features of [`starlette`](https://www.starlette.io). +It requires the `constellation` extra to be installed ([`starlette`](https://github.com/encode/starlette)). -## Hierarchy +You can install it with: +``` +pip install royalnet[constellation] +``` -- `constellation` - - `star` - - `shoot` \ No newline at end of file +It optionally uses the `sentry` extra. + +You can install it with: +``` +pip install royalnet[constellation,sentry] +``` \ No newline at end of file diff --git a/royalnet/constellation/__init__.py b/royalnet/constellation/__init__.py index effea7b3..69c62af0 100644 --- a/royalnet/constellation/__init__.py +++ b/royalnet/constellation/__init__.py @@ -1,6 +1,18 @@ -"""The part of :mod:`royalnet` that handles the webserver and webpages. +"""The subpackage providing all functions and classes that handle the webserver and the webpages. -It uses many features of :mod:`starlette`.""" +It requires the ``constellation`` extra to be installed (:mod:`starlette`). + +You can install it with: :: + + pip install royalnet[constellation] + +It optionally uses the ``sentry`` extra for error reporting. + +You can install them with: :: + + pip install royalnet[constellation,sentry] + +""" from .constellation import Constellation from .star import Star, PageStar, ExceptionStar diff --git a/royalnet/constellation/constellation.py b/royalnet/constellation/constellation.py index 7d6f89af..8055c4ca 100644 --- a/royalnet/constellation/constellation.py +++ b/royalnet/constellation/constellation.py @@ -1,7 +1,9 @@ +from typing import * +import asyncio as aio import logging import importlib -import asyncio as aio -from typing import * +import uvicorn +import starlette.applications import royalnet.alchemy as ra import royalnet.herald as rh import royalnet.utils as ru @@ -9,26 +11,6 @@ import royalnet.commands as rc from .star import PageStar, ExceptionStar from ..utils import init_logging -try: - import uvicorn - from starlette.applications import Starlette -except ImportError: - uvicorn = None - Starlette = None - -try: - import sentry_sdk -except ImportError: - sentry_sdk = None - AioHttpIntegration = None - SqlalchemyIntegration = None - LoggingIntegration = None - -try: - import coloredlogs -except ImportError: - coloredlogs = None - log = logging.getLogger(__name__) @@ -54,9 +36,6 @@ class Constellation: constellation_cfg: Dict[str, Any], logging_cfg: Dict[str, Any] ): - if Starlette is None: - raise ImportError("`constellation` extra is not installed") - # Import packs pack_names = packs_cfg["active"] packs = {} @@ -112,7 +91,7 @@ class Constellation: self.events: Dict[str, rc.Event] = {} """A dictionary containing all :class:`~rc.Event` that can be handled by this :class:`Constellation`.""" - self.starlette = Starlette(debug=__debug__) + self.starlette = starlette.applications.Starlette(debug=__debug__) """The :class:`~starlette.Starlette` app.""" # Register Events diff --git a/royalnet/herald/README.md b/royalnet/herald/README.md new file mode 100644 index 00000000..1f1913f8 --- /dev/null +++ b/royalnet/herald/README.md @@ -0,0 +1,12 @@ +# `royalnet.herald` + +The subpackage providing all functions and classes to handle communication between process (even over the Internet). + +It is based on [`websockets`](https://github.com/websockets). + +It requires the `herald` extra to be installed. + +You can install it with: +``` +pip install royalnet[herald] +``` diff --git a/royalnet/herald/__init__.py b/royalnet/herald/__init__.py index 291c7088..bb340070 100644 --- a/royalnet/herald/__init__.py +++ b/royalnet/herald/__init__.py @@ -1,5 +1,17 @@ +"""The subpackage providing all functions and classes to handle communication between process (even over the Internet). + +It is based on :mod:`websockets`. + +It requires the ``herald`` extra to be installed. + +You can install it with: :: + + pip install royalnet[herald] + +""" + from .config import Config -from .errors import HeraldError, ConnectionClosedError, LinkError, InvalidServerResponseError, ServerError +from .errors import * from .link import Link from .package import Package from .request import Request diff --git a/royalnet/herald/broadcast.py b/royalnet/herald/broadcast.py index 848aff9b..165a1ed7 100644 --- a/royalnet/herald/broadcast.py +++ b/royalnet/herald/broadcast.py @@ -1,8 +1,8 @@ -import typing +from typing import * class Broadcast: - def __init__(self, handler: str, data: dict, msg_type: typing.Optional[str] = None): + def __init__(self, handler: str, data: dict, msg_type: Optional[str] = None): super().__init__() if msg_type is not None: assert msg_type == self.__class__.__name__ diff --git a/royalnet/herald/link.py b/royalnet/herald/link.py index 5a36cf34..bbadc091 100644 --- a/royalnet/herald/link.py +++ b/royalnet/herald/link.py @@ -1,8 +1,9 @@ -import asyncio +from typing import * +import asyncio as aio import uuid import functools -import logging as _logging -import typing +import logging +import websockets from .package import Package from .request import Request from .response import Response, ResponseSuccess, ResponseFailure @@ -10,23 +11,18 @@ from .broadcast import Broadcast from .errors import ConnectionClosedError, InvalidServerResponseError from .config import Config -try: - import websockets -except ImportError: - websockets = None - -log = _logging.getLogger(__name__) +log = logging.getLogger(__name__) class PendingRequest: - def __init__(self, *, loop: asyncio.AbstractEventLoop = None): + def __init__(self, *, loop: aio.AbstractEventLoop = None): if loop is None: - self.loop = asyncio.get_event_loop() + self.loop = aio.get_event_loop() else: self.loop = loop - self.event: asyncio.Event = asyncio.Event(loop=loop) - self.data: typing.Optional[dict] = None + self.event: aio.Event = aio.Event(loop=loop) + self.data: Optional[dict] = None def __repr__(self): if self.event.is_set(): @@ -56,22 +52,20 @@ def requires_identification(func): class Link: def __init__(self, config: Config, request_handler, *, - loop: asyncio.AbstractEventLoop = None): - if websockets is None: - raise ImportError("'websockets' extra is not installed") + loop: aio.AbstractEventLoop = None): self.config: Config = config self.nid: str = str(uuid.uuid4()) - self.websocket: typing.Optional["websockets.WebSocketClientProtocol"] = None - self.request_handler: typing.Callable[[typing.Union[Request, Broadcast]], - typing.Awaitable[Response]] = request_handler - self._pending_requests: typing.Dict[str, PendingRequest] = {} + self.websocket: Optional["websockets.WebSocketClientProtocol"] = None + self.request_handler: Callable[[Union[Request, Broadcast]], + Awaitable[Response]] = request_handler + self._pending_requests: Dict[str, PendingRequest] = {} if loop is None: - self._loop = asyncio.get_event_loop() + self._loop = aio.get_event_loop() else: self._loop = loop - self.error_event: asyncio.Event = asyncio.Event(loop=self._loop) - self.connect_event: asyncio.Event = asyncio.Event(loop=self._loop) - self.identify_event: asyncio.Event = asyncio.Event(loop=self._loop) + self.error_event: aio.Event = aio.Event(loop=self._loop) + self.connect_event: aio.Event = aio.Event(loop=self._loop) + self.identify_event: aio.Event = aio.Event(loop=self._loop) def __repr__(self): if self.identify_event.is_set(): diff --git a/royalnet/herald/package.py b/royalnet/herald/package.py index 77ca97a2..d5e391c8 100644 --- a/royalnet/herald/package.py +++ b/royalnet/herald/package.py @@ -1,6 +1,6 @@ import json import uuid -import typing +from typing import * class Package: @@ -14,8 +14,8 @@ class Package: *, source: str, destination: str, - source_conv_id: typing.Optional[str] = None, - destination_conv_id: typing.Optional[str] = None): + source_conv_id: Optional[str] = None, + destination_conv_id: Optional[str] = None): """Create a Package. Parameters: @@ -31,7 +31,7 @@ class Package: self.source: str = source self.source_conv_id: str = source_conv_id or str(uuid.uuid4()) self.destination: str = destination - self.destination_conv_id: typing.Optional[str] = destination_conv_id + self.destination_conv_id: Optional[str] = destination_conv_id def __repr__(self): return f"<{self.__class__.__qualname__} {self.source} » {self.destination}>" diff --git a/royalnet/herald/request.py b/royalnet/herald/request.py index 1d43e408..6655b65f 100644 --- a/royalnet/herald/request.py +++ b/royalnet/herald/request.py @@ -1,4 +1,4 @@ -import typing +from typing import * class Request: @@ -6,7 +6,7 @@ class Request: It contains the name of the requested handler, in addition to the data.""" - def __init__(self, handler: str, data: dict, msg_type: typing.Optional[str] = None): + def __init__(self, handler: str, data: dict, msg_type: Optional[str] = None): super().__init__() if msg_type is not None: assert msg_type == self.__class__.__name__ diff --git a/royalnet/herald/response.py b/royalnet/herald/response.py index 73d830bb..460fefc8 100644 --- a/royalnet/herald/response.py +++ b/royalnet/herald/response.py @@ -1,4 +1,4 @@ -import typing +from typing import * class Response: @@ -28,7 +28,7 @@ class Response: class ResponseSuccess(Response): """A response to a successful :py:class:`Request`.""" - def __init__(self, data: typing.Optional[dict] = None): + def __init__(self, data: Optional[dict] = None): if data is None: self.data = {} else: @@ -41,10 +41,10 @@ class ResponseSuccess(Response): class ResponseFailure(Response): """A response to a invalid :py:class:`Request`.""" - def __init__(self, name: str, description: str, extra_info: typing.Optional[dict] = None): + def __init__(self, name: str, description: str, extra_info: Optional[dict] = None): self.name: str = name self.description: str = description - self.extra_info: typing.Optional[dict] = extra_info + self.extra_info: Optional[dict] = extra_info def __repr__(self): return f"{self.__class__.__qualname__}(name={self.name}, description={self.description}, extra_info={self.extra_info})" diff --git a/royalnet/herald/server.py b/royalnet/herald/server.py index 6c672a2f..d3378887 100644 --- a/royalnet/herald/server.py +++ b/royalnet/herald/server.py @@ -1,25 +1,16 @@ from typing import * +import asyncio as aio import re import datetime import uuid -import asyncio -import logging as _logging +import logging +import websockets import royalnet.utils as ru from .package import Package from .config import Config -try: - import coloredlogs -except ImportError: - coloredlogs = None -try: - import websockets -except ImportError: - websockets = None - - -log = _logging.getLogger(__name__) +log = logging.getLogger(__name__) class ConnectedClient: @@ -49,7 +40,7 @@ class ConnectedClient: class Server: - def __init__(self, config: Config, *, loop: asyncio.AbstractEventLoop = None): + def __init__(self, config: Config, *, loop: aio.AbstractEventLoop = None): self.config: Config = config self.identified_clients: List[ConnectedClient] = [] self.loop = loop @@ -165,5 +156,5 @@ class Server: def run_blocking(self, logging_cfg: Dict[str, Any]): ru.init_logging(logging_cfg) if self.loop is None: - self.loop = asyncio.get_event_loop() + self.loop = aio.get_event_loop() self.serve() diff --git a/royalnet/serf/README.md b/royalnet/serf/README.md new file mode 100644 index 00000000..2cdb7208 --- /dev/null +++ b/royalnet/serf/README.md @@ -0,0 +1,3 @@ +# `royalnet.serf` + +The subpackage providing all Serf implementations. diff --git a/royalnet/serf/__init__.py b/royalnet/serf/__init__.py index dffff404..dde206f3 100644 --- a/royalnet/serf/__init__.py +++ b/royalnet/serf/__init__.py @@ -1,11 +1,9 @@ +"""The subpackage providing all Serf implementations.""" + from .serf import Serf from .errors import SerfError -from . import telegram, discord, matrix __all__ = [ "Serf", "SerfError", - "telegram", - "discord", - "matrix", ] diff --git a/royalnet/serf/discord/README.md b/royalnet/serf/discord/README.md new file mode 100644 index 00000000..42bf305e --- /dev/null +++ b/royalnet/serf/discord/README.md @@ -0,0 +1,10 @@ +# `royalnet.serf.discord` + +A `Serf` implementation for Discord. + +It requires (obviously) the `discord` extra to be installed. + +Install it with: +``` +pip install royalnet[discord] +``` diff --git a/royalnet/serf/discord/__init__.py b/royalnet/serf/discord/__init__.py index 67120215..18a389ff 100644 --- a/royalnet/serf/discord/__init__.py +++ b/royalnet/serf/discord/__init__.py @@ -1,6 +1,12 @@ """A :class:`Serf` implementation for Discord. -It is pretty unstable, compared to the rest of the bot, but it *should* work.""" +It requires (obviously) the ``discord`` extra to be installed. + +Install it with: :: + + pip install royalnet[discord] + +""" from .escape import escape from .discordserf import DiscordSerf diff --git a/royalnet/serf/matrix/README.md b/royalnet/serf/matrix/README.md new file mode 100644 index 00000000..1543c9e0 --- /dev/null +++ b/royalnet/serf/matrix/README.md @@ -0,0 +1,10 @@ +# `royalnet.serf.matrix` + +A `Serf` implementation for Matrix. + +It requires (obviously) the `matrix` extra to be installed. + +Install it with: +``` +pip install royalnet[matrix] +``` diff --git a/royalnet/serf/matrix/__init__.py b/royalnet/serf/matrix/__init__.py index 534814a9..bd3ed2dc 100644 --- a/royalnet/serf/matrix/__init__.py +++ b/royalnet/serf/matrix/__init__.py @@ -1,3 +1,13 @@ +"""A :class:`Serf` implementation for Matrix. + +It requires (obviously) the ``matrix`` extra to be installed. + +Install it with: :: + + pip install royalnet[matrix] + +""" + from .matrixserf import MatrixSerf from .escape import escape diff --git a/royalnet/serf/serf.py b/royalnet/serf/serf.py index 408d1bb3..da957343 100644 --- a/royalnet/serf/serf.py +++ b/royalnet/serf/serf.py @@ -2,30 +2,13 @@ import logging import importlib import asyncio as aio from typing import * - from sqlalchemy.schema import Table - from royalnet.commands import * import royalnet.utils as ru import royalnet.alchemy as ra import royalnet.backpack as rb import royalnet.herald as rh -try: - import sentry_sdk - from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration - from sentry_sdk.integrations.aiohttp import AioHttpIntegration - from sentry_sdk.integrations.logging import LoggingIntegration -except ImportError: - sentry_sdk = None - SqlalchemyIntegration = None - AioHttpIntegration = None - LoggingIntegration = None - -try: - import coloredlogs -except ImportError: - coloredlogs = None log = logging.getLogger(__name__) @@ -317,8 +300,7 @@ class Serf: try: await key.press(data) except InvalidInputError as e: - await data.reply(f"⚠️ {e.message}\n" - f"Syntax: [c]{command.interface.prefix}{command.name} {command.syntax}[/c]") + await data.reply(f"⚠️ {e.message}") except UserError as e: await data.reply(f"⚠️ {e.message}") except UnsupportedError as e: @@ -337,7 +319,6 @@ class Serf: finally: await data.session_close() - async def run(self): """A coroutine that starts the event loop and handles command calls.""" self.herald_task = self.loop.create_task(self.herald.run()) diff --git a/royalnet/serf/telegram/README.md b/royalnet/serf/telegram/README.md new file mode 100644 index 00000000..7d0d1756 --- /dev/null +++ b/royalnet/serf/telegram/README.md @@ -0,0 +1,10 @@ +# `royalnet.serf.matrix` + +A `Serf` implementation for Telegram. + +It requires (obviously) the `telegram` extra to be installed. + +Install it with: +``` +pip install royalnet[telegram] +``` diff --git a/royalnet/serf/telegram/__init__.py b/royalnet/serf/telegram/__init__.py index 0e94ca9f..cb8b811b 100644 --- a/royalnet/serf/telegram/__init__.py +++ b/royalnet/serf/telegram/__init__.py @@ -1,7 +1,17 @@ +"""A :class:`Serf` implementation for Telegram. + +It requires (obviously) the ``telegram`` extra to be installed. + +Install it with: :: + + pip install royalnet[telegram] + +""" + from .escape import escape from .telegramserf import TelegramSerf __all__ = [ "escape", "TelegramSerf" -] \ No newline at end of file +] diff --git a/royalnet/utils/__init__.py b/royalnet/utils/__init__.py index 4db07821..deb9539b 100644 --- a/royalnet/utils/__init__.py +++ b/royalnet/utils/__init__.py @@ -3,7 +3,6 @@ from .sleep_until import sleep_until from .formatters import andformat, underscorize, ytdldateformat, numberemojiformat, ordinalformat from .urluuid import to_urluuid, from_urluuid from .multilock import MultiLock -from .fileaudiosource import FileAudioSource from .sentry import init_sentry, sentry_exc from .log import init_logging @@ -18,7 +17,6 @@ __all__ = [ "to_urluuid", "from_urluuid", "MultiLock", - "FileAudioSource", "init_sentry", "sentry_exc", "init_logging", diff --git a/royalnet/version.py b/royalnet/version.py index 71c01004..03217183 100644 --- a/royalnet/version.py +++ b/royalnet/version.py @@ -1 +1 @@ -semantic = "5.3.4" +semantic = "5.4"