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

More progress

This commit is contained in:
Steffo 2019-11-12 21:01:18 +01:00
parent db39874408
commit 48c746ea7d
35 changed files with 1618 additions and 125 deletions

View file

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View file

@ -6,5 +6,5 @@
<component name="MacroExpansionManager">
<option name="directoryName" value="cqweyjJr" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (royalnet-1MWM6-kd-py3.8)" project-jdk-type="Python SDK" />
</project>

View file

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

755
poetry.lock generated Normal file
View file

@ -0,0 +1,755 @@
[[package]]
category = "main"
description = "Async http client/server framework (asyncio)"
name = "aiohttp"
optional = true
python-versions = ">=3.5.3"
version = "3.5.4"
[package.dependencies]
async-timeout = ">=3.0,<4.0"
attrs = ">=17.3.0"
chardet = ">=2.0,<4.0"
multidict = ">=4.0,<5.0"
yarl = ">=1.0,<2.0"
[package.extras]
speedups = ["aiodns", "brotlipy", "cchardet"]
[[package]]
category = "main"
description = "Timeout context manager for asyncio programs"
name = "async-timeout"
optional = true
python-versions = ">=3.5.3"
version = "3.0.1"
[[package]]
category = "main"
description = "Classes Without Boilerplate"
name = "attrs"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "19.3.0"
[package.extras]
azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"]
docs = ["sphinx", "zope.interface"]
tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
[[package]]
category = "main"
description = "Python package for providing Mozilla's CA Bundle."
name = "certifi"
optional = true
python-versions = "*"
version = "2019.9.11"
[[package]]
category = "main"
description = "Foreign Function Interface for Python calling C code."
name = "cffi"
optional = true
python-versions = "*"
version = "1.13.2"
[package.dependencies]
pycparser = "*"
[[package]]
category = "main"
description = "Universal encoding detector for Python 2 and 3"
name = "chardet"
optional = true
python-versions = "*"
version = "3.0.4"
[[package]]
category = "main"
description = "Composable command line interface toolkit"
name = "click"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "7.0"
[[package]]
category = "main"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
name = "cryptography"
optional = true
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "2.8"
[package.dependencies]
cffi = ">=1.8,<1.11.3 || >1.11.3"
six = ">=1.4.1"
[package.extras]
docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0)", "sphinx-rtd-theme"]
docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
idna = ["idna (>=2.1)"]
pep8test = ["flake8", "flake8-import-order", "pep8-naming"]
test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"]
[[package]]
category = "main"
description = "Date parsing library designed to parse dates from HTML pages"
name = "dateparser"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.7.2"
[package.dependencies]
python-dateutil = "*"
pytz = "*"
regex = "*"
tzlocal = "*"
[[package]]
category = "main"
description = "A python wrapper for the Discord API"
name = "discord.py"
optional = true
python-versions = ">=3.5.3"
version = "1.3.0a2122+g09a08f9"
[package.dependencies]
aiohttp = ">=3.3.0,<3.6.0"
websockets = ">=8.0"
[package.extras]
docs = ["sphinx (1.8.5)", "sphinxcontrib_trio (1.1.0)", "sphinxcontrib-websupport"]
voice = ["PyNaCl (1.3.0)"]
[package.source]
reference = "09a08f9a9f126aa1f55c2444eb70508d1d52f8d9"
type = "git"
url = "https://github.com/Steffo99/discord.py"
[[package]]
category = "main"
description = "Python bindings for FFmpeg - with complex filtering support"
name = "ffmpeg-python"
optional = true
python-versions = "*"
version = "0.2.0"
[package.dependencies]
future = "*"
[package.extras]
dev = ["future (0.17.1)", "numpy (1.16.4)", "pytest-mock (1.10.4)", "pytest (4.6.1)", "Sphinx (2.1.0)", "tox (3.12.1)"]
[[package]]
category = "main"
description = "Clean single-source support for Python 3 and 2"
name = "future"
optional = true
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "0.18.2"
[[package]]
category = "main"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
name = "h11"
optional = true
python-versions = "*"
version = "0.8.1"
[[package]]
category = "main"
description = "A collection of framework independent HTTP protocol utils."
marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"pypy\""
name = "httptools"
optional = true
python-versions = "*"
version = "0.0.13"
[[package]]
category = "main"
description = "Internationalized Domain Names in Applications (IDNA)"
name = "idna"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.8"
[[package]]
category = "main"
description = "multidict implementation"
name = "multidict"
optional = true
python-versions = ">=3.4.1"
version = "4.5.2"
[[package]]
category = "main"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
name = "psycopg2"
optional = true
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "2.8.4"
[[package]]
category = "main"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
name = "psycopg2-binary"
optional = true
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "2.8.4"
[[package]]
category = "main"
description = "C parser in Python"
name = "pycparser"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.19"
[[package]]
category = "main"
description = "Python binding to the Networking and Cryptography (NaCl) library"
name = "pynacl"
optional = true
python-versions = "*"
version = "1.3.0"
[package.dependencies]
cffi = ">=1.4.1"
six = "*"
[package.extras]
docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)", "hypothesis (>=3.27.0)"]
[[package]]
category = "main"
description = "Extensions to the standard Python datetime module"
name = "python-dateutil"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
version = "2.8.1"
[package.dependencies]
six = ">=1.5"
[[package]]
category = "main"
description = "We have made you a wrapper you can't refuse"
name = "python-telegram-bot"
optional = true
python-versions = "*"
version = "12.2.0"
[package.dependencies]
certifi = "*"
cryptography = "*"
future = ">=0.16.0"
tornado = ">=5.1"
[package.extras]
json = ["ujson"]
socks = ["pysocks"]
[[package]]
category = "main"
description = "World timezone definitions, modern and historical"
name = "pytz"
optional = false
python-versions = "*"
version = "2019.3"
[[package]]
category = "main"
description = "Alternative regular expression module, to replace re."
name = "regex"
optional = false
python-versions = "*"
version = "2019.11.1"
[[package]]
category = "main"
description = "Python client for Sentry (https://getsentry.com)"
name = "sentry-sdk"
optional = true
python-versions = "*"
version = "0.13.2"
[package.dependencies]
certifi = "*"
urllib3 = ">=1.10.0"
[package.extras]
bottle = ["bottle (>=0.12.13)"]
falcon = ["falcon (>=1.4)"]
flask = ["flask (>=0.8)", "blinker (>=1.1)"]
[[package]]
category = "main"
description = "Python 2 and 3 compatibility utilities"
name = "six"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*"
version = "1.13.0"
[[package]]
category = "main"
description = "Database Abstraction Library"
name = "sqlalchemy"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.3.11"
[package.extras]
mssql = ["pyodbc"]
mssql_pymssql = ["pymssql"]
mssql_pyodbc = ["pyodbc"]
mysql = ["mysqlclient"]
oracle = ["cx-oracle"]
postgresql = ["psycopg2"]
postgresql_pg8000 = ["pg8000"]
postgresql_psycopg2binary = ["psycopg2-binary"]
postgresql_psycopg2cffi = ["psycopg2cffi"]
pymysql = ["pymysql"]
[[package]]
category = "main"
description = "The little ASGI library that shines."
name = "starlette"
optional = true
python-versions = ">=3.6"
version = "0.12.13"
[package.extras]
full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"]
[[package]]
category = "main"
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
name = "tornado"
optional = true
python-versions = ">= 3.5"
version = "6.0.3"
[[package]]
category = "main"
description = "tzinfo object for the local timezone"
name = "tzlocal"
optional = false
python-versions = "*"
version = "2.0.0"
[package.dependencies]
pytz = "*"
[[package]]
category = "main"
description = "HTTP library with thread-safe connection pooling, file post, and more."
name = "urllib3"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4"
version = "1.25.7"
[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
[[package]]
category = "main"
description = "The lightning-fast ASGI server."
name = "uvicorn"
optional = true
python-versions = "*"
version = "0.10.8"
[package.dependencies]
click = ">=7.0.0,<8.0.0"
h11 = ">=0.8.0,<0.9.0"
httptools = "0.0.13"
uvloop = ">=0.14.0"
websockets = ">=8.0.0,<9.0.0"
[[package]]
category = "main"
description = "Fast implementation of asyncio event loop on top of libuv"
marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"pypy\""
name = "uvloop"
optional = true
python-versions = "*"
version = "0.14.0"
[[package]]
category = "main"
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
name = "websockets"
optional = true
python-versions = ">=3.6.1"
version = "8.1"
[[package]]
category = "main"
description = "Yet another URL library"
name = "yarl"
optional = true
python-versions = ">=3.5.3"
version = "1.3.0"
[package.dependencies]
idna = ">=2.0"
multidict = ">=4.0"
[[package]]
category = "main"
description = "YouTube video downloader"
name = "youtube-dl"
optional = true
python-versions = "*"
version = "2019.11.5"
[extras]
alchemy_easy = ["sqlalchemy", "psycopg2_binary"]
alchemy_hard = ["sqlalchemy", "psycopg2"]
bard = ["ffmpeg_python", "youtube_dl"]
constellation = ["starlette", "uvicorn"]
discord = ["discord.py", "pynacl"]
sentry = ["sentry_sdk"]
telegram = ["python_telegram_bot"]
[metadata]
content-hash = "d101c51ae28aea2b4692767328e42026950bd3f920fdf6f6afca76bac40e41df"
python-versions = "^3.8"
[metadata.files]
aiohttp = [
{file = "aiohttp-3.5.4-cp35-cp35m-macosx_10_10_x86_64.whl", hash = "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5"},
{file = "aiohttp-3.5.4-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed"},
{file = "aiohttp-3.5.4-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303"},
{file = "aiohttp-3.5.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10"},
{file = "aiohttp-3.5.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa"},
{file = "aiohttp-3.5.4-cp35-cp35m-win32.whl", hash = "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4"},
{file = "aiohttp-3.5.4-cp35-cp35m-win_amd64.whl", hash = "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72"},
{file = "aiohttp-3.5.4-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5"},
{file = "aiohttp-3.5.4-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12"},
{file = "aiohttp-3.5.4-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6"},
{file = "aiohttp-3.5.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6"},
{file = "aiohttp-3.5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d"},
{file = "aiohttp-3.5.4-cp36-cp36m-win32.whl", hash = "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1"},
{file = "aiohttp-3.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d"},
{file = "aiohttp-3.5.4-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300"},
{file = "aiohttp-3.5.4-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390"},
{file = "aiohttp-3.5.4-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366"},
{file = "aiohttp-3.5.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"},
{file = "aiohttp-3.5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55"},
{file = "aiohttp-3.5.4-cp37-cp37m-win32.whl", hash = "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc"},
{file = "aiohttp-3.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939"},
{file = "aiohttp-3.5.4.tar.gz", hash = "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf"},
]
async-timeout = [
{file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
{file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"},
]
attrs = [
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
{file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
]
certifi = [
{file = "certifi-2019.9.11-py2.py3-none-any.whl", hash = "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"},
{file = "certifi-2019.9.11.tar.gz", hash = "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50"},
]
cffi = [
{file = "cffi-1.13.2-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43"},
{file = "cffi-1.13.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396"},
{file = "cffi-1.13.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54"},
{file = "cffi-1.13.2-cp27-cp27m-win32.whl", hash = "sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159"},
{file = "cffi-1.13.2-cp27-cp27m-win_amd64.whl", hash = "sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97"},
{file = "cffi-1.13.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579"},
{file = "cffi-1.13.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc"},
{file = "cffi-1.13.2-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f"},
{file = "cffi-1.13.2-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858"},
{file = "cffi-1.13.2-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42"},
{file = "cffi-1.13.2-cp34-cp34m-win32.whl", hash = "sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b"},
{file = "cffi-1.13.2-cp34-cp34m-win_amd64.whl", hash = "sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20"},
{file = "cffi-1.13.2-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3"},
{file = "cffi-1.13.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25"},
{file = "cffi-1.13.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5"},
{file = "cffi-1.13.2-cp35-cp35m-win32.whl", hash = "sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c"},
{file = "cffi-1.13.2-cp35-cp35m-win_amd64.whl", hash = "sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b"},
{file = "cffi-1.13.2-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04"},
{file = "cffi-1.13.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652"},
{file = "cffi-1.13.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57"},
{file = "cffi-1.13.2-cp36-cp36m-win32.whl", hash = "sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e"},
{file = "cffi-1.13.2-cp36-cp36m-win_amd64.whl", hash = "sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"},
{file = "cffi-1.13.2-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410"},
{file = "cffi-1.13.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a"},
{file = "cffi-1.13.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12"},
{file = "cffi-1.13.2-cp37-cp37m-win32.whl", hash = "sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e"},
{file = "cffi-1.13.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a"},
{file = "cffi-1.13.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d"},
{file = "cffi-1.13.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3"},
{file = "cffi-1.13.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db"},
{file = "cffi-1.13.2-cp38-cp38-win32.whl", hash = "sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506"},
{file = "cffi-1.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba"},
{file = "cffi-1.13.2.tar.gz", hash = "sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346"},
]
chardet = [
{file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
{file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
]
click = [
{file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"},
{file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"},
]
cryptography = [
{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-manylinux2010_x86_64.whl", hash = "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad"},
{file = "cryptography-2.8-cp27-cp27m-win32.whl", hash = "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2"},
{file = "cryptography-2.8-cp27-cp27m-win_amd64.whl", hash = "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912"},
{file = "cryptography-2.8-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d"},
{file = "cryptography-2.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42"},
{file = "cryptography-2.8-cp34-abi3-macosx_10_6_intel.whl", hash = "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879"},
{file = "cryptography-2.8-cp34-abi3-manylinux1_x86_64.whl", hash = "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d"},
{file = "cryptography-2.8-cp34-abi3-manylinux2010_x86_64.whl", hash = "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9"},
{file = "cryptography-2.8-cp34-cp34m-win32.whl", hash = "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c"},
{file = "cryptography-2.8-cp34-cp34m-win_amd64.whl", hash = "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0"},
{file = "cryptography-2.8-cp35-cp35m-win32.whl", hash = "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf"},
{file = "cryptography-2.8-cp35-cp35m-win_amd64.whl", hash = "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793"},
{file = "cryptography-2.8-cp36-cp36m-win32.whl", hash = "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595"},
{file = "cryptography-2.8-cp36-cp36m-win_amd64.whl", hash = "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7"},
{file = "cryptography-2.8-cp37-cp37m-win32.whl", hash = "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff"},
{file = "cryptography-2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f"},
{file = "cryptography-2.8-cp38-cp38-win32.whl", hash = "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e"},
{file = "cryptography-2.8-cp38-cp38-win_amd64.whl", hash = "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13"},
{file = "cryptography-2.8.tar.gz", hash = "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651"},
]
dateparser = [
{file = "dateparser-0.7.2-py2.py3-none-any.whl", hash = "sha256:983d84b5e3861cb0aa240cad07f12899bb10b62328aae188b9007e04ce37d665"},
{file = "dateparser-0.7.2.tar.gz", hash = "sha256:e1eac8ef28de69a554d5fcdb60b172d526d61924b1a40afbbb08df459a36006b"},
]
"discord.py" = []
ffmpeg-python = [
{file = "ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127"},
{file = "ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"},
]
future = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
]
h11 = [
{file = "h11-0.8.1-py2.py3-none-any.whl", hash = "sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"},
{file = "h11-0.8.1.tar.gz", hash = "sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208"},
]
httptools = [
{file = "httptools-0.0.13.tar.gz", hash = "sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc"},
]
idna = [
{file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"},
{file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"},
]
multidict = [
{file = "multidict-4.5.2-cp34-cp34m-macosx_10_12_intel.macosx_10_12_x86_64.macosx_10_13_intel.macosx_10_13_x86_64.whl", hash = "sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73"},
{file = "multidict-4.5.2-cp34-cp34m-macosx_10_6_intel.macosx_10_6_x86_64.macosx_10_7_intel.macosx_10_7_x86_64.macosx_10_8_intel.macosx_10_8_x86_64.whl", hash = "sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5"},
{file = "multidict-4.5.2-cp34-cp34m-macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.macosx_10_11_intel.macosx_10_11_x86_64.whl", hash = "sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d"},
{file = "multidict-4.5.2-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0"},
{file = "multidict-4.5.2-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351"},
{file = "multidict-4.5.2-cp34-cp34m-win32.whl", hash = "sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a"},
{file = "multidict-4.5.2-cp34-cp34m-win_amd64.whl", hash = "sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9"},
{file = "multidict-4.5.2-cp35-cp35m-macosx_10_12_intel.macosx_10_12_x86_64.macosx_10_13_intel.macosx_10_13_x86_64.whl", hash = "sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3"},
{file = "multidict-4.5.2-cp35-cp35m-macosx_10_6_intel.macosx_10_6_x86_64.macosx_10_7_intel.macosx_10_7_x86_64.macosx_10_8_intel.macosx_10_8_x86_64.whl", hash = "sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d"},
{file = "multidict-4.5.2-cp35-cp35m-macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.macosx_10_11_intel.macosx_10_11_x86_64.whl", hash = "sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036"},
{file = "multidict-4.5.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3"},
{file = "multidict-4.5.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1"},
{file = "multidict-4.5.2-cp35-cp35m-win32.whl", hash = "sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941"},
{file = "multidict-4.5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3"},
{file = "multidict-4.5.2-cp36-cp36m-macosx_10_12_intel.macosx_10_12_x86_64.macosx_10_13_intel.macosx_10_13_x86_64.whl", hash = "sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"},
{file = "multidict-4.5.2-cp36-cp36m-macosx_10_6_intel.macosx_10_6_x86_64.macosx_10_7_intel.macosx_10_7_x86_64.macosx_10_8_intel.macosx_10_8_x86_64.whl", hash = "sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a"},
{file = "multidict-4.5.2-cp36-cp36m-macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.macosx_10_11_intel.macosx_10_11_x86_64.whl", hash = "sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a"},
{file = "multidict-4.5.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b"},
{file = "multidict-4.5.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc"},
{file = "multidict-4.5.2-cp36-cp36m-win32.whl", hash = "sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b"},
{file = "multidict-4.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7"},
{file = "multidict-4.5.2-cp37-cp37m-macosx_10_12_intel.macosx_10_12_x86_64.macosx_10_13_intel.macosx_10_13_x86_64.whl", hash = "sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd"},
{file = "multidict-4.5.2-cp37-cp37m-macosx_10_6_intel.macosx_10_6_x86_64.macosx_10_7_intel.macosx_10_7_x86_64.macosx_10_8_intel.macosx_10_8_x86_64.whl", hash = "sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7"},
{file = "multidict-4.5.2-cp37-cp37m-macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.macosx_10_11_intel.macosx_10_11_x86_64.whl", hash = "sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0"},
{file = "multidict-4.5.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014"},
{file = "multidict-4.5.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce"},
{file = "multidict-4.5.2-cp37-cp37m-win32.whl", hash = "sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef"},
{file = "multidict-4.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1"},
{file = "multidict-4.5.2.tar.gz", hash = "sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f"},
]
psycopg2 = [
{file = "psycopg2-2.8.4-cp27-cp27m-win32.whl", hash = "sha256:72772181d9bad1fa349792a1e7384dde56742c14af2b9986013eb94a240f005b"},
{file = "psycopg2-2.8.4-cp27-cp27m-win_amd64.whl", hash = "sha256:893c11064b347b24ecdd277a094413e1954f8a4e8cdaf7ffbe7ca3db87c103f0"},
{file = "psycopg2-2.8.4-cp34-cp34m-win32.whl", hash = "sha256:9ab75e0b2820880ae24b7136c4d230383e07db014456a476d096591172569c38"},
{file = "psycopg2-2.8.4-cp34-cp34m-win_amd64.whl", hash = "sha256:b0845e3bdd4aa18dc2f9b6fb78fbd3d9d371ad167fd6d1b7ad01c0a6cdad4fc6"},
{file = "psycopg2-2.8.4-cp35-cp35m-win32.whl", hash = "sha256:ef6df7e14698e79c59c7ee7cf94cd62e5b869db369ed4b1b8f7b729ea825712a"},
{file = "psycopg2-2.8.4-cp35-cp35m-win_amd64.whl", hash = "sha256:965c4c93e33e6984d8031f74e51227bd755376a9df6993774fd5b6fb3288b1f4"},
{file = "psycopg2-2.8.4-cp36-cp36m-win32.whl", hash = "sha256:ed686e5926929887e2c7ae0a700e32c6129abb798b4ad2b846e933de21508151"},
{file = "psycopg2-2.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:dca2d7203f0dfce8ea4b3efd668f8ea65cd2b35112638e488a4c12594015f67b"},
{file = "psycopg2-2.8.4-cp37-cp37m-win32.whl", hash = "sha256:8396be6e5ff844282d4d49b81631772f80dabae5658d432202faf101f5283b7c"},
{file = "psycopg2-2.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:47fc642bf6f427805daf52d6e52619fe0637648fe27017062d898f3bf891419d"},
{file = "psycopg2-2.8.4-cp38-cp38-win32.whl", hash = "sha256:4212ca404c4445dc5746c0d68db27d2cbfb87b523fe233dc84ecd24062e35677"},
{file = "psycopg2-2.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:92a07dfd4d7c325dd177548c4134052d4842222833576c8391aab6f74038fc3f"},
{file = "psycopg2-2.8.4.tar.gz", hash = "sha256:f898e5cc0a662a9e12bde6f931263a1bbd350cfb18e1d5336a12927851825bb6"},
]
psycopg2-binary = [
{file = "psycopg2-binary-2.8.4.tar.gz", hash = "sha256:3a2522b1d9178575acee4adf8fd9f979f9c0449b00b4164bb63c3475ea6528ed"},
{file = "psycopg2_binary-2.8.4-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:890167d5091279a27e2505ff0e1fb273f8c48c41d35c5b92adbf4af80e6b2ed6"},
{file = "psycopg2_binary-2.8.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:dbc5cd56fff1a6152ca59445178652756f4e509f672e49ccdf3d79c1043113a4"},
{file = "psycopg2_binary-2.8.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:7f42a8490c4fe854325504ce7a6e4796b207960dabb2cbafe3c3959cb00d1d7e"},
{file = "psycopg2_binary-2.8.4-cp27-cp27m-win32.whl", hash = "sha256:8578d6b8192e4c805e85f187bc530d0f52ba86c39172e61cd51f68fddd648103"},
{file = "psycopg2_binary-2.8.4-cp27-cp27m-win_amd64.whl", hash = "sha256:5dd90c5438b4f935c9d01fcbad3620253da89d19c1f5fca9158646407ed7df35"},
{file = "psycopg2_binary-2.8.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9aadff9032e967865f9778485571e93908d27dab21d0fdfdec0ca779bb6f8ad9"},
{file = "psycopg2_binary-2.8.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:659c815b5b8e2a55193ede2795c1e2349b8011497310bb936da7d4745652823b"},
{file = "psycopg2_binary-2.8.4-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:2166e770cb98f02ed5ee2b0b569d40db26788e0bf2ec3ae1a0d864ea6f1d8309"},
{file = "psycopg2_binary-2.8.4-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:7e6e3c52e6732c219c07bd97fff6c088f8df4dae3b79752ee3a817e6f32e177e"},
{file = "psycopg2_binary-2.8.4-cp34-cp34m-win32.whl", hash = "sha256:040234f8a4a8dfd692662a8308d78f63f31a97e1c42d2480e5e6810c48966a29"},
{file = "psycopg2_binary-2.8.4-cp34-cp34m-win_amd64.whl", hash = "sha256:69b13fdf12878b10dc6003acc8d0abf3ad93e79813fd5f3812497c1c9fb9be49"},
{file = "psycopg2_binary-2.8.4-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:19dc39616850342a2a6db70559af55b22955f86667b5f652f40c0e99253d9881"},
{file = "psycopg2_binary-2.8.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:9f24f383a298a0c0f9b3113b982e21751a8ecde6615494a3f1470eb4a9d70e9e"},
{file = "psycopg2_binary-2.8.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:eaed1c65f461a959284649e37b5051224f4db6ebdc84e40b5e65f2986f101a08"},
{file = "psycopg2_binary-2.8.4-cp35-cp35m-win32.whl", hash = "sha256:4c6717962247445b4f9e21c962ea61d2e884fc17df5ddf5e35863b016f8a1f03"},
{file = "psycopg2_binary-2.8.4-cp35-cp35m-win_amd64.whl", hash = "sha256:84156313f258eafff716b2961644a4483a9be44a5d43551d554844d15d4d224e"},
{file = "psycopg2_binary-2.8.4-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:3b5deaa3ee7180585a296af33e14c9b18c218d148e735c7accf78130765a47e3"},
{file = "psycopg2_binary-2.8.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5057669b6a66aa9ca118a2a860159f0ee3acf837eda937bdd2a64f3431361a2d"},
{file = "psycopg2_binary-2.8.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:afd96845e12638d2c44d213d4810a08f4dc4a563f9a98204b7428e567014b1cd"},
{file = "psycopg2_binary-2.8.4-cp36-cp36m-win32.whl", hash = "sha256:a73021b44813b5c84eda4a3af5826dd72356a900bac9bd9dd1f0f81ee1c22c2f"},
{file = "psycopg2_binary-2.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:407af6d7e46593415f216c7f56ba087a9a42bd6dc2ecb86028760aa45b802bd7"},
{file = "psycopg2_binary-2.8.4-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:3aa773580f85a28ffdf6f862e59cb5a3cc7ef6885121f2de3fca8d6ada4dbf3b"},
{file = "psycopg2_binary-2.8.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:eac8a3499754790187bb00574ab980df13e754777d346f85e0ff6df929bcd964"},
{file = "psycopg2_binary-2.8.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7a1cb80e35e1ccea3e11a48afe65d38744a0e0bde88795cc56a4d05b6e4f9d70"},
{file = "psycopg2_binary-2.8.4-cp37-cp37m-win32.whl", hash = "sha256:086f7e89ec85a6704db51f68f0dcae432eff9300809723a6e8782c41c2f48e03"},
{file = "psycopg2_binary-2.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:b73ddf033d8cd4cc9dfed6324b1ad2a89ba52c410ef6877998422fcb9c23e3a8"},
{file = "psycopg2_binary-2.8.4-cp38-cp38-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:4c3c09fb674401f630626310bcaf6cd6285daf0d5e4c26d6e55ca26a2734e39b"},
{file = "psycopg2_binary-2.8.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:18ca813fdb17bc1db73fe61b196b05dd1ca2165b884dd5ec5568877cabf9b039"},
{file = "psycopg2_binary-2.8.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:50446fae5681fc99f87e505d4e77c9407e683ab60c555ec302f9ac9bffa61103"},
{file = "psycopg2_binary-2.8.4-cp38-cp38-win32.whl", hash = "sha256:98e10634792ac0e9e7a92a76b4991b44c2325d3e7798270a808407355e7bb0a1"},
{file = "psycopg2_binary-2.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:b8f490f5fad1767a1331df1259763b3bad7d7af12a75b950c2843ba319b2415f"},
]
pycparser = [
{file = "pycparser-2.19.tar.gz", hash = "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"},
]
pynacl = [
{file = "PyNaCl-1.3.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621"},
{file = "PyNaCl-1.3.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39"},
{file = "PyNaCl-1.3.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255"},
{file = "PyNaCl-1.3.0-cp27-cp27m-win32.whl", hash = "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f"},
{file = "PyNaCl-1.3.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0"},
{file = "PyNaCl-1.3.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1"},
{file = "PyNaCl-1.3.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e"},
{file = "PyNaCl-1.3.0-cp34-abi3-macosx_10_6_intel.whl", hash = "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1"},
{file = "PyNaCl-1.3.0-cp34-abi3-manylinux1_i686.whl", hash = "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786"},
{file = "PyNaCl-1.3.0-cp34-abi3-manylinux1_x86_64.whl", hash = "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415"},
{file = "PyNaCl-1.3.0-cp34-cp34m-win32.whl", hash = "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b"},
{file = "PyNaCl-1.3.0-cp34-cp34m-win_amd64.whl", hash = "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae"},
{file = "PyNaCl-1.3.0-cp35-cp35m-win32.whl", hash = "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310"},
{file = "PyNaCl-1.3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a"},
{file = "PyNaCl-1.3.0-cp36-cp36m-win32.whl", hash = "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20"},
{file = "PyNaCl-1.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b"},
{file = "PyNaCl-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56"},
{file = "PyNaCl-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715"},
{file = "PyNaCl-1.3.0-cp38-cp38-win32.whl", hash = "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5"},
{file = "PyNaCl-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92"},
{file = "PyNaCl-1.3.0.tar.gz", hash = "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c"},
]
python-dateutil = [
{file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
{file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
]
python-telegram-bot = [
{file = "python-telegram-bot-12.2.0.tar.gz", hash = "sha256:346d42771c2b23384c59f5f41e05bd7e801a0ce118d8dcb95209bb73d5f694c5"},
{file = "python_telegram_bot-12.2.0-py2.py3-none-any.whl", hash = "sha256:3beee89cba3bc3217566c96199f04776dd25f541ac8992da27fd247b2d208a14"},
]
pytz = [
{file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"},
{file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"},
]
regex = [
{file = "regex-2019.11.1-cp27-none-win32.whl", hash = "sha256:604dc563a02a74d70ae1f55208ddc9bfb6d9f470f6d1a5054c4bd5ae58744ab1"},
{file = "regex-2019.11.1-cp27-none-win_amd64.whl", hash = "sha256:5e00f65cc507d13ab4dfa92c1232d004fa202c1d43a32a13940ab8a5afe2fb96"},
{file = "regex-2019.11.1-cp35-none-win32.whl", hash = "sha256:15454b37c5a278f46f7aa2d9339bda450c300617ca2fca6558d05d870245edc7"},
{file = "regex-2019.11.1-cp35-none-win_amd64.whl", hash = "sha256:d2b302f8cdd82c8f48e9de749d1d17f85ce9a0f082880b9a4859f66b07037dc6"},
{file = "regex-2019.11.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b4e0406d822aa4993ac45072a584d57aa4931cf8288b5455bbf30c1d59dbad59"},
{file = "regex-2019.11.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:7faf534c1841c09d8fefa60ccde7b9903c9b528853ecf41628689793290ca143"},
{file = "regex-2019.11.1-cp36-none-win32.whl", hash = "sha256:7caf47e4a9ac6ef08cabd3442cc4ca3386db141fb3c8b2a7e202d0470028e910"},
{file = "regex-2019.11.1-cp36-none-win_amd64.whl", hash = "sha256:e3d8dd0ec0ea280cf89026b0898971f5750a7bd92cb62c51af5a52abd020054a"},
{file = "regex-2019.11.1-cp37-none-win32.whl", hash = "sha256:c31eaf28c6fe75ea329add0022efeed249e37861c19681960f99bbc7db981fb2"},
{file = "regex-2019.11.1-cp37-none-win_amd64.whl", hash = "sha256:1ad40708c255943a227e778b022c6497c129ad614bb7a2a2f916e12e8a359ee7"},
{file = "regex-2019.11.1-cp38-none-win32.whl", hash = "sha256:ec032cbfed59bd5a4b8eab943c310acfaaa81394e14f44454ad5c9eba4f24a74"},
{file = "regex-2019.11.1-cp38-none-win_amd64.whl", hash = "sha256:c7393597191fc2043c744db021643549061e12abe0b3ff5c429d806de7b93b66"},
{file = "regex-2019.11.1.tar.gz", hash = "sha256:720e34a539a76a1fedcebe4397290604cc2bdf6f81eca44adb9fb2ea071c0c69"},
]
sentry-sdk = [
{file = "sentry-sdk-0.13.2.tar.gz", hash = "sha256:ff1fa7fb85703ae9414c8b427ee73f8363232767c9cd19158f08f6e4f0b58fc7"},
{file = "sentry_sdk-0.13.2-py2.py3-none-any.whl", hash = "sha256:09e1e8f00f22ea580348f83bbbd880adf40b29f1dec494a8e4b33e22f77184fb"},
]
six = [
{file = "six-1.13.0-py2.py3-none-any.whl", hash = "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd"},
{file = "six-1.13.0.tar.gz", hash = "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"},
]
sqlalchemy = [
{file = "SQLAlchemy-1.3.11.tar.gz", hash = "sha256:afa5541e9dea8ad0014251bc9d56171ca3d8b130c9627c6cb3681cff30be3f8a"},
]
starlette = [
{file = "starlette-0.12.13.tar.gz", hash = "sha256:9597bc28e3c4659107c1c4a45ec32dc45e947d78fe56230222be673b2c36454a"},
]
tornado = [
{file = "tornado-6.0.3-cp35-cp35m-win32.whl", hash = "sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"},
{file = "tornado-6.0.3-cp35-cp35m-win_amd64.whl", hash = "sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60"},
{file = "tornado-6.0.3-cp36-cp36m-win32.whl", hash = "sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281"},
{file = "tornado-6.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c"},
{file = "tornado-6.0.3-cp37-cp37m-win32.whl", hash = "sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5"},
{file = "tornado-6.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7"},
{file = "tornado-6.0.3.tar.gz", hash = "sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9"},
]
tzlocal = [
{file = "tzlocal-2.0.0-py2.py3-none-any.whl", hash = "sha256:11c9f16e0a633b4b60e1eede97d8a46340d042e67b670b290ca526576e039048"},
{file = "tzlocal-2.0.0.tar.gz", hash = "sha256:949b9dd5ba4be17190a80c0268167d7e6c92c62b30026cf9764caf3e308e5590"},
]
urllib3 = [
{file = "urllib3-1.25.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"},
{file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"},
]
uvicorn = [
{file = "uvicorn-0.10.8.tar.gz", hash = "sha256:f4c34642618449f55e2bab8c6b22ff7615b520d2e7e23275be2ca894254327a3"},
]
uvloop = [
{file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"},
{file = "uvloop-0.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726"},
{file = "uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7"},
{file = "uvloop-0.14.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"},
{file = "uvloop-0.14.0-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891"},
{file = "uvloop-0.14.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95"},
{file = "uvloop-0.14.0-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5"},
{file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"},
{file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"},
]
websockets = [
{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_x86_64.whl", hash = "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8"},
{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-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_x86_64.whl", hash = "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779"},
{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.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"},
]
yarl = [
{file = "yarl-1.3.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320"},
{file = "yarl-1.3.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb"},
{file = "yarl-1.3.0-cp35-cp35m-win32.whl", hash = "sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829"},
{file = "yarl-1.3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310"},
{file = "yarl-1.3.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f"},
{file = "yarl-1.3.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842"},
{file = "yarl-1.3.0-cp36-cp36m-win32.whl", hash = "sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8"},
{file = "yarl-1.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4"},
{file = "yarl-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"},
{file = "yarl-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0"},
{file = "yarl-1.3.0.tar.gz", hash = "sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9"},
]
youtube-dl = [
{file = "youtube_dl-2019.11.5-py2.py3-none-any.whl", hash = "sha256:1314de17f0d41c0f1062c4942406b8e0558d14d44b32f9fce00272760a06455b"},
{file = "youtube_dl-2019.11.5.tar.gz", hash = "sha256:25324aab78df9a09b2ee34f642f116933134bc66ea629a778c1fffe05b66f733"},
]

View file

@ -1,3 +1,5 @@
# Remember to run `poetry update` after you edit this file!
[tool.poetry]
name = "royalnet"
version = "5.1a1"
@ -18,30 +20,41 @@
[tool.poetry.dependencies]
python = "^3.8"
dateparser = "^0.7.2"
# telegram
python_telegram_bot = {version="^12.2.0", optional=true}
discord_py = {git = "https://github.com/Rapptz/discord.py", optional=true} # discord.py 1.2.4 is missing Go Live related methods
# discord
"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
# bard
ffmpeg_python = {version="~0.2.0", optional=true}
youtube_dl = {version="*", optional=true}
# alchemy
sqlalchemy = {version="^1.3.10", optional=true}
psycopg2 = {version="^2.8.4", optional=true} # Requires quite a bit of stuff http://initd.org/psycopg/docs/install.html#install-from-source
psycopg2_binary = {version="^2.8.4", optional=true} # Prebuilt alternative to psycopg2, not recommended
# constellation
starlette = {version="^0.12.13", optional=true}
uvicorn = {version="^0.10.7", optional=true}
# sentry
sentry_sdk = {version="~0.13.2", optional=true}
# herald
websockets = {version="^8.1", optional=true}
# Development dependencies
[tool.poetry.dev-dependencies]
pytest = "^5.2.1"
# Optional dependencies
[tool.poetry.extras]
telegram = ["python_telegram_bot"]
discord = ["discord_py", "pynacl"]
discord = ["discord.py", "pynacl"]
alchemy_easy = ["sqlalchemy", "psycopg2_binary"]
alchemy_hard = ["sqlalchemy", "psycopg2"]
bard = ["ffmpeg_python", "youtube_dl"]
constellation = ["starlette"]
constellation = ["starlette", "uvicorn"]
sentry = ["sentry_sdk"]
herald = ["websockets"]
# Development dependencies
[tool.poetry.dev-dependencies]
# There are none
[build-system]
requires = ["poetry>=0.12"]

View file

@ -1,4 +1,4 @@
from typing import Set, Dict, Union, Optional
from typing import Set, Dict, Union, Type
from sqlalchemy import create_engine
from sqlalchemy.engine import Engine
from sqlalchemy.schema import Table
@ -28,15 +28,17 @@ class Alchemy:
self._engine: Engine = create_engine(database_uri)
self._Base: DeclarativeMeta = declarative_base(bind=self._engine)
self._Session: sessionmaker = sessionmaker(bind=self._engine)
self._tables: Dict[str, Table] = {}
self._tables: Dict[str, Type[Table]] = {}
for table in tables:
name = table.__name__
assert self._tables.get(name) is None
assert isinstance(name, str)
self._tables[name] = type(name, (self._Base, table), {})
# noinspection PyTypeChecker
bound_table: Type[Table] = type(name, (self._Base, table), {})
self._tables[name] = bound_table
self._Base.metadata.create_all()
def get(self, table: Union[str, type]) -> Optional[Table]:
def get(self, table: Union[str, type]) -> Type[Table]:
"""Get the table with a specified name or class.
Args:

View file

@ -1,4 +1,3 @@
class AlchemyException(Exception):
"""Base class for Alchemy exceptions."""

View file

@ -1,9 +1,9 @@
from typing import Optional
from typing import Type
from sqlalchemy.inspection import inspect
from sqlalchemy.schema import Table
def table_dfs(starting_table: Table, ending_table: Table) -> tuple:
def table_dfs(starting_table: Type[Table], ending_table: Type[Table]) -> tuple:
"""Depth-first-search for the path from the starting table to the ending table.
Returns:

View file

@ -2,7 +2,7 @@ from .commandinterface import CommandInterface
from .command import Command
from .commanddata import CommandData
from .commandargs import CommandArgs
from .commanderrors import CommandError, InvalidInputError, UnsupportedError, KeyboardExpiredError, ConfigurationError
from .errors import CommandError, InvalidInputError, UnsupportedError, KeyboardExpiredError, ConfigurationError
__all__ = [
"CommandInterface",

View file

@ -1,16 +1,33 @@
import re
from typing import Pattern, AnyStr, Optional, Sequence, Union
from .commanderrors import InvalidInputError
from .errors import InvalidInputError
class CommandArgs(list):
"""An interface to access the arguments of a command with ease."""
"""An interface to easily access the arguments of a command.
Inherits from :class:`list`."""
def __getitem__(self, item):
"""Arguments can be accessed with an array notation, such as ``args[0]``.
"""Access arguments as if they were a :class:`list`.
Raises:
royalnet.error.InvalidInputError: if the requested argument does not exist."""
royalnet.error.InvalidInputError: if the requested argument does not exist.
Examples:
::
# /pasta spaghetti aldente
>>> self[0]
"spaghetti"
>>> self[1]
"aldente"
>>> self[2]
# InvalidInputError: Missing argument #3.
>>> self[0:2]
["spaghetti", "aldente"]
"""
if isinstance(item, int):
try:
return super().__getitem__(item)
@ -33,7 +50,18 @@ class CommandArgs(list):
royalnet.error.InvalidInputError: if there are less than ``require_at_least`` arguments.
Returns:
The space-joined string."""
The space-joined string.
Examples:
::
# /pasta spaghetti aldente
>>> self.joined()
"spaghetti aldente"
>>> self.joined(require_at_least=3)
# InvalidInputError: Not enough arguments specified (minimum is 3).
"""
if len(self) < require_at_least:
raise InvalidInputError(f"Not enough arguments specified (minimum is {require_at_least}).")
return " ".join(self)
@ -63,7 +91,20 @@ class CommandArgs(list):
default: The value returned if the argument is missing.
Returns:
Either the argument or the ``default`` value, defaulting to ``None``."""
Either the argument or the ``default`` value, defaulting to ``None``.
Examples:
::
# /pasta spaghetti aldente
>>> self.optional(0)
"spaghetti"
>>> self.optional(2)
None
>>> self.optional(2, default="carbonara")
"carbonara"
"""
try:
return self[index]
except InvalidInputError:

View file

@ -1,6 +1,6 @@
from typing import Dict, Callable
import warnings
from .commanderrors import UnsupportedError
from .errors import UnsupportedError
from .commandinterface import CommandInterface
from ..utils import asyncify

View file

@ -1,10 +1,10 @@
import typing
import asyncio
from .commanderrors import UnsupportedError
from .errors import UnsupportedError
if typing.TYPE_CHECKING:
from .command import Command
from ..alchemy import Alchemy
from ..interfaces import GenericBot
from ..serf import GenericBot
class CommandInterface:

View file

@ -0,0 +1,26 @@
from .config import Config
from .errors import HeraldError, ConnectionClosedError, LinkError, InvalidServerResponseError, ServerError
from .link import Link
from .package import Package
from .request import Request
from .response import Response, ResponseSuccess, ResponseFailure
from .server import Server
from .broadcast import Broadcast
__all__ = [
"Config",
"HeraldError",
"ConnectionClosedError",
"LinkError",
"InvalidServerResponseError",
"ServerError",
"Link",
"Package",
"Request",
"Response",
"ResponseSuccess",
"ResponseFailure",
"Server",
"Broadcast",
]

View file

@ -0,0 +1,26 @@
import typing
class Broadcast:
def __init__(self, handler: str, data: dict, msg_type: typing.Optional[str] = None):
super().__init__()
if msg_type is not None:
assert msg_type == self.__class__.__name__
self.msg_type = self.__class__.__name__
self.handler: str = handler
self.data: dict = data
def to_dict(self):
return self.__dict__
@classmethod
def from_dict(cls, d: dict):
return cls(**d)
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.handler == other.handler and self.data == other.data
return False
def __repr__(self):
return f"{self.__class__.__qualname__}(handler={self.handler}, data={self.data})"

34
royalnet/herald/config.py Normal file
View file

@ -0,0 +1,34 @@
class Config:
def __init__(self,
name: str,
address: str,
port: int,
secret: str,
secure: bool = False,
path: str = "/"):
if ":" in name:
raise ValueError("Herald names cannot contain colons (:)")
self.name = name
self.address = address
if port < 0 or port > 65535:
raise ValueError("No such port")
self.port = port
self.secure = secure
if ":" in secret:
raise ValueError("Herald secrets cannot contain colons (:)")
self.secret = secret
if not path.startswith("/"):
raise ValueError("Herald paths must start with a slash (/)")
self.path = path
@property
def url(self):
return f"ws{'s' if self.secure else ''}://{self.address}:{self.port}{self.path}"
def __repr__(self):
return f"<HeraldConfig for {self.url}>"

18
royalnet/herald/errors.py Normal file
View file

@ -0,0 +1,18 @@
class HeraldError(Exception):
"""A generic :py:mod:`royalherald` error."""
class LinkError(HeraldError):
"""An error for something that happened in a :py:class:`Link`."""
class ServerError(HeraldError):
"""An error for something that happened in a :py:class:`Server`."""
class ConnectionClosedError(LinkError):
"""The :py:class:`Link`'s connection was closed unexpectedly. The link can't be used anymore."""
class InvalidServerResponseError(LinkError):
"""The :py:class:`Server` sent invalid data to the :py:class:`Link`."""

179
royalnet/herald/link.py Normal file
View file

@ -0,0 +1,179 @@
import asyncio
import websockets
import uuid
import functools
import logging as _logging
import typing
from .package import Package
from .request import Request
from .response import Response, ResponseSuccess, ResponseFailure
from .broadcast import Broadcast
from .errors import ConnectionClosedError, InvalidServerResponseError
from .config import Config
log = _logging.getLogger(__name__)
class PendingRequest:
def __init__(self, *, loop: asyncio.AbstractEventLoop = None):
if loop is None:
self.loop = asyncio.get_event_loop()
else:
self.loop = loop
self.event: asyncio.Event = asyncio.Event(loop=loop)
self.data: typing.Optional[dict] = None
def __repr__(self):
if self.event.is_set():
return f"<{self.__class__.__qualname__}: {self.data.__class__.__name__}>"
return f"<{self.__class__.__qualname__}>"
def set(self, data):
self.data = data
self.event.set()
def requires_connection(func):
@functools.wraps(func)
async def new_func(self, *args, **kwargs):
await self.connect_event.wait()
return await func(self, *args, **kwargs)
return new_func
def requires_identification(func):
@functools.wraps(func)
async def new_func(self, *args, **kwargs):
await self.identify_event.wait()
return await func(self, *args, **kwargs)
return new_func
class Link:
def __init__(self, config: Config, request_handler, *,
loop: asyncio.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] = {}
if loop is None:
self._loop = asyncio.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)
def __repr__(self):
if self.identify_event.is_set():
return f"<{self.__class__.__qualname__} (identified)>"
elif self.connect_event.is_set():
return f"<{self.__class__.__qualname__} (connected)>"
elif self.error_event.is_set():
return f"<{self.__class__.__qualname__} (error)>"
else:
return f"<{self.__class__.__qualname__} (disconnected)>"
async def connect(self):
"""Connect to the :py:class:`royalnet.network.NetworkServer` at ``self.master_uri``."""
log.info(f"Connecting to {self.config.url}...")
self.websocket = await websockets.connect(self.config.url, loop=self._loop)
self.connect_event.set()
log.info(f"Connected!")
@requires_connection
async def receive(self) -> Package:
"""Recieve a :py:class:`Package` from the :py:class:`Server`.
Raises:
:py:exc:`royalnet.network.royalnetlink.ConnectionClosedError` if the connection closes."""
try:
jbytes: bytes = await self.websocket.recv()
package: Package = Package.from_json_bytes(jbytes)
except websockets.ConnectionClosed:
self.error_event.set()
self.connect_event.clear()
self.identify_event.clear()
log.info(f"Connection to {self.config.url} was closed.")
# What to do now? Let's just reraise.
raise ConnectionClosedError()
if self.identify_event.is_set() and package.destination != self.nid:
raise InvalidServerResponseError("Package is not addressed to this NetworkLink.")
log.debug(f"Received package: {package}")
return package
@requires_connection
async def identify(self) -> None:
log.info(f"Identifying...")
await self.websocket.send(f"Identify {self.nid}:{self.config.name}:{self.config.secret}")
response: Package = await self.receive()
if not response.source == "<server>":
raise InvalidServerResponseError("Received a non-service package before identification.")
if "type" not in response.data:
raise InvalidServerResponseError("Missing 'type' in response data")
if response.data["type"] == "error":
raise ConnectionClosedError(f"Identification error: {response.data['type']}")
assert response.data["type"] == "success"
self.identify_event.set()
log.info(f"Identified successfully!")
@requires_identification
async def send(self, package: Package):
await self.websocket.send(package.to_json_bytes())
log.debug(f"Sent package: {package}")
@requires_identification
async def broadcast(self, destination: str, broadcast: Broadcast) -> None:
package = Package(broadcast.to_dict(), source=self.nid, destination=destination)
await self.send(package)
log.debug(f"Sent broadcast: {broadcast}")
@requires_identification
async def request(self, destination: str, request: Request) -> Response:
if destination.startswith("*"):
raise ValueError("requests cannot have multiple destinations")
package = Package(request.to_dict(), source=self.nid, destination=destination)
request = PendingRequest(loop=self._loop)
self._pending_requests[package.source_conv_id] = request
await self.send(package)
log.debug(f"Sent request to {destination}: {request}")
await request.event.wait()
if request.data["type"] == "ResponseSuccess":
response: Response = ResponseSuccess.from_dict(request.data)
elif request.data["type"] == "ResponseFailure":
response: Response = ResponseFailure.from_dict(request.data)
else:
raise TypeError("Unknown response type")
log.debug(f"Received from {destination}: {request} -> {response}")
return response
async def run(self):
"""Blockingly run the Link."""
log.debug(f"Running main client loop for {self.nid}.")
if self.error_event.is_set():
raise ConnectionClosedError("RoyalnetLinks can't be rerun after an error.")
while True:
if not self.connect_event.is_set():
await self.connect()
if not self.identify_event.is_set():
await self.identify()
package: Package = await self.receive()
# Package is a response
if package.destination_conv_id in self._pending_requests:
request = self._pending_requests[package.destination_conv_id]
request.set(package.data)
continue
# Package is a request
elif package.data["msg_type"] == "Request":
log.debug(f"Received request {package.source_conv_id}: {package}")
response: Response = await self.request_handler(Request.from_dict(package.data))
response_package: Package = package.reply(response.to_dict())
await self.send(response_package)
log.debug(f"Replied to request {response_package.source_conv_id}: {response_package}")
# Package is a broadcast
elif package.data["msg_type"] == "Broadcast":
log.debug(f"Received broadcast {package.source_conv_id}: {package}")
await self.request_handler(Broadcast.from_dict(package.data))

113
royalnet/herald/package.py Normal file
View file

@ -0,0 +1,113 @@
import json
import uuid
import typing
class Package:
"""A ``royalherald`` package, the data type with which a :py:class:`Link` communicates with a :py:class:`Server` or
another Link.
Contains info about the source and the destination."""
def __init__(self,
data: dict,
*,
source: str,
destination: str,
source_conv_id: typing.Optional[str] = None,
destination_conv_id: typing.Optional[str] = None):
"""Create a Package.
Parameters:
data: The data that should be sent.
source: The ``nid`` of the node that created this Package.
destination: The ``link_type`` of the destination node, or alternatively, the ``nid`` of the node. Can also be the ``NULL`` value to send the message to nobody.
source_conv_id: The conversation id of the node that created this package. Akin to the sequence number on IP packets.
destination_conv_id: The conversation id of the node that this Package is a reply to."""
# TODO: something is not right in these type hints. Check them.
self.data: dict = data
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
def __repr__(self):
return f"<{self.__class__.__qualname__} {self.source} ({self.source_conv_id}) to {self.destination} ({self.destination_conv_id}>"
def __eq__(self, other):
if isinstance(other, Package):
return (self.data == other.data) and \
(self.source == other.source) and \
(self.destination == other.destination) and \
(self.source_conv_id == other.source_conv_id) and \
(self.destination_conv_id == other.destination_conv_id)
return False
def reply(self, data) -> "Package":
"""Reply to this Package with another Package.
Parameters:
data: The data that should be sent. Usually a :py:class:`royalnet.network.Message`.
Returns:
The reply Package."""
return Package(data,
source=self.destination,
destination=self.source,
source_conv_id=self.destination_conv_id or str(uuid.uuid4()),
destination_conv_id=self.source_conv_id)
@staticmethod
def from_dict(d) -> "Package":
"""Create a Package from a dictionary."""
if "source" not in d:
raise ValueError("Missing source field")
if "nid" not in d["source"]:
raise ValueError("Missing source.nid field")
if "conv_id" not in d["source"]:
raise ValueError("Missing source.conv_id field")
if "destination" not in d:
raise ValueError("Missing destination field")
if "nid" not in d["destination"]:
raise ValueError("Missing destination.nid field")
if "conv_id" not in d["destination"]:
raise ValueError("Missing destination.conv_id field")
if "data" not in d:
raise ValueError("Missing data field")
return Package(d["data"],
source=d["source"]["nid"],
destination=d["destination"]["nid"],
source_conv_id=d["source"]["conv_id"],
destination_conv_id=d["destination"]["conv_id"])
def to_dict(self) -> dict:
"""Convert the Package into a dictionary."""
return {
"source": {
"nid": self.source,
"conv_id": self.source_conv_id
},
"destination": {
"nid": self.destination,
"conv_id": self.destination_conv_id
},
"data": self.data
}
@staticmethod
def from_json_string(string: str) -> "Package":
"""Create a Package from a JSON string."""
return Package.from_dict(json.loads(string))
def to_json_string(self) -> str:
"""Convert the Package into a JSON string."""
return json.dumps(self.to_dict())
@staticmethod
def from_json_bytes(b: bytes) -> "Package":
"""Create a Package from UTF8-encoded JSON bytes."""
return Package.from_json_string(str(b, encoding="utf8"))
def to_json_bytes(self) -> bytes:
"""Convert the Package into UTF8-encoded JSON bytes."""
return bytes(self.to_json_string(), encoding="utf8")

View file

@ -0,0 +1,30 @@
import typing
class Request:
"""A request sent from a :py:class:`Link` to another.
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):
super().__init__()
if msg_type is not None:
assert msg_type == self.__class__.__name__
self.msg_type = self.__class__.__name__
self.handler: str = handler
self.data: dict = data
def to_dict(self):
return self.__dict__
@classmethod
def from_dict(cls, d: dict):
return cls(**d)
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.handler == other.handler and self.data == other.data
return False
def __repr__(self):
return f"{self.__class__.__qualname__}(handler={self.handler}, data={self.data})"

View file

@ -0,0 +1,50 @@
import typing
class Response:
"""A base class to be inherited by all other response types."""
def to_dict(self) -> dict:
"""Prepare the Response to be sent by converting it to a JSONable :py:class:`dict`."""
return {
"type": self.__class__.__name__,
**self.__dict__
}
def __eq__(self, other):
if isinstance(other, Response):
return self.to_dict() == other.to_dict()
return False
@classmethod
def from_dict(cls, d: dict) -> "Response":
"""Recreate the response from a received :py:class:`dict`."""
# Ignore type in dict
del d["type"]
# noinspection PyArgumentList
return cls(**d)
class ResponseSuccess(Response):
"""A response to a successful :py:class:`Request`."""
def __init__(self, data: typing.Optional[dict] = None):
if data is None:
self.data = {}
else:
self.data = data
def __repr__(self):
return f"{self.__class__.__qualname__}(data={self.data})"
class ResponseFailure(Response):
"""A response to a invalid :py:class:`Request`."""
def __init__(self, name: str, description: str, extra_info: typing.Optional[dict] = None):
self.name: str = name
self.description: str = description
self.extra_info: typing.Optional[dict] = extra_info
def __repr__(self):
return f"{self.__class__.__qualname__}(name={self.name}, description={self.description}, extra_info={self.extra_info})"

153
royalnet/herald/server.py Normal file
View file

@ -0,0 +1,153 @@
import typing
import websockets
import re
import datetime
import uuid
import asyncio
import logging as _logging
from .package import Package
from .config import Config
log = _logging.getLogger(__name__)
class ConnectedClient:
"""The :py:class:`Server`-side representation of a connected :py:class:`Link`."""
def __init__(self, socket: websockets.WebSocketServerProtocol):
self.socket: websockets.WebSocketServerProtocol = socket
self.nid: typing.Optional[str] = None
self.link_type: typing.Optional[str] = None
self.connection_datetime: datetime.datetime = datetime.datetime.now()
def __repr__(self):
return f"<{self.__class__.__qualname__} {self.nid}>"
@property
def is_identified(self) -> bool:
"""Has the client sent a valid identification package?"""
return bool(self.nid)
async def send_service(self, msg_type: str, message: str):
await self.send(Package({"type": msg_type, "service": message},
source="<server>",
destination=self.nid))
async def send(self, package: Package):
"""Send a :py:class:`Package` to the :py:class:`Link`."""
await self.socket.send(package.to_json_bytes())
class Server:
def __init__(self, config: Config, *, loop: asyncio.AbstractEventLoop = None):
self.config: Config = config
self.identified_clients: typing.List[ConnectedClient] = []
self.loop = loop
def __repr__(self):
return f"<{self.__class__.__qualname__}>"
def find_client(self, *, nid: str = None, link_type: str = None) -> typing.List[ConnectedClient]:
assert not (nid and link_type)
if nid:
matching = [client for client in self.identified_clients if client.nid == nid]
assert len(matching) <= 1
return matching
if link_type:
matching = [client for client in self.identified_clients if client.link_type == link_type]
return matching or []
async def listener(self, websocket: websockets.server.WebSocketServerProtocol, path):
log.info(f"{websocket.remote_address} connected to the server.")
connected_client = ConnectedClient(websocket)
# Wait for identification
identify_msg = await websocket.recv()
log.debug(f"{websocket.remote_address} identified itself with: {identify_msg}.")
if not isinstance(identify_msg, str):
await connected_client.send_service("error", "Invalid identification message (not a str)")
return
identification = re.match(r"Identify ([^:\s]+):([^:\s]+):([^:\s]+)", identify_msg)
if identification is None:
await connected_client.send_service("error", "Invalid identification message (regex failed)")
return
secret = identification.group(3)
if secret != self.config.secret:
await connected_client.send_service("error", "Invalid secret")
return
# Identification successful
connected_client.nid = identification.group(1)
connected_client.link_type = identification.group(2)
self.identified_clients.append(connected_client)
log.debug(f"{websocket.remote_address} identified successfully as {connected_client.nid}"
f" ({connected_client.link_type}).")
await connected_client.send_service("success", "Identification successful!")
log.debug(f"{connected_client.nid}'s identification confirmed.")
# Main loop
while True:
# Receive packages
raw_bytes = await websocket.recv()
package: Package = Package.from_json_bytes(raw_bytes)
log.debug(f"Received package: {package}")
# Check if the package destination is the server itself.
if package.destination == "<server>":
# TODO: do stuff
pass
# Otherwise, route the package to its destination
# noinspection PyAsyncCall
self.loop.create_task(self.route_package(package))
def find_destination(self, package: Package) -> typing.List[ConnectedClient]:
"""Find a list of destinations for the package.
Parameters:
package: The package to find the destination of.
Returns:
A :py:class:`list` of :py:class:`ConnectedClient` to send the package to."""
# Parse destination
# Is it nothing?
if package.destination == "<none>":
return []
# Is it all possible destinations?
if package.destination == "*":
return self.identified_clients
# Is it a valid nid?
try:
destination = str(uuid.UUID(package.destination))
except ValueError:
pass
else:
return self.find_client(nid=destination)
# Is it a link_type?
return self.find_client(link_type=package.destination)
async def route_package(self, package: Package) -> None:
"""Executed every time a package is received and must be routed somewhere."""
destinations = self.find_destination(package)
log.debug(f"Routing package: {package} -> {destinations}")
for destination in destinations:
# This may have some consequences
specific_package = Package(package.data,
source=package.source,
destination=destination.nid,
source_conv_id=package.source_conv_id,
destination_conv_id=package.destination_conv_id)
await destination.send(specific_package)
def serve(self):
if self.config.secure:
raise Exception("Secure servers aren't supported yet")
log.debug(f"Serving on {self.config.url}")
self.loop.run_until_complete(self.run())
self.loop.run_forever()
async def run(self):
await websockets.serve(self.listener,
host=self.config.address,
port=self.config.port,
loop=self.loop)
def run_blocking(self):
if self.loop is None:
self.loop = asyncio.get_event_loop()
self.serve()

View file

@ -1,6 +1,6 @@
"""Various bot interfaces, and a common class to create new ones."""
from .interface import GenericBot
from .serf import GenericBot
from .telegram import TelegramBot
from .discord import DiscordBot

View file

@ -0,0 +1,12 @@
from typing import Type
from sqlalchemy.schema import Table
class AlchemyConfig:
"""A helper class to configure :class:`Alchemy` in a :class:`Serf`."""
def __init__(self,
master_table: Type[Table],
identity_table: Type[Table],
):
self.master_table: Type[Table] = master_table
self.identity_table: Type[Table] = identity_table

View file

@ -31,7 +31,7 @@ class DiscordBot(GenericBot):
def _interface_factory(self) -> typing.Type[CommandInterface]:
# noinspection PyPep8Naming
GenericInterface = super()._interface_factory()
GenericInterface = super().interface_factory()
# noinspection PyMethodParameters,PyAbstractClass
class DiscordInterface(GenericInterface):

View file

@ -1,86 +1,101 @@
import sys
import asyncio
import logging
from typing import Type, Optional, Awaitable, Dict, List, Any, Callable, Union
import sentry_sdk
import keyring
import royalnet.version
import royalherald as rh
from royalnet.herald import Response, ResponseSuccess, Broadcast, ResponseFailure, Request, Link
from royalnet.herald import Config as HeraldConfig
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from ..utils import *
from ..alchemy import *
from ..commands import *
from ..error import *
from royalnet.commands import Command, CommandInterface, CommandData, CommandError, UnsupportedError
from royalnet.alchemy import Alchemy
from .alchemyconfig import AlchemyConfig
log = logging.getLogger(__name__)
class Interface:
"""A common bot class, to be used as base for the other more specific classes, such as
:py:class:`royalnet.bots.TelegramBot` and :py:class:`royalnet.bots.DiscordBot`. """
class Serf:
"""An abstract class, to be used as base to implement Royalnet bots on multiple interfaces (such as Telegram or
Discord)."""
interface_name = NotImplemented
def _init_commands(self) -> None:
"""Generate the ``packs`` dictionary required to handle incoming messages, and the ``network_handlers``
dictionary required to handle incoming requests. """
log.info(f"Registering packs...")
self._Interface = self._interface_factory()
self._Data = self._data_factory()
self.commands = {}
self.network_handlers: typing.Dict[str, typing.Callable[["Interface", typing.Any],
typing.Awaitable[typing.Optional[typing.Dict]]]] = {}
for SelectedCommand in self.uninitialized_commands:
interface = self._Interface()
try:
command = SelectedCommand(interface)
except Exception as e:
log.error(f"{e.__class__.__qualname__} during the registration of {SelectedCommand.__qualname__}")
sentry_sdk.capture_exception(e)
continue
# Linking the command to the interface
interface.command = command
# Override the main command name, but warn if it's overriding something
if f"{interface.prefix}{SelectedCommand.name}" in self.commands:
log.warning(f"Overriding (already defined): {SelectedCommand.__qualname__} -> {interface.prefix}{SelectedCommand.name}")
else:
log.debug(f"Registering: {SelectedCommand.__qualname__} -> {interface.prefix}{SelectedCommand.name}")
self.commands[f"{interface.prefix}{SelectedCommand.name}"] = command
# Register aliases, but don't override anything
for alias in SelectedCommand.aliases:
if f"{interface.prefix}{alias}" not in self.commands:
log.debug(f"Aliasing: {SelectedCommand.__qualname__} -> {interface.prefix}{alias}")
self.commands[f"{interface.prefix}{alias}"] = self.commands[f"{interface.prefix}{SelectedCommand.name}"]
else:
log.info(f"Ignoring (already defined): {SelectedCommand.__qualname__} -> {interface.prefix}{alias}")
def __init__(self, *,
alchemy_config: Optional[AlchemyConfig] = None,
commands: List[Type[Command]] = None,
network_config: Optional[HeraldConfig] = None,
sentry_dsn: Optional[str] = None,
loop: asyncio.AbstractEventLoop = None,
secrets_name: str = "__default__"):
self.secrets_name = secrets_name
def _interface_factory(self) -> typing.Type[CommandInterface]:
# noinspection PyAbstractClass,PyMethodParameters
if alchemy_config is not None:
self.init_alchemy(alchemy_config)
self.Interface: Type[CommandInterface] = self.interface_factory()
"""The :class:`CommandInterface` class of this Serf."""
self.Data: Type[CommandData] = self.data_factory()
"""The :class:`CommandData` class of this Serf."""
self.commands: Dict[str, Command] = {}
"""The :class:`dict` connecting each command name to its :class:`Command` object."""
if commands is None:
commands = []
self.register_commands(commands)
self.herald_handlers: Dict[str, Callable[["Serf", Any], Awaitable[Optional[dict]]]] = {}
"""A :class:`dict` linking :class:`Request` event names to coroutines returning a :class:`dict` that will be
sent as :class:`Response` to the event."""
self.herald: Optional[Link] = None
"""The :class:`Link` object connecting the Serf to the rest of the herald network."""
self.herald_task: Optional[asyncio.Task] = None
"""A reference to the :class:`asyncio.Task` that runs the :class:`Link`."""
if network_config is not None:
self.init_network(network_config)
def interface_factory(self) -> Type[CommandInterface]:
"""Create the :class:`CommandInterface` class for the Serf."""
# noinspection PyMethodParameters
class GenericInterface(CommandInterface):
alchemy = self.alchemy
bot = self
loop = self.loop
alchemy: Alchemy = self.alchemy
bot: "Serf" = self
loop: asyncio.AbstractEventLoop = self.loop
def register_herald_action(ci,
event_name: str,
coroutine: typing.Callable[[typing.Any], typing.Awaitable[typing.Dict]]) -> None:
self.network_handlers[event_name] = coroutine
coroutine: Callable[[Any], Awaitable[Dict]]) -> None:
"""Allow a coroutine to be called when a :class:`royalherald.Request` is received."""
if self.herald is None:
raise UnsupportedError("`royalherald` is not enabled on this bot.")
self.herald_handlers[event_name] = coroutine
def unregister_herald_action(ci, event_name: str):
del self.network_handlers[event_name]
"""Disable a previously registered coroutine from being called on reception of a
:class:`royalherald.Request`."""
if self.herald is None:
raise UnsupportedError("`royalherald` is not enabled on this bot.")
del self.herald_handlers[event_name]
async def call_herald_action(ci, destination: str, event_name: str, args: typing.Dict) -> typing.Dict:
if self.network is None:
raise UnsupportedError("Herald is not enabled on this bot")
request: rh.Request = rh.Request(handler=event_name, data=args)
response: rh.Response = await self.network.request(destination=destination, request=request)
if isinstance(response, rh.ResponseFailure):
async def call_herald_action(ci, destination: str, event_name: str, args: Dict) -> Dict:
"""Send a :class:`royalherald.Request` to a specific destination, and wait for a
:class:`royalherald.Response`."""
if self.herald is None:
raise UnsupportedError("`royalherald` is not enabled on this bot.")
request: Request = Request(handler=event_name, data=args)
response: Response = await self.herald.request(destination=destination, request=request)
if isinstance(response, ResponseFailure):
if response.extra_info["type"] == "CommandError":
raise CommandError(response.extra_info["message"])
raise CommandError(f"Herald action call failed:\n"
f"[p]{response}[/p]")
elif isinstance(response, rh.ResponseSuccess):
# TODO: change exception type
raise Exception(f"Herald action call failed:\n"
f"[p]{response}[/p]")
elif isinstance(response, ResponseSuccess):
return response.data
else:
raise TypeError(f"Other Herald Link returned unknown response:\n"
@ -88,41 +103,78 @@ class Interface:
return GenericInterface
def _data_factory(self) -> typing.Type[CommandData]:
def data_factory(self) -> Type[CommandData]:
"""Create the :class:`CommandData` for the Serf."""
raise NotImplementedError()
def _init_network(self):
"""Create a :py:class:`royalherald.Link`, and run it as a :py:class:`asyncio.Task`."""
if self.uninitialized_network_config is not None:
self.network: rh.Link = rh.Link(self.uninitialized_network_config.master_uri,
self.uninitialized_network_config.master_secret,
self.interface_name,
self._network_handler)
log.debug(f"Running NetworkLink {self.network}")
self.loop.create_task(self.network.run())
def register_commands(self, commands: List[Type[Command]]) -> None:
"""Initialize and register all commands passed as argument.
async def _network_handler(self, message: typing.Union[rh.Request, rh.Broadcast]) -> rh.Response:
If called again during the execution of the bot, all current commands will be replaced with the new ones.
Warning:
Hot-replacing commands was never tested and probably doesn't work."""
log.info(f"Registering {len(commands)} commands...")
# Instantiate the Commands
for SelectedCommand in commands:
# Warn if the command would be overriding something
if f"{self.Interface.prefix}{SelectedCommand.name}" in self.commands:
log.warning(f"Overriding (already defined): "
f"{SelectedCommand.__qualname__} -> {self.Interface.prefix}{SelectedCommand.name}")
else:
log.debug(f"Registering: "
f"{SelectedCommand.__qualname__} -> {self.Interface.prefix}{SelectedCommand.name}")
# Create a new interface
interface = self.Interface()
# Try to instantiate the command
try:
command = SelectedCommand(interface)
except Exception as e:
log.error(f"Skipping: "
f"{SelectedCommand.__qualname__} - {e.__class__.__qualname__} in the initialization.")
sentry_sdk.capture_exception(e)
continue
# Link the interface to the command
interface.command = command
# Register the command in the commands dict
self.commands[f"{interface.prefix}{SelectedCommand.name}"] = command
# Register aliases, but don't override anything
for alias in SelectedCommand.aliases:
if f"{interface.prefix}{alias}" not in self.commands:
log.debug(f"Aliasing: {SelectedCommand.__qualname__} -> {interface.prefix}{alias}")
self.commands[f"{interface.prefix}{alias}"] = \
self.commands[f"{interface.prefix}{SelectedCommand.name}"]
else:
log.info(f"Ignoring (already defined): {SelectedCommand.__qualname__} -> {interface.prefix}{alias}")
def init_network(self, config: HeraldConfig):
"""Create a :py:class:`Link`, and run it as a :py:class:`asyncio.Task`."""
log.debug(f"Initializing herald...")
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:
try:
network_handler = self.network_handlers[message.handler]
network_handler = self.herald_handlers[message.handler]
except KeyError:
log.warning(f"Missing network_handler for {message.handler}")
return rh.ResponseFailure("no_handler", f"This bot is missing a network handler for {message.handler}.")
return ResponseFailure("no_handler", f"This bot is missing a network handler for {message.handler}.")
else:
log.debug(f"Using {network_handler} as handler for {message.handler}")
if isinstance(message, rh.Request):
if isinstance(message, Request):
try:
response_data = await network_handler(self, **message.data)
return rh.ResponseSuccess(data=response_data)
return ResponseSuccess(data=response_data)
except Exception as e:
sentry_sdk.capture_exception(e)
log.error(f"Exception {e} in {network_handler}")
return rh.ResponseFailure("exception_in_handler",
f"An exception was raised in {network_handler} for {message.handler}.",
extra_info={
"type": e.__class__.__name__,
"message": str(e)
})
elif isinstance(message, rh.Broadcast):
return ResponseFailure("exception_in_handler",
f"An exception was raised in {network_handler} for {message.handler}.",
extra_info={
"type": e.__class__.__name__,
"message": str(e)
})
elif isinstance(message, Broadcast):
await network_handler(self, **message.data)
def _init_database(self):
@ -173,21 +225,6 @@ class Interface:
else:
self.loop = self.uninitialized_loop
def __init__(self, *,
network_config: typing.Optional[rh.Config] = None,
database_config: typing.Optional[DatabaseConfig] = None,
commands: typing.List[typing.Type[Command]] = None,
sentry_dsn: typing.Optional[str] = None,
loop: asyncio.AbstractEventLoop = None,
secrets_name: str = "__default__"):
self.initialized = False
self.uninitialized_network_config = network_config
self.uninitialized_database_config = database_config
self.uninitialized_commands = commands
self.uninitialized_sentry_dsn = sentry_dsn
self.uninitialized_loop = loop
self.secrets_name = secrets_name
def get_secret(self, username: str):
return keyring.get_password(f"Royalnet/{self.secrets_name}", username)
@ -200,7 +237,7 @@ class Interface:
self._init_loop()
self._init_database()
self._init_commands()
self._init_network()
self.init_network()
self.initialized = True
def run(self):

View file

@ -29,7 +29,7 @@ class TelegramBot(GenericBot):
def _interface_factory(self) -> typing.Type[CommandInterface]:
# noinspection PyPep8Naming
GenericInterface = super()._interface_factory()
GenericInterface = super().interface_factory()
# noinspection PyMethodParameters,PyAbstractClass
class TelegramInterface(GenericInterface):