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

Make the bot runnable

This commit is contained in:
Steffo 2019-11-15 20:47:32 +01:00
parent ef645ab143
commit 192b4cf3d6
29 changed files with 247 additions and 315 deletions

View file

@ -3,6 +3,7 @@
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/royalnet" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/royalnet" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/royalnet.egg-info" />
<excludeFolder url="file://$MODULE_DIR$/venv" /> <excludeFolder url="file://$MODULE_DIR$/venv" />
</content> </content>
<orderEntry type="jdk" jdkName="Python 3.8 (royalnet-1MWM6-kd-py3.8)" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Python 3.8 (royalnet-1MWM6-kd-py3.8)" jdkType="Python SDK" />

39
poetry.lock generated
View file

@ -82,7 +82,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "7.0" version = "7.0"
[[package]] [[package]]
category = "dev" category = "main"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
marker = "sys_platform == \"win32\"" marker = "sys_platform == \"win32\""
name = "colorama" name = "colorama"
@ -90,6 +90,17 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.4.1" version = "0.4.1"
[[package]]
category = "main"
description = "Log formatting with colors!"
name = "colorlog"
optional = true
python-versions = "*"
version = "4.0.2"
[package.dependencies]
colorama = "*"
[[package]] [[package]]
category = "main" category = "main"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
@ -142,7 +153,7 @@ voice = ["PyNaCl (1.3.0)"]
[package.source] [package.source]
reference = "09a08f9a9f126aa1f55c2444eb70508d1d52f8d9" reference = "09a08f9a9f126aa1f55c2444eb70508d1d52f8d9"
type = "git" type = "git"
url = "https://github.com/Steffo99/discord.py.git" url = "https://github.com/Steffo99/discord.py"
[[package]] [[package]]
category = "main" category = "main"
description = "Discover and load entry points from installed packages." description = "Discover and load entry points from installed packages."
@ -328,7 +339,7 @@ description = "pytest: simple powerful testing with Python"
name = "pytest" name = "pytest"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "5.2.2" version = "5.2.3"
[package.dependencies] [package.dependencies]
atomicwrites = ">=1.0" atomicwrites = ">=1.0"
@ -562,6 +573,7 @@ version = "2019.11.5"
alchemy_easy = ["sqlalchemy", "psycopg2_binary"] alchemy_easy = ["sqlalchemy", "psycopg2_binary"]
alchemy_hard = ["sqlalchemy", "psycopg2"] alchemy_hard = ["sqlalchemy", "psycopg2"]
bard = ["ffmpeg_python", "youtube_dl"] bard = ["ffmpeg_python", "youtube_dl"]
colorlog = ["colorlog"]
constellation = ["starlette", "uvicorn"] constellation = ["starlette", "uvicorn"]
discord = ["discord.py", "pynacl"] discord = ["discord.py", "pynacl"]
herald = ["websockets"] herald = ["websockets"]
@ -569,7 +581,7 @@ sentry = ["sentry_sdk"]
telegram = ["python_telegram_bot"] telegram = ["python_telegram_bot"]
[metadata] [metadata]
content-hash = "a51bc903341dd7fb6c2923e4f0a45654ccc9c011182ad9a67c2c80d24f62fb26" content-hash = "48fd4f6a0a25ffaf89999db4a999774b8e22794e07488b032ac0f244049caa5f"
python-versions = "^3.8" python-versions = "^3.8"
[metadata.files] [metadata.files]
@ -660,6 +672,10 @@ colorama = [
{file = "colorama-0.4.1-py2.py3-none-any.whl", hash = "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"}, {file = "colorama-0.4.1-py2.py3-none-any.whl", hash = "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"},
{file = "colorama-0.4.1.tar.gz", hash = "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d"}, {file = "colorama-0.4.1.tar.gz", hash = "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d"},
] ]
colorlog = [
{file = "colorlog-4.0.2-py2.py3-none-any.whl", hash = "sha256:450f52ea2a2b6ebb308f034ea9a9b15cea51e65650593dca1da3eb792e4e4981"},
{file = "colorlog-4.0.2.tar.gz", hash = "sha256:3cf31b25cbc8f86ec01fef582ef3b840950dea414084ed19ab922c8b493f9b42"},
]
cryptography = [ cryptography = [
{file = "cryptography-2.8-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"}, {file = "cryptography-2.8-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"},
{file = "cryptography-2.8-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2"}, {file = "cryptography-2.8-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2"},
@ -845,8 +861,8 @@ pyparsing = [
{file = "pyparsing-2.4.5.tar.gz", hash = "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"}, {file = "pyparsing-2.4.5.tar.gz", hash = "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"},
] ]
pytest = [ pytest = [
{file = "pytest-5.2.2-py3-none-any.whl", hash = "sha256:58cee9e09242937e136dbb3dab466116ba20d6b7828c7620f23947f37eb4dae4"}, {file = "pytest-5.2.3-py3-none-any.whl", hash = "sha256:b6cf7ad9064049ee486586b3a0ddd70dc5136c40e1147e7d286efd77ba66c5eb"},
{file = "pytest-5.2.2.tar.gz", hash = "sha256:27abc3fef618a01bebb1f0d6d303d2816a99aa87a5968ebc32fe971be91eb1e6"}, {file = "pytest-5.2.3.tar.gz", hash = "sha256:15837d2880cb94821087bc07476892ea740696b20e90288fd6c19e44b435abdb"},
] ]
python-dateutil = [ python-dateutil = [
{file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
@ -936,24 +952,13 @@ websockets = [
{file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"}, {file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"},
{file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"}, {file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"},
{file = "websockets-8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8"}, {file = "websockets-8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8"},
{file = "websockets-8.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb"},
{file = "websockets-8.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5"},
{file = "websockets-8.1-cp36-cp36m-win32.whl", hash = "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a"}, {file = "websockets-8.1-cp36-cp36m-win32.whl", hash = "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a"},
{file = "websockets-8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5"}, {file = "websockets-8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5"},
{file = "websockets-8.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989"}, {file = "websockets-8.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989"},
{file = "websockets-8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d"}, {file = "websockets-8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d"},
{file = "websockets-8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779"}, {file = "websockets-8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779"},
{file = "websockets-8.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8"},
{file = "websockets-8.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422"},
{file = "websockets-8.1-cp37-cp37m-win32.whl", hash = "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc"}, {file = "websockets-8.1-cp37-cp37m-win32.whl", hash = "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc"},
{file = "websockets-8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308"}, {file = "websockets-8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308"},
{file = "websockets-8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092"},
{file = "websockets-8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485"},
{file = "websockets-8.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1"},
{file = "websockets-8.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55"},
{file = "websockets-8.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824"},
{file = "websockets-8.1-cp38-cp38-win32.whl", hash = "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36"},
{file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"},
{file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"}, {file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"},
] ]
yarl = [ yarl = [

View file

@ -24,7 +24,7 @@
# telegram # telegram
python_telegram_bot = {version="^12.2.0", optional=true} python_telegram_bot = {version="^12.2.0", optional=true}
# discord # discord
"discord.py" = {git="https://github.com/Steffo99/discord.py.git", optional=true} # discord.py 1.2.4 is missing Go Live related methods "discord.py" = {git="https://github.com/Steffo99/discord.py", optional=true} # discord.py 1.2.4 is missing Go Live related methods
pynacl = {version="^1.3.0", optional=true} # This requires libffi-dev and python3.*-dev to be installed on Linux systems pynacl = {version="^1.3.0", optional=true} # This requires libffi-dev and python3.*-dev to be installed on Linux systems
# bard # bard
ffmpeg_python = {version="~0.2.0", optional=true} ffmpeg_python = {version="~0.2.0", optional=true}
@ -40,6 +40,8 @@
sentry_sdk = {version="~0.13.2", optional=true} sentry_sdk = {version="~0.13.2", optional=true}
# herald # herald
websockets = {version="^8.1", optional=true} websockets = {version="^8.1", optional=true}
# colorlog
colorlog = {version="^4.0.2", optional=true}
# Development dependencies # Development dependencies
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
@ -55,7 +57,7 @@
constellation = ["starlette", "uvicorn"] constellation = ["starlette", "uvicorn"]
sentry = ["sentry_sdk"] sentry = ["sentry_sdk"]
herald = ["websockets"] herald = ["websockets"]
colorlog = ["colorlog"]
[build-system] [build-system]
requires = ["poetry>=0.12"] requires = ["poetry>=0.12"]

View file

@ -1 +1,14 @@
__version__ = "5.1a1" __version__ = "5.1a1"
from . import alchemy, bard, commands, constellation, herald, backpack, serf, utils
__all__ = [
"alchemy",
"bard",
"commands",
"constellation",
"herald",
"serf",
"utils",
"backpack",
]

View file

@ -2,10 +2,9 @@ import click
import typing import typing
import importlib import importlib
import royalnet as r import royalnet as r
import royalherald as rh
import multiprocessing import multiprocessing
import keyring import keyring
import logging from logging import Formatter, StreamHandler, getLogger, Logger
@click.command() @click.command()
@ -13,55 +12,53 @@ import logging
help="Enable/disable the Telegram bot.") help="Enable/disable the Telegram bot.")
@click.option("--discord/--no-discord", default=None, @click.option("--discord/--no-discord", default=None,
help="Enable/disable the Discord bot.") help="Enable/disable the Discord bot.")
@click.option("--webserver/--no-webserver", default=None, @click.option("--constellation/--no-constellation", default=None,
help="Enable/disable the Web server.") help="Enable/disable the Constellation web server.")
@click.option("--webserver-port", default=8001, @click.option("--herald/--no-herald", default=None,
help="The port on which the web server will listen on.") help="Enable/disable the integrated Herald server."
@click.option("-d", "--database", type=str, default=None, " If turned off, Royalnet will try to connect to another server.")
help="The PostgreSQL database path.") @click.option("--remote-herald-address", type=str, default=None,
@click.option("-p", "--packs", type=str, multiple=True, default=[], help="If --no-herald is specified, connect to the Herald server at this URL instead.")
help="The names of the Packs that should be used.") @click.option("-c", "--constellation-port", default=44445,
@click.option("-n", "--network-address", type=str, default=None, help="The port on which the Constellation will serve webpages on.")
help="The Network server URL to connect to.") @click.option("-a", "--alchemy-url", type=str, default=None,
@click.option("-l", "--local-network-server", is_flag=True, default=False, help="The Alchemy database path.")
help="Locally run a Network server and bind it to port 44444. Overrides -n.") @click.option("-h", "--herald-port", type=int, default=44444,
@click.option("--local-network-server-port", type=int, default=44444, help="The port on which the Herald should be running.")
help="The port on which the local network will be ran.") @click.option("-p", "--pack", type=str, multiple=True, default=tuple(),
help="Import the pack with the specified name and use it in the Royalnet instance.")
@click.option("-s", "--secrets-name", type=str, default="__default__", @click.option("-s", "--secrets-name", type=str, default="__default__",
help="The name in the keyring that the secrets are stored with.") help="The name in the keyring that the secrets are stored with.")
@click.option("-v", "--verbose", is_flag=True, default=False, @click.option("-l", "--log-level", type=str, default="INFO",
help="Print all possible debug information.") help="Select how much information you want to be printed on the console."
" Valid log levels are: FATAL/ERROR/WARNING/INFO/DEBUG")
def run(telegram: typing.Optional[bool], def run(telegram: typing.Optional[bool],
discord: typing.Optional[bool], discord: typing.Optional[bool],
webserver: typing.Optional[bool], constellation: typing.Optional[bool],
webserver_port: typing.Optional[int], herald: typing.Optional[bool],
database: typing.Optional[str], remote_herald_address: typing.Optional[str],
packs: typing.Tuple[str], constellation_port: int,
network_address: typing.Optional[str], alchemy_url: typing.Optional[str],
local_network_server: bool, herald_port: int,
local_network_server_port: int, pack: typing.Tuple[str],
secrets_name: str, secrets_name: str,
verbose: bool): log_level: str):
# Setup logging # Initialize logging
if verbose: royalnet_log: Logger = getLogger("royalnet")
core_logger = logging.root royalnet_log.setLevel(log_level)
core_logger.setLevel(logging.DEBUG) stream_handler = StreamHandler()
stream_handler = logging.StreamHandler() stream_handler.formatter = Formatter("{asctime}\t{name}\t{levelname}\t{message}", style="{")
stream_handler.formatter = logging.Formatter("{asctime}\t{name}\t{levelname}\t{message}", style="{") royalnet_log.addHandler(stream_handler)
core_logger.addHandler(stream_handler)
core_logger.debug("Logging setup complete.")
# Get the network password def get_secret(username: str):
network_password = keyring.get_password(f"Royalnet/{secrets_name}", "network") return keyring.get_password(f"Royalnet/{secrets_name}", username)
# Get the sentry dsn
sentry_dsn = keyring.get_password(f"Royalnet/{secrets_name}", "sentry")
# Enable / Disable interfaces # Enable / Disable interfaces
interfaces = { interfaces = {
"telegram": telegram, "telegram": telegram,
"discord": discord, "discord": discord,
"webserver": webserver "herald": herald,
"constellation": constellation,
} }
# If any interface is True, then the undefined ones should be False # If any interface is True, then the undefined ones should be False
if any(interfaces[name] is True for name in interfaces): if any(interfaces[name] is True for name in interfaces):
@ -79,36 +76,30 @@ def run(telegram: typing.Optional[bool],
for name in interfaces: for name in interfaces:
interfaces[name] = True interfaces[name] = True
server_process: typing.Optional[multiprocessing.Process] = None herald_process: typing.Optional[multiprocessing.Process] = None
# Start the network server # Start the Herald server
if local_network_server: if interfaces["herald"]:
server_process = multiprocessing.Process(name="Network Server", herald_config = r.herald.Config(name="<server>",
target=rh.Server("0.0.0.0", local_network_server_port, network_password).run_blocking, address="127.0.0.1",
port=herald_port,
secret=get_secret("herald"),
secure=False,
path="/")
herald_process = multiprocessing.Process(name="Herald",
target=r.herald.Server(config=herald_config).run_blocking,
daemon=True) daemon=True)
server_process.start() herald_process.start()
network_address = f"ws://127.0.0.1:{local_network_server_port}/" else:
herald_config = r.herald.Config(name=...,
# Create a Royalnet configuration address=remote_herald_address,
network_config: typing.Optional[rh.Config] = None port=herald_port,
if network_address is not None: secret=get_secret("herald"),
network_config = rh.Config(network_address, network_password) secure=False,
path="/")
# Create a Alchemy configuration
telegram_db_config: typing.Optional[r.alchemy.DatabaseConfig] = None
discord_db_config: typing.Optional[r.alchemy.DatabaseConfig] = None
if database is not None:
telegram_db_config = r.alchemy.DatabaseConfig(database,
r.packs.common.tables.User,
r.packs.common.tables.Telegram,
"tg_id")
discord_db_config = r.alchemy.DatabaseConfig(database,
r.packs.common.tables.User,
r.packs.common.tables.Discord,
"discord_id")
# Import command and star packs # Import command and star packs
packs: typing.List[str] = list(packs) packs: typing.List[str] = list(pack)
packs.append("royalnet.packs.common") # common pack is always imported packs.append("royalnet.backpack") # backpack is always imported
enabled_commands = [] enabled_commands = []
enabled_page_stars = [] enabled_page_stars = []
enabled_exception_stars = [] enabled_exception_stars = []
@ -132,62 +123,66 @@ def run(telegram: typing.Optional[bool],
telegram_process: typing.Optional[multiprocessing.Process] = None telegram_process: typing.Optional[multiprocessing.Process] = None
if interfaces["telegram"]: if interfaces["telegram"]:
click.echo("\n@BotFather Commands String") telegram_db_config = r.serf.AlchemyConfig(database_url=alchemy_url,
for command in enabled_commands: master_table=r.backpack.tables.User,
click.echo(f"{command.name} - {command.description}") identity_table=r.backpack.tables.Telegram,
click.echo("") identity_column="tg_id")
telegram_bot = r.interfaces.TelegramBot(network_config=network_config, telegram_serf_kwargs = {
database_config=telegram_db_config, 'alchemy_config': telegram_db_config,
sentry_dsn=sentry_dsn, 'commands': enabled_commands,
commands=enabled_commands, 'network_config': herald_config.copy(name="telegram"),
secrets_name=secrets_name) 'secrets_name': secrets_name
telegram_process = multiprocessing.Process(name="Telegram Interface", }
target=telegram_bot.run_blocking, telegram_process = multiprocessing.Process(name="Telegram Serf",
args=(verbose,), target=r.serf.telegram.TelegramSerf.run_process,
kwargs=telegram_serf_kwargs,
daemon=True) daemon=True)
telegram_process.start() telegram_process.start()
discord_process: typing.Optional[multiprocessing.Process] = None discord_process: typing.Optional[multiprocessing.Process] = None
if interfaces["discord"]: if interfaces["discord"]:
discord_bot = r.interfaces.DiscordBot(network_config=network_config, discord_db_config = r.serf.AlchemyConfig(database_url=alchemy_url,
database_config=discord_db_config, master_table=r.backpack.tables.User,
sentry_dsn=sentry_dsn, identity_table=r.backpack.tables.Discord,
commands=enabled_commands, identity_column="discord_id")
secrets_name=secrets_name) discord_serf_kwargs = {
discord_process = multiprocessing.Process(name="Discord Interface", 'alchemy_config': discord_db_config,
target=discord_bot.run_blocking, 'commands': enabled_commands,
args=(verbose,), 'network_config': herald_config.copy(name="discord"),
'secrets_name': secrets_name
}
discord_process = multiprocessing.Process(name="Discord Serf",
target=r.serf.discord.DiscordSerf.run_process,
kwargs=discord_serf_kwargs,
daemon=True) daemon=True)
discord_process.start() discord_process.start()
webserver_process: typing.Optional[multiprocessing.Process] = None constellation_process: typing.Optional[multiprocessing.Process] = None
if interfaces["webserver"]: if interfaces["constellation"]:
# Common tables are always included
constellation_tables = set(r.packs.common.available_tables)
# Find the required tables
for star in [*enabled_page_stars, *enabled_exception_stars]:
constellation_tables = constellation_tables.union(star.tables)
# Create the Constellation # Create the Constellation
constellation = r.web.Constellation(page_stars=enabled_page_stars, constellation_kwargs = {
exc_stars=enabled_exception_stars, 'address': "127.0.0.1",
secrets_name=secrets_name, 'port': constellation_port,
database_uri=database, 'secrets_name': secrets_name,
tables=constellation_tables) 'database_uri': alchemy_url,
webserver_process = multiprocessing.Process(name="Constellation Webserver", 'page_stars': enabled_page_stars,
target=constellation.run_blocking, 'exc_stars': enabled_exception_stars,
args=("0.0.0.0", webserver_port, verbose,), }
constellation_process = multiprocessing.Process(name="Constellation",
target=r.constellation.Constellation.run_process,
kwargs=constellation_kwargs,
daemon=True) daemon=True)
webserver_process.start() constellation_process.start()
click.echo("Royalnet processes have been started. You can force-quit by pressing Ctrl+C.") click.echo("Royalnet is now running! You can stop its execution by pressing Ctrl+C at any time.")
if server_process is not None: if herald_process is not None:
server_process.join() herald_process.join()
if telegram_process is not None: if telegram_process is not None:
telegram_process.join() telegram_process.join()
if discord_process is not None: if discord_process is not None:
discord_process.join() discord_process.join()
if webserver_process is not None: if constellation_process is not None:
webserver_process.join() constellation_process.join()
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -0,0 +1,3 @@
# `backpack`
A Pack that is imported by default by all `royalnet` instances.

View file

@ -1,4 +1,4 @@
# This is a template Pack __init__. You can use this without changing anything in other packages too! """A Pack that is imported by default by all :mod:`royalnet` instances."""
from . import commands, tables, stars from . import commands, tables, stars
from .commands import available_commands from .commands import available_commands

View file

@ -1,5 +1,5 @@
import royalnet
from royalnet.commands import * from royalnet.commands import *
from royalnet.version import semantic
class VersionCommand(Command): class VersionCommand(Command):
@ -8,7 +8,7 @@ class VersionCommand(Command):
description: str = "Get the current Royalnet version." description: str = "Get the current Royalnet version."
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: CommandArgs, data: CommandData) -> None:
message = f" Royalnet {semantic}\n" message = f" Royalnet {royalnet.__version__}\n"
if "69" in message: if "69" in message:
message += "(Nice.)" message += "(Nice.)"
await data.reply(message) await data.reply(message)

View file

@ -1,15 +1,18 @@
import royalnet import royalnet
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import * from starlette.responses import *
from royalnet.web import PageStar from royalnet.constellation import PageStar
from ..tables import available_tables
class ApiRoyalnetVersionStar(PageStar): class ApiRoyalnetVersionStar(PageStar):
path = "/api/royalnet/version" path = "/api/royalnet/version"
tables = set(available_tables)
async def page(self, request: Request) -> JSONResponse: async def page(self, request: Request) -> JSONResponse:
return JSONResponse({ return JSONResponse({
"version": { "version": {
"semantic": royalnet.version.semantic "semantic": royalnet.__version__,
} }
}) })

View file

@ -34,7 +34,7 @@ class YtdlMp3:
) )
self.mp3_filename = destination_filename self.mp3_filename = destination_filename
def delete_asap(self) -> None: async def delete_asap(self) -> None:
"""Delete the mp3 file.""" """Delete the mp3 file."""
if self.is_converted: if self.is_converted:
async with self.lock.exclusive(): async with self.lock.exclusive():

View file

@ -1,3 +1,4 @@
from asyncio import AbstractEventLoop
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from .errors import UnsupportedError from .errors import UnsupportedError
from .commandinterface import CommandInterface from .commandinterface import CommandInterface
@ -8,9 +9,10 @@ if TYPE_CHECKING:
class CommandData: class CommandData:
def __init__(self, interface: CommandInterface, session: Optional["Session"]): def __init__(self, interface: CommandInterface, session: Optional["Session"], loop: AbstractEventLoop):
self._interface: CommandInterface = interface self._interface: CommandInterface = interface
self._session: Optional["Session"] = session self._session: Optional["Session"] = session
self.loop: AbstractEventLoop = loop
@property @property
def session(self) -> "Session": def session(self) -> "Session":

View file

@ -6,9 +6,9 @@ import keyring
def run(): def run():
click.echo("Welcome to the Royalnet configuration creator!") click.echo("Welcome to the Royalnet configuration creator!")
secrets_name = click.prompt("Desired secrets name", default="__default__") secrets_name = click.prompt("Desired secrets name", default="__default__")
network = click.prompt("Network password", default="") network = click.prompt("Herald password", default="")
if network: if network:
keyring.set_password(f"Royalnet/{secrets_name}", "network", network) keyring.set_password(f"Royalnet/{secrets_name}", "herald", network)
telegram = click.prompt("Telegram Bot API token", default="") telegram = click.prompt("Telegram Bot API token", default="")
if telegram: if telegram:
keyring.set_password(f"Royalnet/{secrets_name}", "telegram", telegram) keyring.set_password(f"Royalnet/{secrets_name}", "telegram", telegram)
@ -21,9 +21,6 @@ def run():
sentry = click.prompt("Sentry DSN", default="") sentry = click.prompt("Sentry DSN", default="")
if sentry: if sentry:
keyring.set_password(f"Royalnet/{secrets_name}", "sentry", sentry) keyring.set_password(f"Royalnet/{secrets_name}", "sentry", sentry)
leagueoflegends = click.prompt("League of Legends API Token", default="")
if leagueoflegends:
keyring.set_password(f"Royalnet/{secrets_name}", "leagueoflegends", leagueoflegends)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -2,7 +2,6 @@ import typing
import logging import logging
import royalnet import royalnet
import keyring import keyring
from royalnet.alchemy import Alchemy
from .star import PageStar, ExceptionStar from .star import PageStar, ExceptionStar
try: try:
@ -60,7 +59,7 @@ class Constellation:
"""The :class:`Starlette` app.""" """The :class:`Starlette` app."""
log.debug("Finding required Tables...") log.debug("Finding required Tables...")
tables = set() tables = set(royalnet.backpack.available_tables)
for SelectedPageStar in page_stars: for SelectedPageStar in page_stars:
tables = tables.union(SelectedPageStar.tables) tables = tables.union(SelectedPageStar.tables)
for SelectedExcStar in exc_stars: for SelectedExcStar in exc_stars:
@ -68,7 +67,7 @@ class Constellation:
log.debug(f"Found Tables: {' '.join([table.__name__ for table in tables])}") log.debug(f"Found Tables: {' '.join([table.__name__ for table in tables])}")
log.info(f"Creating Alchemy...") log.info(f"Creating Alchemy...")
self.alchemy: Alchemy = Alchemy(database_uri=database_uri, tables=tables) self.alchemy: royalnet.alchemy.Alchemy = royalnet.alchemy.Alchemy(database_uri=database_uri, tables=tables)
"""The :class:`Alchemy: of this Constellation.""" """The :class:`Alchemy: of this Constellation."""
log.info("Registering PageStars...") log.info("Registering PageStars...")
@ -98,19 +97,34 @@ class Constellation:
username: the name of the secret that should be retrieved.""" username: the name of the secret that should be retrieved."""
return keyring.get_password(f"Royalnet/{self.secrets_name}", username) return keyring.get_password(f"Royalnet/{self.secrets_name}", username)
def run_blocking(self, address: str, port: int): @classmethod
"""Blockingly run the Constellation. def run_process(cls,
address: str,
port: int,
secrets_name: str,
database_uri: str,
page_stars: typing.List[typing.Type[PageStar]] = None,
exc_stars: typing.List[typing.Type[ExceptionStar]] = None,
*,
debug: bool = __debug__,):
"""Blockingly create and run the Constellation.
This should be used as the target of a :class:`multiprocessing.Process`. This should be used as the target of a :class:`multiprocessing.Process`.
Args: Args:
address: The IP address this Constellation should bind to. address: The IP address this Constellation should bind to.
port: The port this Constellation should listen for requests on.""" port: The port this Constellation should listen for requests on."""
constellation = cls(secrets_name=secrets_name,
database_uri=database_uri,
page_stars=page_stars,
exc_stars=exc_stars,
debug=debug)
# Initialize Sentry on the process # Initialize Sentry on the process
if sentry_sdk is None: if sentry_sdk is None:
log.info("Sentry: not installed") log.info("Sentry: not installed")
else: else:
sentry_dsn = self.get_secret("sentry") sentry_dsn = constellation.get_secret("sentry")
if not sentry_dsn: if not sentry_dsn:
log.info("Sentry: disabled") log.info("Sentry: disabled")
else: else:
@ -128,11 +142,11 @@ class Constellation:
log.info(f"Sentry: enabled (Royalnet {release})") log.info(f"Sentry: enabled (Royalnet {release})")
# Run the server # Run the server
log.info(f"Running Constellation on {address}:{port}...") log.info(f"Running Constellation on {address}:{port}...")
self.running = True constellation.running = True
try: try:
uvicorn.run(self.starlette, host=address, port=port) uvicorn.run(constellation.starlette, host=address, port=port)
finally: finally:
self.running = False constellation.running = False
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__qualname__}: {'running' if self.running else 'inactive'}>" return f"<{self.__class__.__qualname__}: {'running' if self.running else 'inactive'}>"

View file

@ -1,3 +1,6 @@
from typing import Optional
class Config: class Config:
def __init__(self, def __init__(self,
name: str, name: str,
@ -30,5 +33,20 @@ class Config:
def url(self): def url(self):
return f"ws{'s' if self.secure else ''}://{self.address}:{self.port}{self.path}" return f"ws{'s' if self.secure else ''}://{self.address}:{self.port}{self.path}"
def copy(self,
name: Optional[str] = None,
address: Optional[str] = None,
port: Optional[int] = None,
secret: Optional[str] = None,
secure: Optional[bool] = None,
path: Optional[str] = None):
"""Create an exact copy of this configuration, but with different parameters."""
return self.__class__(name=name if name else self.name,
address=address if address else self.address,
port=port if port else self.port,
secret=secret if secret else self.secret,
secure=secure if secure else self.secure,
path=path if path else self.path)
def __repr__(self): def __repr__(self):
return f"<HeraldConfig for {self.url}>" return f"<HeraldConfig for {self.url}>"

View file

@ -1,7 +1,10 @@
from .serf import Serf from .serf import Serf
from .alchemyconfig import AlchemyConfig from .alchemyconfig import AlchemyConfig
from . import telegram, discord
__all__ = [ __all__ = [
"Serf", "Serf",
"AlchemyConfig" "AlchemyConfig",
"telegram",
"discord",
] ]

View file

@ -1,18 +1,16 @@
from typing import Type, TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING:
from sqlalchemy.schema import Table
class AlchemyConfig: class AlchemyConfig:
"""A helper class to configure :class:`Alchemy` in a :class:`Serf`.""" """A helper class to configure :class:`Alchemy` in a :class:`Serf`."""
def __init__(self, def __init__(self,
database_url: str, database_url: str,
master_table: Type[Table], master_table: type,
identity_table: Type[Table], identity_table: type,
identity_column: str): identity_column: str):
self.database_url: str = database_url self.database_url: str = database_url
self.master_table: Type[Table] = master_table self.master_table: type = master_table
self.identity_table: Type[Table] = identity_table self.identity_table: type = identity_table
self.identity_column: str = identity_column self.identity_column: str = identity_column
def __repr__(self): def __repr__(self):

View file

@ -1,3 +1,4 @@
import asyncio
import logging import logging
from typing import Type, Optional, List, Union from typing import Type, Optional, List, Union
from royalnet.commands import Command, CommandInterface, CommandData, CommandArgs, CommandError, InvalidInputError, \ from royalnet.commands import Command, CommandInterface, CommandData, CommandArgs, CommandError, InvalidInputError, \
@ -63,8 +64,12 @@ class DiscordSerf(Serf):
def data_factory(self) -> Type[CommandData]: def data_factory(self) -> Type[CommandData]:
# noinspection PyMethodParameters,PyAbstractClass # noinspection PyMethodParameters,PyAbstractClass
class DiscordData(CommandData): class DiscordData(CommandData):
def __init__(data, interface: CommandInterface, session, message: discord.Message): def __init__(data,
super().__init__(interface=interface, session=session) interface: CommandInterface,
session,
loop: asyncio.AbstractEventLoop,
message: discord.Message):
super().__init__(interface=interface, session=session, loop=loop)
data.message = message data.message = message
async def reply(data, text: str): async def reply(data, text: str):
@ -118,7 +123,7 @@ class DiscordSerf(Serf):
else: else:
session = None session = None
# Prepare data # Prepare data
data = self.Data(interface=command.interface, session=session, message=message) data = self.Data(interface=command.interface, session=session, loop=self.loop, message=message)
try: try:
# Run the command # Run the command
await command.run(CommandArgs(parameters), data) await command.run(CommandArgs(parameters), data)
@ -196,6 +201,7 @@ class DiscordSerf(Serf):
return DiscordClient return DiscordClient
async def run(self): async def run(self):
await super().run()
token = self.get_secret("discord") token = self.get_secret("discord")
await self.client.login(token) await self.client.login(token)
await self.client.connect() await self.client.connect()

View file

@ -1,5 +1,5 @@
import logging import logging
from asyncio import Task, AbstractEventLoop from asyncio import Task, AbstractEventLoop, get_event_loop
from typing import Type, Optional, Awaitable, Dict, List, Any, Callable, Union, Set from typing import Type, Optional, Awaitable, Dict, List, Any, Callable, Union, Set
from keyring import get_password from keyring import get_password
from sqlalchemy.schema import Table from sqlalchemy.schema import Table
@ -130,10 +130,10 @@ class Serf:
self.alchemy = Alchemy(alchemy_config.database_url, tables) self.alchemy = Alchemy(alchemy_config.database_url, tables)
self._master_table = self.alchemy.get(alchemy_config.master_table) self._master_table = self.alchemy.get(alchemy_config.master_table)
self._identity_table = self.alchemy.get(alchemy_config.identity_table) self._identity_table = self.alchemy.get(alchemy_config.identity_table)
# FIXME: this MAY break # This is fine, as Pycharm doesn't know that identity_table is a class and not an object
self._identity_column = self._identity_table.__getattribute__(alchemy_config.identity_column) # noinspection PyArgumentList
# self._identity_column = self._identity_table.__getattribute__(self._identity_table, self._identity_column = self._identity_table.__getattribute__(self._identity_table,
# alchemy_config.identity_column) alchemy_config.identity_column)
@property @property
def _identity_chain(self) -> tuple: def _identity_chain(self) -> tuple:
@ -147,7 +147,6 @@ class Serf:
class GenericInterface(CommandInterface): class GenericInterface(CommandInterface):
alchemy: Alchemy = self.alchemy alchemy: Alchemy = self.alchemy
bot: "Serf" = self bot: "Serf" = self
loop: AbstractEventLoop = self.loop
def register_herald_action(ci, def register_herald_action(ci,
event_name: str, event_name: str,
@ -232,10 +231,9 @@ class Serf:
def init_network(self, config: HeraldConfig): def init_network(self, config: HeraldConfig):
"""Create a :py:class:`Link`, and run it as a :py:class:`asyncio.Task`.""" """Create a :py:class:`Link`, and run it as a :py:class:`asyncio.Task`."""
log.debug(f"Initializing herald...") log.debug(f"Initializing herald...")
self.herald: Link = Link(config, self._network_handler) self.herald: Link = Link(config, self.network_handler)
self.herald_task = self.loop.create_task(self.herald.run())
async def _network_handler(self, message: Union[Request, Broadcast]) -> Response: async def network_handler(self, message: Union[Request, Broadcast]) -> Response:
try: try:
network_handler = self.herald_handlers[message.handler] network_handler = self.herald_handlers[message.handler]
except KeyError: except KeyError:
@ -288,19 +286,24 @@ class Serf:
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."""
raise NotImplementedError() self.herald_task = self.loop.create_task(self.herald.run())
# OVERRIDE THIS METHOD!
def run_blocking(self): @classmethod
"""Blockingly run the Serf. def run_process(cls, *args, **kwargs):
"""Blockingly create and run the Serf.
This should be used as the target of a :class:`multiprocessing.Process`.""" This should be used as the target of a :class:`multiprocessing.Process`."""
serf = cls(*args, **kwargs)
if sentry_sdk is None: if sentry_sdk is None:
log.info("Sentry: not installed") log.info("Sentry: not installed")
else: else:
sentry_dsn = self.get_secret("sentry") sentry_dsn = serf.get_secret("sentry")
if sentry_dsn is None: if sentry_dsn is None:
log.info("Sentry: disabled") log.info("Sentry: disabled")
else: else:
self.init_sentry(sentry_dsn) serf.init_sentry(sentry_dsn)
self.loop.run_until_complete(self.run()) serf.loop = get_event_loop()
serf.loop.run_until_complete(serf.run())

View file

@ -99,8 +99,12 @@ class TelegramSerf(Serf):
def data_factory(self) -> Type[CommandData]: def data_factory(self) -> Type[CommandData]:
# noinspection PyMethodParameters # noinspection PyMethodParameters
class TelegramData(CommandData): class TelegramData(CommandData):
def __init__(data, interface: CommandInterface, session, update: telegram.Update): def __init__(data,
super().__init__(interface=interface, session=session) interface: CommandInterface,
session,
loop: asyncio.AbstractEventLoop,
update: telegram.Update):
super().__init__(interface=interface, session=session, loop=loop)
data.update = update data.update = update
async def reply(data, text: str): async def reply(data, text: str):
@ -192,7 +196,7 @@ class TelegramSerf(Serf):
session = None session = None
try: try:
# Create the command data # Create the command data
data = self.Data(interface=command.interface, session=session, update=update) data = self.Data(interface=command.interface, session=session, loop=self.loop, update=update)
try: try:
# Run the command # Run the command
await command.run(CommandArgs(parameters), data) await command.run(CommandArgs(parameters), data)
@ -240,6 +244,7 @@ class TelegramSerf(Serf):
pass pass
async def run(self): async def run(self):
await super().run()
while True: while True:
# Get the latest 100 updates # Get the latest 100 updates
last_updates: List[telegram.Update] = await self.api_call(self.client.get_updates, last_updates: List[telegram.Update] = await self.api_call(self.client.get_updates,

View file

@ -1,11 +1,7 @@
"""Miscellaneous useful functions and classes."""
from .asyncify import asyncify from .asyncify import asyncify
from .escaping import telegram_escape, discord_escape
from .safeformat import safeformat from .safeformat import safeformat
from .classdictjanitor import cdj
from .sleep_until import sleep_until from .sleep_until import sleep_until
from .formatters import andformat, plusformat, underscorize, ytdldateformat, numberemojiformat, splitstring, ordinalformat from .formatters import andformat, underscorize, ytdldateformat, numberemojiformat, splitstring, ordinalformat
from .urluuid import to_urluuid, from_urluuid from .urluuid import to_urluuid, from_urluuid
from .multilock import MultiLock from .multilock import MultiLock

View file

@ -1,95 +0,0 @@
import typing
import uvicorn
import logging
import sentry_sdk
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
import royalnet
import keyring
from starlette.applications import Starlette
from .star import PageStar, ExceptionStar
log = logging.getLogger(__name__)
class Constellation:
def __init__(self,
secrets_name: str,
database_uri: str,
tables: set,
page_stars: typing.List[typing.Type[PageStar]] = None,
exc_stars: typing.List[typing.Type[ExceptionStar]] = None,
*,
debug: bool = __debug__,):
if page_stars is None:
page_stars = []
if exc_stars is None:
exc_stars = []
self.secrets_name: str = secrets_name
log.info("Creating starlette app...")
self.starlette = Starlette(debug=debug)
log.info(f"Creating alchemy with tables: {' '.join([table.__name__ for table in tables])}")
self.alchemy: royalnet.alchemy.Alchemy = royalnet.alchemy.Alchemy(database_uri=database_uri, tables=tables)
log.info("Registering page_stars...")
for SelectedPageStar in page_stars:
try:
page_star_instance = SelectedPageStar(constellation=self)
except Exception as e:
log.error(f"{e.__class__.__qualname__} during the registration of {SelectedPageStar.__qualname__}")
sentry_sdk.capture_exception(e)
continue
log.info(f"Registering: {page_star_instance.path} -> {page_star_instance.__class__.__name__}")
self.starlette.add_route(page_star_instance.path, page_star_instance.page, page_star_instance.methods)
log.info("Registering exc_stars...")
for SelectedExcStar in exc_stars:
try:
exc_star_instance = SelectedExcStar(constellation=self)
except Exception as e:
log.error(f"{e.__class__.__qualname__} during the registration of {SelectedExcStar.__qualname__}")
sentry_sdk.capture_exception(e)
continue
log.info(f"Registering: {exc_star_instance.error} -> {exc_star_instance.__class__.__name__}")
self.starlette.add_exception_handler(exc_star_instance.error, exc_star_instance.page)
def _init_sentry(self):
sentry_dsn = self.get_secret("sentry")
if sentry_dsn:
# noinspection PyUnreachableCode
if __debug__:
release = "DEV"
else:
release = royalnet.version.semantic
log.info(f"Sentry: enabled (Royalnet {release})")
self.sentry = sentry_sdk.init(sentry_dsn,
integrations=[AioHttpIntegration(),
SqlalchemyIntegration(),
LoggingIntegration(event_level=None)],
release=release)
else:
log.info("Sentry: disabled")
def get_secret(self, username: str):
return keyring.get_password(f"Royalnet/{self.secrets_name}", username)
def set_secret(self, username: str, password: str):
return keyring.set_password(f"Royalnet/{self.secrets_name}", username, password)
def run_blocking(self, address: str, port: int, verbose: bool):
if verbose:
core_logger = logging.root
core_logger.setLevel(logging.DEBUG)
stream_handler = logging.StreamHandler()
stream_handler.formatter = logging.Formatter("{asctime}\t{name}\t{levelname}\t{message}", style="{")
core_logger.addHandler(stream_handler)
core_logger.debug("Logging setup complete.")
self._init_sentry()
log.info(f"Running constellation server on {address}:{port}...")
uvicorn.run(self.starlette, host=address, port=port)

View file

@ -1,37 +0,0 @@
import typing
from starlette.requests import Request
from starlette.responses import Response
if typing.TYPE_CHECKING:
from .constellation import Constellation
class Star:
tables: set = {}
def __init__(self, constellation: "Constellation"):
self.constellation: "Constellation" = constellation
async def page(self, request: Request) -> Response:
raise NotImplementedError()
@property
def alchemy(self):
return self.constellation.alchemy
@property
def Session(self):
return self.constellation.alchemy._Session
@property
def session_acm(self):
return self.constellation.alchemy.session_acm
class PageStar(Star):
path: str = NotImplemented
methods: typing.List[str] = ["GET"]
class ExceptionStar(Star):
error: typing.Union[typing.Type[Exception], int]