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

View file

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

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 .table_dfs import table_dfs
@ -8,5 +21,5 @@ __all__ = [
"Alchemy",
"table_dfs",
"AlchemyException",
"TableNotFoundError"
"TableNotFoundError",
]

View file

@ -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")

View file

@ -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):

View file

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

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
from .ytdlfile import YtdlFile
from .ytdlmp3 import YtdlMp3
from .ytdldiscord import YtdlDiscord
"""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:
from .fileaudiosource import FileAudioSource
import ffmpeg
import youtube_dl
import eyed3
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__ = [
"YtdlInfo",
"YtdlFile",
"YtdlMp3",
"YtdlDiscord",
"FileAudioSource",
"BardError",
"YtdlError",
"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
except ImportError:
discord = None
import discord
class FileAudioSource(discord.AudioSource):

View file

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

View file

@ -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:

View file

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

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 .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__ = [

View file

@ -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:

View file

@ -1,5 +1,5 @@
import re
from typing import Pattern, AnyStr, Optional, Sequence, Union
import typing
from .errors import InvalidInputError
@ -66,7 +66,7 @@ class CommandArgs(list):
raise InvalidInputError(f"Not enough arguments specified (minimum is {require_at_least}).")
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.
Parameters:
@ -83,7 +83,7 @@ class CommandArgs(list):
raise InvalidInputError("Invalid syntax.")
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
``default`` value.

View file

@ -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)

View file

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

View file

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

View file

@ -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`
It optionally uses the `sentry` extra.
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 .star import Star, PageStar, ExceptionStar

View file

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

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

View file

@ -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__

View file

@ -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():

View file

@ -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:
@ -30,7 +30,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}>"

View file

@ -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__

View file

@ -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})"

View file

@ -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()

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 .errors import SerfError
from . import telegram, discord, matrix
__all__ = [
"Serf",
"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.
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

View file

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

View file

@ -2,30 +2,14 @@ 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
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__)
@ -56,7 +40,7 @@ class Serf:
try:
packs[pack_name] = importlib.import_module(pack_name)
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")
self.alchemy: Optional[ra.Alchemy] = None
@ -317,8 +301,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 +320,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())

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,3 +1,13 @@
"""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

View file

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

View file

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