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:
commit
026f1e5b6e
46 changed files with 311 additions and 318 deletions
|
@ -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__ = []
|
||||
|
|
|
@ -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"],
|
||||
|
|
15
royalnet/alchemy/README.md
Normal file
15
royalnet/alchemy/README.md
Normal 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]
|
||||
```
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
10
royalnet/bard/README.md
Normal 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]
|
||||
```
|
|
@ -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",
|
||||
]
|
||||
|
|
10
royalnet/bard/discord/README.md
Normal file
10
royalnet/bard/discord/README.md
Normal 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]
|
||||
```
|
17
royalnet/bard/discord/__init__.py
Normal file
17
royalnet/bard/discord/__init__.py
Normal 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",
|
||||
]
|
|
@ -1,7 +1,4 @@
|
|||
try:
|
||||
import discord
|
||||
except ImportError:
|
||||
discord = None
|
||||
import discord
|
||||
|
||||
|
||||
class FileAudioSource(discord.AudioSource):
|
|
@ -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,
|
|
@ -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:
|
||||
|
|
|
@ -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 []
|
||||
|
|
|
@ -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
|
3
royalnet/commands/README.md
Normal file
3
royalnet/commands/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# `royalnet.commands`
|
||||
|
||||
The subpackage providing all classes related to Royalnet commands.
|
|
@ -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__ = [
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
```
|
|
@ -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
|
||||
|
|
|
@ -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
12
royalnet/herald/README.md
Normal 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]
|
||||
```
|
|
@ -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
|
||||
|
|
|
@ -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__
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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}>"
|
||||
|
|
|
@ -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__
|
||||
|
|
|
@ -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})"
|
||||
|
|
|
@ -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
3
royalnet/serf/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# `royalnet.serf`
|
||||
|
||||
The subpackage providing all Serf implementations.
|
|
@ -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",
|
||||
]
|
||||
|
|
10
royalnet/serf/discord/README.md
Normal file
10
royalnet/serf/discord/README.md
Normal 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]
|
||||
```
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
10
royalnet/serf/matrix/README.md
Normal file
10
royalnet/serf/matrix/README.md
Normal 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]
|
||||
```
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
10
royalnet/serf/telegram/README.md
Normal file
10
royalnet/serf/telegram/README.md
Normal 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]
|
||||
```
|
|
@ -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"
|
||||
]
|
||||
]
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1 +1 @@
|
|||
semantic = "5.3.4"
|
||||
semantic = "5.4"
|
||||
|
|
Loading…
Reference in a new issue