From 48c746ea7df74916524b5b37005eb5aa41810b63 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 12 Nov 2019 21:01:18 +0100 Subject: [PATCH] More progress --- .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/misc.xml | 2 +- .idea/royalnet.iml | 2 +- poetry.lock | 755 ++++++++++++++++++ pyproject.toml | 25 +- royalnet/alchemy/alchemy.py | 10 +- royalnet/alchemy/errors.py | 1 - royalnet/alchemy/table_dfs.py | 4 +- royalnet/commands/__init__.py | 2 +- royalnet/commands/commandargs.py | 53 +- royalnet/commands/commanddata.py | 2 +- royalnet/commands/commandinterface.py | 4 +- .../commands/{commanderrors.py => errors.py} | 0 royalnet/herald/__init__.py | 26 + royalnet/herald/broadcast.py | 26 + royalnet/herald/config.py | 34 + royalnet/herald/errors.py | 18 + royalnet/herald/link.py | 179 +++++ royalnet/herald/package.py | 113 +++ royalnet/herald/request.py | 30 + royalnet/herald/response.py | 50 ++ royalnet/herald/server.py | 153 ++++ royalnet/{interfaces => serf}/__init__.py | 2 +- royalnet/serf/alchemyconfig.py | 12 + .../{interfaces => serf}/discord/__init__.py | 0 .../discord/create_rich_embed.py | 0 .../{interfaces => serf}/discord/discord.py | 2 +- .../{interfaces => serf}/discord/escape.py | 0 .../discord/fileaudiosource.py | 0 .../{interfaces => serf}/discord/playmodes.py | 0 .../discord/ytdldiscord.py | 0 .../{interfaces/interface.py => serf/serf.py} | 231 +++--- .../{interfaces => serf}/telegram/__init__.py | 0 .../{interfaces => serf}/telegram/escape.py | 0 .../{interfaces => serf}/telegram/telegram.py | 2 +- 35 files changed, 1618 insertions(+), 125 deletions(-) create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 poetry.lock rename royalnet/commands/{commanderrors.py => errors.py} (100%) create mode 100644 royalnet/herald/__init__.py create mode 100644 royalnet/herald/broadcast.py create mode 100644 royalnet/herald/config.py create mode 100644 royalnet/herald/errors.py create mode 100644 royalnet/herald/link.py create mode 100644 royalnet/herald/package.py create mode 100644 royalnet/herald/request.py create mode 100644 royalnet/herald/response.py create mode 100644 royalnet/herald/server.py rename royalnet/{interfaces => serf}/__init__.py (84%) create mode 100644 royalnet/serf/alchemyconfig.py rename royalnet/{interfaces => serf}/discord/__init__.py (100%) rename royalnet/{interfaces => serf}/discord/create_rich_embed.py (100%) rename royalnet/{interfaces => serf}/discord/discord.py (99%) rename royalnet/{interfaces => serf}/discord/escape.py (100%) rename royalnet/{interfaces => serf}/discord/fileaudiosource.py (100%) rename royalnet/{interfaces => serf}/discord/playmodes.py (100%) rename royalnet/{interfaces => serf}/discord/ytdldiscord.py (100%) rename royalnet/{interfaces/interface.py => serf/serf.py} (50%) rename royalnet/{interfaces => serf}/telegram/__init__.py (100%) rename royalnet/{interfaces => serf}/telegram/escape.py (100%) rename royalnet/{interfaces => serf}/telegram/telegram.py (99%) diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..a55e7a17 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 0422079e..90fe8c99 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -6,5 +6,5 @@ - + \ No newline at end of file diff --git a/.idea/royalnet.iml b/.idea/royalnet.iml index 2b5b5330..f344fe9e 100644 --- a/.idea/royalnet.iml +++ b/.idea/royalnet.iml @@ -5,7 +5,7 @@ - + diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..71585ea0 --- /dev/null +++ b/poetry.lock @@ -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"}, +] diff --git a/pyproject.toml b/pyproject.toml index bf573702..642497b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/royalnet/alchemy/alchemy.py b/royalnet/alchemy/alchemy.py index 7d7d16bc..7c5a064b 100644 --- a/royalnet/alchemy/alchemy.py +++ b/royalnet/alchemy/alchemy.py @@ -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: diff --git a/royalnet/alchemy/errors.py b/royalnet/alchemy/errors.py index 860900c9..dd96c310 100644 --- a/royalnet/alchemy/errors.py +++ b/royalnet/alchemy/errors.py @@ -1,4 +1,3 @@ - class AlchemyException(Exception): """Base class for Alchemy exceptions.""" diff --git a/royalnet/alchemy/table_dfs.py b/royalnet/alchemy/table_dfs.py index 99376d24..74d850e9 100644 --- a/royalnet/alchemy/table_dfs.py +++ b/royalnet/alchemy/table_dfs.py @@ -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: diff --git a/royalnet/commands/__init__.py b/royalnet/commands/__init__.py index b4fc1cc3..6cbc9c77 100644 --- a/royalnet/commands/__init__.py +++ b/royalnet/commands/__init__.py @@ -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", diff --git a/royalnet/commands/commandargs.py b/royalnet/commands/commandargs.py index 69d7ef54..ccaaa904 100644 --- a/royalnet/commands/commandargs.py +++ b/royalnet/commands/commandargs.py @@ -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: diff --git a/royalnet/commands/commanddata.py b/royalnet/commands/commanddata.py index 60a1eb5e..c46997e3 100644 --- a/royalnet/commands/commanddata.py +++ b/royalnet/commands/commanddata.py @@ -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 diff --git a/royalnet/commands/commandinterface.py b/royalnet/commands/commandinterface.py index 3ad62e03..9a539ecc 100644 --- a/royalnet/commands/commandinterface.py +++ b/royalnet/commands/commandinterface.py @@ -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: diff --git a/royalnet/commands/commanderrors.py b/royalnet/commands/errors.py similarity index 100% rename from royalnet/commands/commanderrors.py rename to royalnet/commands/errors.py diff --git a/royalnet/herald/__init__.py b/royalnet/herald/__init__.py new file mode 100644 index 00000000..291c7088 --- /dev/null +++ b/royalnet/herald/__init__.py @@ -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", +] diff --git a/royalnet/herald/broadcast.py b/royalnet/herald/broadcast.py new file mode 100644 index 00000000..848aff9b --- /dev/null +++ b/royalnet/herald/broadcast.py @@ -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})" diff --git a/royalnet/herald/config.py b/royalnet/herald/config.py new file mode 100644 index 00000000..1b9b1a44 --- /dev/null +++ b/royalnet/herald/config.py @@ -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"" diff --git a/royalnet/herald/errors.py b/royalnet/herald/errors.py new file mode 100644 index 00000000..268eb85a --- /dev/null +++ b/royalnet/herald/errors.py @@ -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`.""" diff --git a/royalnet/herald/link.py b/royalnet/herald/link.py new file mode 100644 index 00000000..ce234282 --- /dev/null +++ b/royalnet/herald/link.py @@ -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 == "": + 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)) diff --git a/royalnet/herald/package.py b/royalnet/herald/package.py new file mode 100644 index 00000000..4f162233 --- /dev/null +++ b/royalnet/herald/package.py @@ -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") diff --git a/royalnet/herald/request.py b/royalnet/herald/request.py new file mode 100644 index 00000000..9e760e67 --- /dev/null +++ b/royalnet/herald/request.py @@ -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})" diff --git a/royalnet/herald/response.py b/royalnet/herald/response.py new file mode 100644 index 00000000..73d830bb --- /dev/null +++ b/royalnet/herald/response.py @@ -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})" diff --git a/royalnet/herald/server.py b/royalnet/herald/server.py new file mode 100644 index 00000000..dff0bd29 --- /dev/null +++ b/royalnet/herald/server.py @@ -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="", + 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 == "": + # 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 == "": + 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() diff --git a/royalnet/interfaces/__init__.py b/royalnet/serf/__init__.py similarity index 84% rename from royalnet/interfaces/__init__.py rename to royalnet/serf/__init__.py index fdedb173..b34fa4f4 100644 --- a/royalnet/interfaces/__init__.py +++ b/royalnet/serf/__init__.py @@ -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 diff --git a/royalnet/serf/alchemyconfig.py b/royalnet/serf/alchemyconfig.py new file mode 100644 index 00000000..ba6d7b88 --- /dev/null +++ b/royalnet/serf/alchemyconfig.py @@ -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 \ No newline at end of file diff --git a/royalnet/interfaces/discord/__init__.py b/royalnet/serf/discord/__init__.py similarity index 100% rename from royalnet/interfaces/discord/__init__.py rename to royalnet/serf/discord/__init__.py diff --git a/royalnet/interfaces/discord/create_rich_embed.py b/royalnet/serf/discord/create_rich_embed.py similarity index 100% rename from royalnet/interfaces/discord/create_rich_embed.py rename to royalnet/serf/discord/create_rich_embed.py diff --git a/royalnet/interfaces/discord/discord.py b/royalnet/serf/discord/discord.py similarity index 99% rename from royalnet/interfaces/discord/discord.py rename to royalnet/serf/discord/discord.py index 313fe622..24df3140 100644 --- a/royalnet/interfaces/discord/discord.py +++ b/royalnet/serf/discord/discord.py @@ -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): diff --git a/royalnet/interfaces/discord/escape.py b/royalnet/serf/discord/escape.py similarity index 100% rename from royalnet/interfaces/discord/escape.py rename to royalnet/serf/discord/escape.py diff --git a/royalnet/interfaces/discord/fileaudiosource.py b/royalnet/serf/discord/fileaudiosource.py similarity index 100% rename from royalnet/interfaces/discord/fileaudiosource.py rename to royalnet/serf/discord/fileaudiosource.py diff --git a/royalnet/interfaces/discord/playmodes.py b/royalnet/serf/discord/playmodes.py similarity index 100% rename from royalnet/interfaces/discord/playmodes.py rename to royalnet/serf/discord/playmodes.py diff --git a/royalnet/interfaces/discord/ytdldiscord.py b/royalnet/serf/discord/ytdldiscord.py similarity index 100% rename from royalnet/interfaces/discord/ytdldiscord.py rename to royalnet/serf/discord/ytdldiscord.py diff --git a/royalnet/interfaces/interface.py b/royalnet/serf/serf.py similarity index 50% rename from royalnet/interfaces/interface.py rename to royalnet/serf/serf.py index 73d38cf7..204a8c6f 100644 --- a/royalnet/interfaces/interface.py +++ b/royalnet/serf/serf.py @@ -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): diff --git a/royalnet/interfaces/telegram/__init__.py b/royalnet/serf/telegram/__init__.py similarity index 100% rename from royalnet/interfaces/telegram/__init__.py rename to royalnet/serf/telegram/__init__.py diff --git a/royalnet/interfaces/telegram/escape.py b/royalnet/serf/telegram/escape.py similarity index 100% rename from royalnet/interfaces/telegram/escape.py rename to royalnet/serf/telegram/escape.py diff --git a/royalnet/interfaces/telegram/telegram.py b/royalnet/serf/telegram/telegram.py similarity index 99% rename from royalnet/interfaces/telegram/telegram.py rename to royalnet/serf/telegram/telegram.py index 03dd4638..e5d683b4 100644 --- a/royalnet/interfaces/telegram/telegram.py +++ b/royalnet/serf/telegram/telegram.py @@ -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):