1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-23 19:44:20 +00:00

Merge branch '5.4-optional' into 5.4

# Conflicts:
#	royalnet/commands/commandinterface.py
This commit is contained in:
Steffo 2020-01-31 18:55:50 +01:00
commit 026f1e5b6e
46 changed files with 311 additions and 318 deletions

View file

@ -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__ = [ __all__ = []
"alchemy",
"bard",
"commands",
"constellation",
"herald",
"serf",
"utils",
"backpack",
]

View file

@ -1,11 +1,13 @@
import click import click
import multiprocessing 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 toml
import logging 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: try:
import coloredlogs import coloredlogs
@ -60,7 +62,7 @@ def run(config_filename: str):
telegram_process = None telegram_process = None
if "Telegram" in config["Serfs"] and config["Serfs"]["Telegram"]["enabled"]: if "Telegram" in config["Serfs"] and config["Serfs"]["Telegram"]["enabled"]:
telegram_process = multiprocessing.Process(name="Serf.Telegram", telegram_process = multiprocessing.Process(name="Serf.Telegram",
target=rs.telegram.TelegramSerf.run_process, target=rst.TelegramSerf.run_process,
daemon=True, daemon=True,
kwargs={ kwargs={
"alchemy_cfg": config["Alchemy"], "alchemy_cfg": config["Alchemy"],
@ -78,7 +80,7 @@ def run(config_filename: str):
discord_process = None discord_process = None
if "Discord" in config["Serfs"] and config["Serfs"]["Discord"]["enabled"]: if "Discord" in config["Serfs"] and config["Serfs"]["Discord"]["enabled"]:
discord_process = multiprocessing.Process(name="Serf.Discord", discord_process = multiprocessing.Process(name="Serf.Discord",
target=rs.discord.DiscordSerf.run_process, target=rsd.DiscordSerf.run_process,
daemon=True, daemon=True,
kwargs={ kwargs={
"alchemy_cfg": config["Alchemy"], "alchemy_cfg": config["Alchemy"],
@ -96,7 +98,7 @@ def run(config_filename: str):
matrix_process = None matrix_process = None
if "Matrix" in config["Serfs"] and config["Serfs"]["Matrix"]["enabled"]: if "Matrix" in config["Serfs"] and config["Serfs"]["Matrix"]["enabled"]:
matrix_process = multiprocessing.Process(name="Serf.Matrix", matrix_process = multiprocessing.Process(name="Serf.Matrix",
target=rs.matrix.MatrixSerf.run_process, target=rsm.MatrixSerf.run_process,
daemon=True, daemon=True,
kwargs={ kwargs={
"alchemy_cfg": config["Alchemy"], "alchemy_cfg": config["Alchemy"],

View file

@ -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]
```

View file

@ -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 <https://pypi.org/project/psycopg2/>}`_ installation instructions,
then run: ::
pip install royalnet[alchemy_hard]
"""
from .alchemy import Alchemy from .alchemy import Alchemy
from .table_dfs import table_dfs from .table_dfs import table_dfs
@ -8,5 +21,5 @@ __all__ = [
"Alchemy", "Alchemy",
"table_dfs", "table_dfs",
"AlchemyException", "AlchemyException",
"TableNotFoundError" "TableNotFoundError",
] ]

View file

@ -2,21 +2,12 @@ from typing import Set, Dict, Union
from contextlib import contextmanager, asynccontextmanager from contextlib import contextmanager, asynccontextmanager
from royalnet.utils import asyncify from royalnet.utils import asyncify
from royalnet.alchemy.errors import TableNotFoundError from royalnet.alchemy.errors import TableNotFoundError
from sqlalchemy import create_engine
try: from sqlalchemy.engine import Engine
from sqlalchemy import create_engine from sqlalchemy.schema import Table
from sqlalchemy.engine import Engine from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.schema import Table from sqlalchemy.ext.declarative.api import DeclarativeMeta
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker
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
class Alchemy: class Alchemy:
@ -31,9 +22,6 @@ class Alchemy:
tables: The :class:`set` of tables to be created and used in the selected database. tables: The :class:`set` of tables to be created and used in the selected database.
Check the tables submodule for more details. Check the tables submodule for more details.
""" """
if create_engine is None:
raise ImportError("'alchemy' extra is not installed")
if database_uri.startswith("sqlite"): if database_uri.startswith("sqlite"):
raise NotImplementedError("sqlite databases aren't supported, as they can't be used in multithreaded" raise NotImplementedError("sqlite databases aren't supported, as they can't be used in multithreaded"
" applications") " applications")

View file

@ -1,9 +1,5 @@
try: from sqlalchemy.inspection import inspect
from sqlalchemy.inspection import inspect from sqlalchemy.schema import Table
from sqlalchemy.schema import Table
except ImportError:
inspect = None
Table = None
def table_dfs(starting_table: Table, ending_table: Table) -> tuple: 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: Returns:
A :class:`tuple` containing the path, starting from the starting table and ending at the ending table.""" 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() inspected = set()
def search(_mapper, chain): def search(_mapper, chain):

View file

@ -1,6 +1,4 @@
"""A Pack that is imported by default by all Royalnet instances. """A Pack that is imported by default by all Royalnet instances."""
Keep things here to a minimum!"""
from . import commands, tables, stars, events from . import commands, tables, stars, events
from .commands import available_commands from .commands import available_commands

10
royalnet/bard/README.md Normal file
View file

@ -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]
```

View file

@ -1,18 +1,29 @@
from .ytdlinfo import YtdlInfo """The subpackage providing all classes related to music files.
from .ytdlfile import YtdlFile
from .ytdlmp3 import YtdlMp3 It requires the ``bard`` extra to be installed (the :mod:`ffmpeg_python`, :mod:`youtube_dl` and :mod:`eyed3` packages).
from .ytdldiscord import YtdlDiscord
You can install it with: ::
pip install royalnet[bard]
"""
try: try:
from .fileaudiosource import FileAudioSource import ffmpeg
import youtube_dl
import eyed3
except ImportError: except ImportError:
FileAudioSource = None 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 .errors import BardError, YtdlError, NotFoundError, MultipleFilesError
__all__ = [ __all__ = [
"YtdlInfo", "YtdlInfo",
"YtdlFile", "YtdlFile",
"YtdlMp3", "BardError",
"YtdlDiscord", "YtdlError",
"FileAudioSource", "NotFoundError",
"MultipleFilesError",
] ]

View file

@ -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]
```

View file

@ -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",
]

View file

@ -1,7 +1,4 @@
try: import discord
import discord
except ImportError:
discord = None
class FileAudioSource(discord.AudioSource): class FileAudioSource(discord.AudioSource):

View file

@ -2,19 +2,12 @@ import typing
import re import re
import os import os
import logging import logging
import ffmpeg
import discord
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from royalnet.utils import asyncify, MultiLock, FileAudioSource from royalnet.utils import asyncify, MultiLock
from royalnet.bard import YtdlInfo, YtdlFile from royalnet.bard import YtdlInfo, YtdlFile
from .fileaudiosource import FileAudioSource
try:
import ffmpeg
except ImportError:
ffmpeg = None
try:
import discord
except ImportError:
discord = None
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -44,8 +37,6 @@ class YtdlDiscord:
async def convert_to_pcm(self) -> None: async def convert_to_pcm(self) -> None:
"""Convert the file to pcm with :mod:`ffmpeg`.""" """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() await self.ytdl_file.download_file()
if self.pcm_filename is None: if self.pcm_filename is None:
async with self.ytdl_file.lock.normal(): async with self.ytdl_file.lock.normal():
@ -87,8 +78,6 @@ class YtdlDiscord:
@asynccontextmanager @asynccontextmanager
async def spawn_audiosource(self): async def spawn_audiosource(self):
log.debug(f"Spawning audio_source for: {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() await self.convert_to_pcm()
async with self.lock.normal(): async with self.lock.normal():
with open(self.pcm_filename, "rb") as stream: with open(self.pcm_filename, "rb") as stream:
@ -97,8 +86,6 @@ class YtdlDiscord:
def embed(self) -> "discord.Embed": def embed(self) -> "discord.Embed":
"""Return this info as a :py:class:`discord.Embed`.""" """Return this info as a :py:class:`discord.Embed`."""
if discord is None:
raise ImportError("'discord' extra is not installed")
colors = { colors = {
"youtube": 0xCC0000, "youtube": 0xCC0000,
"soundcloud": 0xFF5400, "soundcloud": 0xFF5400,

View file

@ -8,11 +8,7 @@ from royalnet.utils import *
from asyncio import AbstractEventLoop, get_event_loop from asyncio import AbstractEventLoop, get_event_loop
from .ytdlinfo import YtdlInfo from .ytdlinfo import YtdlInfo
from .errors import NotFoundError, MultipleFilesError from .errors import NotFoundError, MultipleFilesError
from youtube_dl import YoutubeDL
try:
from youtube_dl import YoutubeDL
except ImportError:
YoutubeDL = None
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -78,9 +74,6 @@ class YtdlFile:
async def download_file(self) -> None: async def download_file(self) -> None:
"""Download the file.""" """Download the file."""
if YoutubeDL is None:
raise ImportError("'bard' extra is not installed")
def download(): def download():
"""Download function block to be asyncified.""" """Download function block to be asyncified."""
with YoutubeDL(self.ytdl_args) as ytdl: with YoutubeDL(self.ytdl_args) as ytdl:

View file

@ -1,14 +1,10 @@
from asyncio import AbstractEventLoop, get_event_loop from typing import *
from typing import Optional, Dict, List, Any import asyncio as aio
from datetime import datetime, timedelta import datetime
import dateparser import dateparser
import logging import logging
from royalnet.utils import ytdldateformat, asyncify import royalnet.utils as ru
import youtube_dl
try:
from youtube_dl import YoutubeDL
except ImportError:
YoutubeDL = None
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -36,7 +32,7 @@ class YtdlInfo:
self.uploader_url: Optional[str] = info.get("uploader_url") self.uploader_url: Optional[str] = info.get("uploader_url")
self.channel_id: Optional[str] = info.get("channel_id") self.channel_id: Optional[str] = info.get("channel_id")
self.channel_url: Optional[str] = info.get("channel_url") 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.license: Optional[str] = info.get("license")
self.creator: Optional[...] = info.get("creator") self.creator: Optional[...] = info.get("creator")
self.title: Optional[str] = info.get("title") self.title: Optional[str] = info.get("title")
@ -47,7 +43,7 @@ class YtdlInfo:
self.tags: Optional[List[str]] = info.get("tags") self.tags: Optional[List[str]] = info.get("tags")
self.subtitles: Optional[Dict[str, List[Dict[str, str]]]] = info.get("subtitles") self.subtitles: Optional[Dict[str, List[Dict[str, str]]]] = info.get("subtitles")
self.automatic_captions: Optional[dict] = info.get("automatic_captions") 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.age_limit: Optional[int] = info.get("age_limit")
self.annotations: Optional[...] = info.get("annotations") self.annotations: Optional[...] = info.get("annotations")
self.chapters: Optional[...] = info.get("chapters") self.chapters: Optional[...] = info.get("chapters")
@ -90,20 +86,17 @@ class YtdlInfo:
self.album: Optional[str] = None self.album: Optional[str] = None
@classmethod @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`. """Fetch the info for an url through :class:`YoutubeDL`.
Returns: Returns:
A :class:`list` containing the infos for the requested videos.""" 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: if loop is None:
loop: AbstractEventLoop = get_event_loop() loop: aio.AbstractEventLoop = aio.get_event_loop()
# So many redundant options! # So many redundant options!
log.debug(f"Fetching info: {url}") log.debug(f"Fetching info: {url}")
with YoutubeDL({**cls._default_ytdl_args, **ytdl_args}) as ytdl: with youtube_dl.YoutubeDL({**cls._default_ytdl_args, **ytdl_args}) as ytdl:
first_info = await asyncify(ytdl.extract_info, loop=loop, url=url, download=False) first_info = await ru.asyncify(ytdl.extract_info, loop=loop, url=url, download=False)
# No video was found # No video was found
if first_info is None: if first_info is None:
return [] return []

View file

@ -1,73 +0,0 @@
import typing
import re
import os
from royalnet.utils import asyncify, MultiLock
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()
def __repr__(self):
if not self.ytdl_file.has_info:
return f"<{self.__class__.__qualname__} without info>"
elif not self.ytdl_file.is_downloaded:
return f"<{self.__class__.__qualname__} not downloaded>"
elif not self.is_converted:
return f"<{self.__class__.__qualname__} at '{self.ytdl_file.filename}' not converted>"
else:
return f"<{self.__class__.__qualname__} at '{self.mp3_filename}'>"
@property
def is_converted(self):
"""Has the file been converted?"""
return self.mp3_filename is not None
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(
ffmpeg.input(self.ytdl_file.filename)
.output(destination_filename, format="mp3")
.overwrite_output()
.run
)
self.mp3_filename = destination_filename
async def delete_asap(self) -> None:
"""Delete the mp3 file."""
if self.is_converted:
async with self.lock.exclusive():
os.remove(self.mp3_filename)
self.mp3_filename = None
@classmethod
async def from_url(cls, url, **ytdl_args) -> typing.List["YtdlMp3"]:
"""Create a :class:`list` of :class:`YtdlMp3` from a URL."""
files = await YtdlFile.from_url(url, **ytdl_args)
dfiles = []
for file in files:
dfile = YtdlMp3(file)
dfiles.append(dfile)
return dfiles
@property
def info(self) -> typing.Optional[YtdlInfo]:
"""Shortcut to get the :class:`YtdlInfo` of the object."""
return self.ytdl_file.info

View file

@ -0,0 +1,3 @@
# `royalnet.commands`
The subpackage providing all classes related to Royalnet commands.

View file

@ -1,15 +1,12 @@
"""The subpackage providing all classes related to Royalnet commands."""
from .commandinterface import CommandInterface from .commandinterface import CommandInterface
from .command import Command from .command import Command
from .commanddata import CommandData from .commanddata import CommandData
from .commandargs import CommandArgs from .commandargs import CommandArgs
from .event import Event from .event import Event
from .errors import CommandError, \ from .errors import \
InvalidInputError, \ CommandError, InvalidInputError, UnsupportedError, ConfigurationError, ExternalError, UserError, ProgramError
UnsupportedError, \
ConfigurationError, \
ExternalError, \
UserError, \
ProgramError
from .keyboardkey import KeyboardKey from .keyboardkey import KeyboardKey
__all__ = [ __all__ = [

View file

@ -1,4 +1,4 @@
import typing from typing import *
from .commandinterface import CommandInterface from .commandinterface import CommandInterface
from .commandargs import CommandArgs from .commandargs import CommandArgs
from .commanddata import CommandData from .commanddata import CommandData
@ -11,7 +11,7 @@ class Command:
Example: Example:
To be able to call ``/example`` on Telegram, the name should be ``"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. """A list of possible aliases for a command.
Example: Example:

View file

@ -1,5 +1,5 @@
import re import re
from typing import Pattern, AnyStr, Optional, Sequence, Union import typing
from .errors import InvalidInputError from .errors import InvalidInputError
@ -66,7 +66,7 @@ class CommandArgs(list):
raise InvalidInputError(f"Not enough arguments specified (minimum is {require_at_least}).") raise InvalidInputError(f"Not enough arguments specified (minimum is {require_at_least}).")
return " ".join(self) return " ".join(self)
def match(self, pattern: Union[str, Pattern], *flags) -> Sequence[AnyStr]: def match(self, pattern: typing.Union[str, typing.Pattern], *flags) -> typing.Sequence[typing.AnyStr]:
"""Match the :meth:`.joined` string to a :class:`re.Pattern`-like object. """Match the :meth:`.joined` string to a :class:`re.Pattern`-like object.
Parameters: Parameters:
@ -83,7 +83,7 @@ class CommandArgs(list):
raise InvalidInputError("Invalid syntax.") raise InvalidInputError("Invalid syntax.")
return match.groups() return match.groups()
def optional(self, index: int, default=None) -> Optional[str]: def optional(self, index: int, default=None) -> typing.Optional[str]:
"""Get the argument at a specific index, but don't raise an error if nothing is found, instead returning the """Get the argument at a specific index, but don't raise an error if nothing is found, instead returning the
``default`` value. ``default`` value.

View file

@ -1,11 +1,11 @@
from typing import *
import contextlib import contextlib
import logging import logging
import asyncio as aio import asyncio as aio
from typing import * import royalnet.utils as ru
from sqlalchemy.orm.session import Session
from .errors import UnsupportedError from .errors import UnsupportedError
from .commandinterface import CommandInterface from .commandinterface import CommandInterface
import royalnet.utils as ru
if TYPE_CHECKING: if TYPE_CHECKING:
from .keyboardkey import KeyboardKey from .keyboardkey import KeyboardKey
@ -14,26 +14,28 @@ log = logging.getLogger(__name__)
class CommandData: class CommandData:
def __init__(self, interface: CommandInterface, loop: aio.AbstractEventLoop): def __init__(self, interface: CommandInterface, loop: aio.AbstractEventLoop):
self._interface: CommandInterface = interface
self.loop: aio.AbstractEventLoop = loop self.loop: aio.AbstractEventLoop = loop
self._interface: CommandInterface = interface
self._session = None self._session = None
# TODO: make this asyncronous... somehow?
@property @property
def session(self): def session(self):
if self._session is None: if self._session is None:
if self._interface.alchemy is None: if self._interface.alchemy is None:
raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance") raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance")
# FIXME: this may take a while
self._session = self._interface.alchemy.Session() self._session = self._interface.alchemy.Session()
return self._session return self._session
async def session_commit(self): async def session_commit(self):
"""Asyncronously commit the :attr:`.session` of this object."""
if self._session: if self._session:
log.warning("Session had to be created to be committed") log.warning("Session had to be created to be committed")
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
await ru.asyncify(self.session.commit) await ru.asyncify(self.session.commit)
async def session_close(self): async def session_close(self):
"""Asyncronously close the :attr:`.session` of this object."""
if self._session is not None: if self._session is not None:
await ru.asyncify(self._session.close) await ru.asyncify(self._session.close)

View file

@ -1,6 +1,7 @@
from typing import * from typing import *
import asyncio as aio import asyncio as aio
from .errors import * from .errors import UnsupportedError
if TYPE_CHECKING: if TYPE_CHECKING:
from .event import Event from .event import Event
from .command import Command from .command import Command

View file

@ -1,5 +1,7 @@
import asyncio as aio
from .commandinterface import CommandInterface from .commandinterface import CommandInterface
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from serf import Serf from serf import Serf
@ -16,7 +18,7 @@ class Event:
"""The :class:`CommandInterface` available to this :class:`Event`.""" """The :class:`CommandInterface` available to this :class:`Event`."""
@property @property
def serf(self): def serf(self) -> "Serf":
"""A shortcut for :attr:`.interface.serf`.""" """A shortcut for :attr:`.interface.serf`."""
return self.interface.serf return self.interface.serf
@ -26,12 +28,12 @@ class Event:
return self.interface.alchemy return self.interface.alchemy
@property @property
def loop(self): def loop(self) -> aio.AbstractEventLoop:
"""A shortcut for :attr:`.interface.loop`.""" """A shortcut for :attr:`.interface.loop`."""
return self.interface.loop return self.interface.loop
@property @property
def config(self): def config(self) -> dict:
"""A shortcut for :attr:`.interface.config`.""" """A shortcut for :attr:`.interface.config`."""
return self.interface.config return self.interface.config

View file

@ -1,11 +1,17 @@
# `royalnet.constellation` # `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` It optionally uses the `sentry` extra.
- `star`
- `shoot` You can install it with:
```
pip install royalnet[constellation,sentry]
```

View file

@ -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 .constellation import Constellation
from .star import Star, PageStar, ExceptionStar from .star import Star, PageStar, ExceptionStar

View file

@ -1,7 +1,9 @@
from typing import *
import asyncio as aio
import logging import logging
import importlib import importlib
import asyncio as aio import uvicorn
from typing import * import starlette.applications
import royalnet.alchemy as ra import royalnet.alchemy as ra
import royalnet.herald as rh import royalnet.herald as rh
import royalnet.utils as ru import royalnet.utils as ru
@ -9,26 +11,6 @@ import royalnet.commands as rc
from .star import PageStar, ExceptionStar from .star import PageStar, ExceptionStar
from ..utils import init_logging 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__) log = logging.getLogger(__name__)
@ -54,9 +36,6 @@ class Constellation:
constellation_cfg: Dict[str, Any], constellation_cfg: Dict[str, Any],
logging_cfg: Dict[str, Any] logging_cfg: Dict[str, Any]
): ):
if Starlette is None:
raise ImportError("`constellation` extra is not installed")
# Import packs # Import packs
pack_names = packs_cfg["active"] pack_names = packs_cfg["active"]
packs = {} packs = {}
@ -112,7 +91,7 @@ class Constellation:
self.events: Dict[str, rc.Event] = {} self.events: Dict[str, rc.Event] = {}
"""A dictionary containing all :class:`~rc.Event` that can be handled by this :class:`Constellation`.""" """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.""" """The :class:`~starlette.Starlette` app."""
# Register Events # Register Events

12
royalnet/herald/README.md Normal file
View file

@ -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]
```

View file

@ -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 .config import Config
from .errors import HeraldError, ConnectionClosedError, LinkError, InvalidServerResponseError, ServerError from .errors import *
from .link import Link from .link import Link
from .package import Package from .package import Package
from .request import Request from .request import Request

View file

@ -1,8 +1,8 @@
import typing from typing import *
class Broadcast: 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__() super().__init__()
if msg_type is not None: if msg_type is not None:
assert msg_type == self.__class__.__name__ assert msg_type == self.__class__.__name__

View file

@ -1,8 +1,9 @@
import asyncio from typing import *
import asyncio as aio
import uuid import uuid
import functools import functools
import logging as _logging import logging
import typing import websockets
from .package import Package from .package import Package
from .request import Request from .request import Request
from .response import Response, ResponseSuccess, ResponseFailure from .response import Response, ResponseSuccess, ResponseFailure
@ -10,23 +11,18 @@ from .broadcast import Broadcast
from .errors import ConnectionClosedError, InvalidServerResponseError from .errors import ConnectionClosedError, InvalidServerResponseError
from .config import Config from .config import Config
try:
import websockets
except ImportError:
websockets = None
log = logging.getLogger(__name__)
log = _logging.getLogger(__name__)
class PendingRequest: class PendingRequest:
def __init__(self, *, loop: asyncio.AbstractEventLoop = None): def __init__(self, *, loop: aio.AbstractEventLoop = None):
if loop is None: if loop is None:
self.loop = asyncio.get_event_loop() self.loop = aio.get_event_loop()
else: else:
self.loop = loop self.loop = loop
self.event: asyncio.Event = asyncio.Event(loop=loop) self.event: aio.Event = aio.Event(loop=loop)
self.data: typing.Optional[dict] = None self.data: Optional[dict] = None
def __repr__(self): def __repr__(self):
if self.event.is_set(): if self.event.is_set():
@ -56,22 +52,20 @@ def requires_identification(func):
class Link: class Link:
def __init__(self, config: Config, request_handler, *, def __init__(self, config: Config, request_handler, *,
loop: asyncio.AbstractEventLoop = None): loop: aio.AbstractEventLoop = None):
if websockets is None:
raise ImportError("'websockets' extra is not installed")
self.config: Config = config self.config: Config = config
self.nid: str = str(uuid.uuid4()) self.nid: str = str(uuid.uuid4())
self.websocket: typing.Optional["websockets.WebSocketClientProtocol"] = None self.websocket: Optional["websockets.WebSocketClientProtocol"] = None
self.request_handler: typing.Callable[[typing.Union[Request, Broadcast]], self.request_handler: Callable[[Union[Request, Broadcast]],
typing.Awaitable[Response]] = request_handler Awaitable[Response]] = request_handler
self._pending_requests: typing.Dict[str, PendingRequest] = {} self._pending_requests: Dict[str, PendingRequest] = {}
if loop is None: if loop is None:
self._loop = asyncio.get_event_loop() self._loop = aio.get_event_loop()
else: else:
self._loop = loop self._loop = loop
self.error_event: asyncio.Event = asyncio.Event(loop=self._loop) self.error_event: aio.Event = aio.Event(loop=self._loop)
self.connect_event: asyncio.Event = asyncio.Event(loop=self._loop) self.connect_event: aio.Event = aio.Event(loop=self._loop)
self.identify_event: asyncio.Event = asyncio.Event(loop=self._loop) self.identify_event: aio.Event = aio.Event(loop=self._loop)
def __repr__(self): def __repr__(self):
if self.identify_event.is_set(): if self.identify_event.is_set():

View file

@ -1,6 +1,6 @@
import json import json
import uuid import uuid
import typing from typing import *
class Package: class Package:
@ -14,8 +14,8 @@ class Package:
*, *,
source: str, source: str,
destination: str, destination: str,
source_conv_id: typing.Optional[str] = None, source_conv_id: Optional[str] = None,
destination_conv_id: typing.Optional[str] = None): destination_conv_id: Optional[str] = None):
"""Create a Package. """Create a Package.
Parameters: Parameters:
@ -30,7 +30,7 @@ class Package:
self.source: str = source self.source: str = source
self.source_conv_id: str = source_conv_id or str(uuid.uuid4()) self.source_conv_id: str = source_conv_id or str(uuid.uuid4())
self.destination: str = destination 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): def __repr__(self):
return f"<{self.__class__.__qualname__} {self.source} » {self.destination}>" return f"<{self.__class__.__qualname__} {self.source} » {self.destination}>"

View file

@ -1,4 +1,4 @@
import typing from typing import *
class Request: class Request:
@ -6,7 +6,7 @@ class Request:
It contains the name of the requested handler, in addition to the data.""" 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__() super().__init__()
if msg_type is not None: if msg_type is not None:
assert msg_type == self.__class__.__name__ assert msg_type == self.__class__.__name__

View file

@ -1,4 +1,4 @@
import typing from typing import *
class Response: class Response:
@ -28,7 +28,7 @@ class Response:
class ResponseSuccess(Response): class ResponseSuccess(Response):
"""A response to a successful :py:class:`Request`.""" """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: if data is None:
self.data = {} self.data = {}
else: else:
@ -41,10 +41,10 @@ class ResponseSuccess(Response):
class ResponseFailure(Response): class ResponseFailure(Response):
"""A response to a invalid :py:class:`Request`.""" """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.name: str = name
self.description: str = description self.description: str = description
self.extra_info: typing.Optional[dict] = extra_info self.extra_info: Optional[dict] = extra_info
def __repr__(self): def __repr__(self):
return f"{self.__class__.__qualname__}(name={self.name}, description={self.description}, extra_info={self.extra_info})" return f"{self.__class__.__qualname__}(name={self.name}, description={self.description}, extra_info={self.extra_info})"

View file

@ -1,25 +1,16 @@
from typing import * from typing import *
import asyncio as aio
import re import re
import datetime import datetime
import uuid import uuid
import asyncio import logging
import logging as _logging import websockets
import royalnet.utils as ru import royalnet.utils as ru
from .package import Package from .package import Package
from .config import Config from .config import Config
try:
import coloredlogs
except ImportError:
coloredlogs = None
try: log = logging.getLogger(__name__)
import websockets
except ImportError:
websockets = None
log = _logging.getLogger(__name__)
class ConnectedClient: class ConnectedClient:
@ -49,7 +40,7 @@ class ConnectedClient:
class Server: 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.config: Config = config
self.identified_clients: List[ConnectedClient] = [] self.identified_clients: List[ConnectedClient] = []
self.loop = loop self.loop = loop
@ -165,5 +156,5 @@ class Server:
def run_blocking(self, logging_cfg: Dict[str, Any]): def run_blocking(self, logging_cfg: Dict[str, Any]):
ru.init_logging(logging_cfg) ru.init_logging(logging_cfg)
if self.loop is None: if self.loop is None:
self.loop = asyncio.get_event_loop() self.loop = aio.get_event_loop()
self.serve() self.serve()

3
royalnet/serf/README.md Normal file
View file

@ -0,0 +1,3 @@
# `royalnet.serf`
The subpackage providing all Serf implementations.

View file

@ -1,11 +1,9 @@
"""The subpackage providing all Serf implementations."""
from .serf import Serf from .serf import Serf
from .errors import SerfError from .errors import SerfError
from . import telegram, discord, matrix
__all__ = [ __all__ = [
"Serf", "Serf",
"SerfError", "SerfError",
"telegram",
"discord",
"matrix",
] ]

View file

@ -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]
```

View file

@ -1,6 +1,12 @@
"""A :class:`Serf` implementation for Discord. """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 .escape import escape
from .discordserf import DiscordSerf from .discordserf import DiscordSerf

View file

@ -87,6 +87,7 @@ class VoicePlayer:
raise PlayerNotConnectedError() raise PlayerNotConnectedError()
if self.voice_client.is_playing(): if self.voice_client.is_playing():
raise PlayerAlreadyPlaying() raise PlayerAlreadyPlaying()
self.playing = None
log.debug("Getting next AudioSource...") log.debug("Getting next AudioSource...")
next_source: Optional["discord.AudioSource"] = await self.playing.next() next_source: Optional["discord.AudioSource"] = await self.playing.next()
if next_source is None: if next_source is None:
@ -97,7 +98,6 @@ class VoicePlayer:
self.loop.create_task(self._playback_check()) self.loop.create_task(self._playback_check())
async def _playback_check(self): async def _playback_check(self):
# FIXME: quite spaghetti
while True: while True:
if self._playback_ended_event.is_set(): if self._playback_ended_event.is_set():
self._playback_ended_event.clear() self._playback_ended_event.clear()

View file

@ -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]
```

View file

@ -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 .matrixserf import MatrixSerf
from .escape import escape from .escape import escape

View file

@ -2,30 +2,14 @@ import logging
import importlib import importlib
import asyncio as aio import asyncio as aio
from typing import * from typing import *
from sqlalchemy.schema import Table from sqlalchemy.schema import Table
from royalnet.commands import * from royalnet.commands import *
import royalnet.utils as ru import royalnet.utils as ru
import royalnet.alchemy as ra import royalnet.alchemy as ra
import royalnet.backpack as rb import royalnet.backpack as rb
import royalnet.herald as rh import royalnet.herald as rh
import traceback
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__) log = logging.getLogger(__name__)
@ -56,7 +40,7 @@ class Serf:
try: try:
packs[pack_name] = importlib.import_module(pack_name) packs[pack_name] = importlib.import_module(pack_name)
except ImportError as e: except ImportError as e:
log.error(f"Error during the import of {pack_name}: {e}") log.error(f"{e.__class__.__name__} during the import of {pack_name}: {e}")
log.info(f"Packs: {len(packs)} imported") log.info(f"Packs: {len(packs)} imported")
self.alchemy: Optional[ra.Alchemy] = None self.alchemy: Optional[ra.Alchemy] = None
@ -317,8 +301,7 @@ class Serf:
try: try:
await key.press(data) await key.press(data)
except InvalidInputError as e: except InvalidInputError as e:
await data.reply(f"⚠️ {e.message}\n" await data.reply(f"⚠️ {e.message}")
f"Syntax: [c]{command.interface.prefix}{command.name} {command.syntax}[/c]")
except UserError as e: except UserError as e:
await data.reply(f"⚠️ {e.message}") await data.reply(f"⚠️ {e.message}")
except UnsupportedError as e: except UnsupportedError as e:
@ -337,7 +320,6 @@ class Serf:
finally: finally:
await data.session_close() await data.session_close()
async def run(self): async def run(self):
"""A coroutine that starts the event loop and handles command calls.""" """A coroutine that starts the event loop and handles command calls."""
self.herald_task = self.loop.create_task(self.herald.run()) self.herald_task = self.loop.create_task(self.herald.run())

View file

@ -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]
```

View file

@ -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 .escape import escape
from .telegramserf import TelegramSerf from .telegramserf import TelegramSerf
__all__ = [ __all__ = [
"escape", "escape",
"TelegramSerf" "TelegramSerf"
] ]

View file

@ -3,7 +3,6 @@ from .sleep_until import sleep_until
from .formatters import andformat, underscorize, ytdldateformat, numberemojiformat, ordinalformat from .formatters import andformat, underscorize, ytdldateformat, numberemojiformat, ordinalformat
from .urluuid import to_urluuid, from_urluuid from .urluuid import to_urluuid, from_urluuid
from .multilock import MultiLock from .multilock import MultiLock
from .fileaudiosource import FileAudioSource
from .sentry import init_sentry, sentry_exc from .sentry import init_sentry, sentry_exc
from .log import init_logging from .log import init_logging
@ -18,7 +17,6 @@ __all__ = [
"to_urluuid", "to_urluuid",
"from_urluuid", "from_urluuid",
"MultiLock", "MultiLock",
"FileAudioSource",
"init_sentry", "init_sentry",
"sentry_exc", "sentry_exc",
"init_logging", "init_logging",

View file

@ -1 +1 @@
semantic = "5.3.4" semantic = "5.4"