commit 17903c469141ec7954af93d9d71c94066cd06b80 Author: deathrow Date: Tue Nov 1 18:55:12 2022 -0400 Add Code diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..30cf63e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +ARG PYTHON_VERSION=3.8 + +FROM docker.io/python:${PYTHON_VERSION} as base + +WORKDIR /app + +FROM base as builder + +ENV POETRY_VERSION=1.2.2 + +RUN pip install "poetry==$POETRY_VERSION" +RUN python -m venv /venv +COPY pyproject.toml poetry.lock config.sample.yaml matrix_registration ./ +RUN . /venv/bin/activate && poetry install --no-dev --no-root + +COPY . . +RUN . /venv/bin/activate && poetry build + +# Runtime +FROM base as final + +COPY --from=builder /venv /venv +COPY --from=builder /app/dist . + +RUN . /venv/bin/activate && pip install *.whl + +VOLUME ["/data"] + +EXPOSE 5000/tcp + +ENTRYPOINT ["/venv/bin/matrix-registration", "--config-path=/data/config.yaml"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f16380d --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +## Synapse-Captcha + +A custom captcha for Synapse. +Disable registration in ``homeserver.yaml`` + +### Building +`` +git clone https://codeberg.org/deathrow/synapse-captcha +`` + +`` +cd synapse-captcha +`` + +`` +docker build . +`` + +Modify `config.sample.yaml` to your needs and save as `config.yaml` +The `shared_secret` can be found in `homeserver.yaml`. + + +Redirect to your docker installation: + +(Modify for your needs) +``` + location /register { + include /config/nginx/proxy.conf; + include /config/nginx/resolver.conf; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + set $upstream_app matrix-registration; + set $upstream_port 5000; + set $upstream_proto http; + proxy_pass $upstream_proto://$upstream_app:$upstream_port; + } +``` + +ex. `matrix.example.tld/register` + +Docker-compose example: + +``` + build: ./images/synapse-captcha + container_name: matrix-registration + restart: always + command: [ + "--config-path=/data/config.yaml", + "serve" + ] + ports: + - 127.0.0.1:5000:5000 + volumes: + - ./matrix-registration_data:/data:Z + networks: + - matrix +``` \ No newline at end of file diff --git a/config.sample.yaml b/config.sample.yaml new file mode 100644 index 0000000..540c6a0 --- /dev/null +++ b/config.sample.yaml @@ -0,0 +1,38 @@ +server_location: 'http://synapse:8008' +server_name: 'matrix.org' +shared_secret: 'RegistrationSharedSecret' +base_url: '' +riot_instance: 'https://riot.im/app/' +db: 'sqlite:////data/db.sqlite3' +host: '0.0.0.0' +port: 5000 +rate_limit: ["10000 per day", "100 per minute"] +allow_cors: false +logging: + disable_existing_loggers: False + version: 1 + root: + level: DEBUG + handlers: [console, file] + formatters: + brief: + format: '%(name)s - %(levelname)s - %(message)s' + precise: + format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + handlers: + console: + class: logging.StreamHandler + level: INFO + formatter: brief + stream: ext://sys.stdout + file: + class: logging.handlers.RotatingFileHandler + formatter: precise + level: INFO + filename: m_reg.log + maxBytes: 10485760 # 10MB + backupCount: 3 + encoding: utf8 +# password requirements +password: + min_length: 8 diff --git a/matrix_registration/__init__.py b/matrix_registration/__init__.py new file mode 100644 index 0000000..26f17a9 --- /dev/null +++ b/matrix_registration/__init__.py @@ -0,0 +1,5 @@ +from . import api +from . import config +from . import captcha + +name = 'matrix_registration' diff --git a/matrix_registration/api.py b/matrix_registration/api.py new file mode 100644 index 0000000..aa3104c --- /dev/null +++ b/matrix_registration/api.py @@ -0,0 +1,194 @@ +# Standard library imports... +import logging +from requests import exceptions +import re +from urllib.parse import urlparse + +# Third-party imports... +from flask import (Blueprint, abort, jsonify, request, make_response, + render_template) +from wtforms import (Form, StringField, PasswordField, validators) +from wtforms.fields.simple import HiddenField + +# Local imports... +from .matrix_api import create_account +from . import config +from . import captcha + +logger = logging.getLogger(__name__) + +api = Blueprint("api", __name__) + +re_mxid = re.compile(r'^@?[a-zA-Z_\-=\.\/0-9]+(:[a-zA-Z\-\.:\/0-9]+)?$') + + +def validate_captcha(form, captcha_answer): + """ + validates captcha + Parameters + ---------- + arg1 : Form object + arg2 : str + captcha answer, e.g. '4tg' + Raises + ------- + ValidationError + captcha is invalid + """ + if not captcha.captcha.validate(captcha_answer.data, + form.captcha_token.data): + raise validators.ValidationError("captcha is invalid") + + +def validate_username(form, username): + """ + validates username + + Parameters + ---------- + arg1 : Form object + arg2 : str + username name, e.g: '@user:matrix.org' or 'user' + https://github.com/matrix-org/matrix-doc/blob/master/specification/appendices/identifier_grammar.rst#user-identifiers + Raises + ------- + ValidationError + Username doesn't follow mxid requirements + """ + domain = urlparse(config.config.server_location).hostname + re_mxid = r'^@?[a-zA-Z_\-=\.\/0-9]+(:' + \ + re.escape(domain) + \ + r')?$' + err = "Username doesn't follow pattern: '%s'" % re_mxid + if not re.search(re_mxid, username.data): + raise validators.ValidationError(err) + + +def validate_password(form, password): + """ + validates username + + Parameters + ---------- + arg1 : Form object + arg2 : str + password + Raises + ------- + ValidationError + Password doesn't follow length requirements + """ + min_length = config.config.password['min_length'] + err = 'Password should be between %s and 255 chars long' % min_length + if len(password.data) < min_length or len(password.data) > 255: + raise validators.ValidationError(err) + + +class RegistrationForm(Form): + """ + Registration Form + + validates user account registration requests + """ + username = StringField( + 'Username', + [ + validators.Length(min=1, max=200), + # validators.Regexp(re_mxid) + validate_username + ]) + password = PasswordField( + 'New Password', + [ + # validators.Length(min=8), + validate_password, + validators.DataRequired(), + validators.EqualTo('confirm', message='Passwords must match') + ]) + confirm = PasswordField('Repeat Password') + captcha_answer = StringField("Captcha answer", [validate_captcha]) + captcha_token = HiddenField("Captcha token") + + +@api.route('/register', methods=['GET', 'POST']) +def register(): + """ + main user account registration endpoint + to register an account you need to send a + application/x-www-form-urlencoded request with + - username + - password + - confirm + - captcha_answer + - captcha_token + as described in the RegistrationForm + """ + if request.method == 'POST': + logger.debug('an account registration started...') + form = RegistrationForm(request.form) + logger.debug('validating request data...') + if form.validate(): + logger.debug('request valid') + # remove sigil and the domain from the username + username = form.username.data.rsplit(':')[0].split('@')[-1] + logger.debug('creating account %s...' % username) + # send account creation request to the hs + try: + account_data = create_account(form.username.data, + form.password.data, + config.config.server_location, + config.config.shared_secret) + except exceptions.ConnectionError: + logger.error('can not connect to %s' % + config.config.server_location, + exc_info=True) + abort(500) + except exceptions.HTTPError as e: + resp = e.response + error = resp.json() + status_code = resp.status_code + if status_code == 404: + logger.error('no HS found at %s' % + config.config.server_location) + elif status_code == 403: + logger.error( + 'wrong shared registration secret or not enabled') + elif status_code == 400: + # most likely this should only be triggered if a userid + # is already in use + return make_response(jsonify(error), 400) + else: + logger.error('failure communicating with HS', + exc_info=True) + abort(500) + logger.debug('account creation succeded!') + return jsonify(access_token=account_data['access_token'], + home_server=account_data['home_server'], + user_id=account_data['user_id'], + status='success', + status_code=200) + else: + logger.debug('account creation failed!') + captcha_data = captcha.captcha.generate() + resp = { + 'errcode': 'MR_BAD_USER_REQUEST', + 'error': form.errors, + "captcha_image": captcha_data["captcha_image"].decode(), + "captcha_token": captcha_data["captcha_token"] + } + return make_response(jsonify(resp), 400) + # for fieldName, errorMessages in form.errors.items(): + # for err in errorMessages: + # # return error to user + else: + server_name = config.config.server_name + pw_length = config.config.password['min_length'] + captcha_data = captcha.captcha.generate() + return render_template( + 'register.html', + server_name=server_name, + pw_length=pw_length, + riot_instance=config.config.riot_instance, + base_url=config.config.base_url, + captcha_token=captcha_data["captcha_token"], + captcha_image=captcha_data["captcha_image"].decode()) diff --git a/matrix_registration/app.py b/matrix_registration/app.py new file mode 100644 index 0000000..5534d04 --- /dev/null +++ b/matrix_registration/app.py @@ -0,0 +1,64 @@ +import logging +import logging.config +import click + +from flask import Flask +from flask.cli import FlaskGroup, pass_script_info +from flask_cors import CORS +from waitress import serve + +from . import captcha +from .captcha import db +from . import config +import os + + +def create_app(testing=False): + app = Flask(__name__) + app.testing = testing + + with app.app_context(): + from .api import api + app.register_blueprint(api) + + return app + + +@click.group(cls=FlaskGroup, + add_default_commands=False, + create_app=create_app, + context_settings=dict(help_option_names=['-h', '--help'])) +@click.option("--config-path", + default="config.yaml", + help='specifies the config file to be used') +@pass_script_info +def cli(info, config_path): + """a token based matrix registration app""" + config.config = config.Config(config_path) + logging.config.dictConfig(config.config.logging) + app = info.load_app() + with app.app_context(): + app.config.from_mapping( + SQLALCHEMY_DATABASE_URI=config.config.db.format( + cwd=f"{os.getcwd()}/"), + SQLALCHEMY_TRACK_MODIFICATIONS=False) + db.init_app(app) + db.create_all() + captcha.captcha = captcha.CaptchaGenerator() + + +@cli.command("serve", help="start api server") +@pass_script_info +def run_server(info): + app = info.load_app() + if config.config.allow_cors: + CORS(app) + serve(app, + host=config.config.host, + port=config.config.port, + url_prefix=config.config.base_url) + + +if __name__ == "__main__": + cli() + run_server() diff --git a/matrix_registration/captcha.py b/matrix_registration/captcha.py new file mode 100644 index 0000000..6c877d1 --- /dev/null +++ b/matrix_registration/captcha.py @@ -0,0 +1,71 @@ +from captcha.image import ImageCaptcha +from flask_sqlalchemy import SQLAlchemy + +import base64 +import random +import string +import time +import uuid + +CAPTCHA_TIMEOUT = 5 # minutes +CAPTCHA_LENGTH = 5 # characters +CAPTCHA_WIDTH = 320 +CAPTCHA_HEIGHT = 94 + +db = SQLAlchemy() + + +class Captcha(db.Model): + __tablename__ = 'captcha' + token = db.Column(db.String(36), primary_key=True) + answer = db.Column(db.String(24)) + timestamp = db.Column(db.Integer, default=0) + + +class CaptchaGenerator: + + def clean(self): + Captcha.query.filter( + Captcha.timestamp < (time.time() - CAPTCHA_TIMEOUT * 60)).delete() + db.session.commit() + + def validate(self, captcha_answer, captcha_token): + self.clean() + try: + cpt = Captcha.query.filter(Captcha.token == captcha_token).one() + except: + # when the user stay on the page too long the captcha is removed + return False + + if cpt: + answer = cpt.answer + db.session.delete(cpt) + db.session.commit() + return captcha_answer.lower() == answer + return False + + def generate(self): + self.clean() + captcha_token = str(uuid.uuid4()) + captcha_answer = (''.join( + random.choice(string.ascii_lowercase + string.digits) + for _ in range(CAPTCHA_LENGTH))) + image = ImageCaptcha(width=CAPTCHA_WIDTH, height=CAPTCHA_HEIGHT) + captcha_image = base64.b64encode( + image.generate(captcha_answer).getvalue()) + timestamp = time.time() + data = { + "captcha_image": captcha_image, + "captcha_token": captcha_token, + "captcha_answer": captcha_answer, + "timestamp": timestamp + } + cpt = Captcha(token=captcha_token, + answer=captcha_answer, + timestamp=timestamp) + db.session.add(cpt) + db.session.commit() + return data + + +captcha = None diff --git a/matrix_registration/config.py b/matrix_registration/config.py new file mode 100644 index 0000000..eaf31e3 --- /dev/null +++ b/matrix_registration/config.py @@ -0,0 +1,128 @@ +# Standard library imports... +# from collections import namedtuple +import logging +import os +import sys + +# Third-party imports... +import yaml + +# Local imports... +from .constants import (CONFIG_PATH1, CONFIG_PATH2, CONFIG_PATH3, CONFIG_PATH4, + CONFIG_PATH5) + +CONFIG_PATHS = [ + CONFIG_PATH1, CONFIG_PATH2, CONFIG_PATH3, CONFIG_PATH4, CONFIG_PATH5 +] +CONFIG_SAMPLE_NAME = "config.sample.yaml" +CONFIG_NAME = 'config.yaml' +logger = logging.getLogger(__name__) + + +class Config: + """ + Config + + loads a dict or a yaml file to be accessible by all files in the module + """ + + def __init__(self, data): + self.data = data + self.CONFIG_PATH = None + self.location = None + self.load() + + def load(self): + """ + loads the dict/the yaml file and recursively sets dictionary to class properties + """ + logger.debug('loading config...') + dictionary = None + config_default = True + if type(self.data) is dict: + logger.debug('from dict...') + dictionary = self.data + config_default = False + else: + logger.debug('from file...') + # check work dir and all other pip install locations for config + if os.path.isfile(self.data): + config_default = False + else: + # provided file not found checking typical installation dirs + config_exists = False + for path in CONFIG_PATHS: + if os.path.isfile(path + CONFIG_NAME): + self.CONFIG_PATH = path + config_exists = True + config_default = False + if not config_exists: + # no config exists, use sample config instead + # check typical installation dirs for sample configs + for path in CONFIG_PATHS: + if os.path.isfile(path + CONFIG_SAMPLE_NAME): + self.CONFIG_PATH = path + config_exists = True + # check if still no config found + if not config_exists: + sys.exit('could not find any configuration file!') + self.data = os.path.join(self.CONFIG_PATH, + CONFIG_SAMPLE_NAME) + else: + self.data = os.path.join(self.CONFIG_PATH, CONFIG_NAME) + try: + with open(self.data, 'r') as stream: + dictionary = yaml.load(stream, Loader=yaml.SafeLoader) + except IOError as e: + sys.exit(e) + if config_default: + self.read_config(dictionary) + + logger.debug('setting config...') + # recusively set dictionary to class properties + for k, v in dictionary.items(): + setattr(self, k, v) + logger.debug('config set!') + # self.x = namedtuple('config', + # dictionary.keys())(*dictionary.values()) + + def update(self, data): + """ + resets all options and loads the new config + + Parameters + ---------- + arg1 : dict or path to config file + """ + logger.debug('updating config...') + self.data = data + self.CONFIG_PATH = None + self.location = None + self.load() + logger.debug('config updated!') + + def read_config(self, dictionary): + """ + asks the user how to set the essential options + + Parameters + ---------- + arg1 : dict + with sample values + """ + # important keys that need to be changed + keys = ['server_location', 'server_name', 'shared_secret', 'port'] + for key in keys: + temp = dictionary[key] + dictionary[key] = input('enter {}, e.g. {}\n'.format(key, temp)) + if not dictionary[key].strip(): + dictionary[key] = temp + # write to config file + new_config_path = self.CONFIG_PATH + CONFIG_NAME + relative_path = os.path.relpath(self.CONFIG_PATH + CONFIG_NAME) + with open(new_config_path, 'w') as stream: + yaml.dump(dictionary, stream, default_flow_style=False) + print('config file written to "%s"' % relative_path) + + +config = None diff --git a/matrix_registration/constants.py b/matrix_registration/constants.py new file mode 100644 index 0000000..6929a0b --- /dev/null +++ b/matrix_registration/constants.py @@ -0,0 +1,19 @@ +# Standard library imports... +import os +import site +import sys + +# Third-party imports... +from appdirs import user_config_dir + +__location__ = os.path.realpath( + os.path.join(os.getcwd(), os.path.dirname(__file__))) +WORD_LIST_PATH = os.path.join(__location__, 'wordlist.txt') +# first check in current working dir +CONFIG_PATH1 = os.path.join(os.getcwd() + '/') +CONFIG_PATH2 = os.path.join(os.getcwd() + '/config/') +# then check in XDG_CONFIG_HOME +CONFIG_PATH3 = os.path.join(user_config_dir('matrix-registration') + '/') +# check at installed location +CONFIG_PATH4 = os.path.join(__location__, '../') +CONFIG_PATH5 = os.path.join(sys.prefix, 'config/') diff --git a/matrix_registration/matrix_api.py b/matrix_registration/matrix_api.py new file mode 100644 index 0000000..1dad817 --- /dev/null +++ b/matrix_registration/matrix_api.py @@ -0,0 +1,75 @@ +# Standard library imports... +import hashlib +import hmac +import requests + +import logging + +logger = logging.getLogger(__name__) + + +def create_account(user, + password, + server_location, + shared_secret, + admin=False): + """ + creates account + https://github.com/matrix-org/synapse/blob/master/synapse/_scripts/register_new_matrix_user.py + + Parameters + ---------- + arg1 : str + local part of the new user + arg2 : str + password + arg3 : str + url to homeserver + arg4 : str + Registration Shared Secret as set in the homeserver.yaml + arg5 : bool + register new user as an admin. + Raises + ------- + requests.exceptions.ConnectionError: + can't connect to homeserver + requests.exceptions.HTTPError: + something with the communciation to the homeserver failed + """ + nonce = _get_nonce(server_location) + + mac = hmac.new( + key=str.encode(shared_secret), + digestmod=hashlib.sha1, + ) + + mac.update(nonce.encode()) + mac.update(b'\x00') + mac.update(user.encode()) + mac.update(b'\x00') + mac.update(password.encode()) + mac.update(b'\x00') + mac.update(b'admin' if admin else b'notadmin') + + mac = mac.hexdigest() + + data = { + 'nonce': nonce, + 'username': user, + 'password': password, + 'admin': admin, + 'mac': mac, + } + + server_location = server_location.rstrip('/') + + r = requests.post('%s/_synapse/admin/v1/register' % (server_location), + json=data) + r.raise_for_status() + return r.json() + + +def _get_nonce(server_location): + r = requests.get('%s/_synapse/admin/v1/register' % (server_location)) + r.raise_for_status() + return r.json()['nonce'] diff --git a/matrix_registration/static/css/style.css b/matrix_registration/static/css/style.css new file mode 100644 index 0000000..e3f4b7d --- /dev/null +++ b/matrix_registration/static/css/style.css @@ -0,0 +1,300 @@ +html, +body { + height: 100%; + margin: 0; +} + +body { + background-size: cover; + background-attachment: fixed; + overflow: hidden; +} + +h1 { + font-size: 1.3em; +} + +article { + color: white; +} + +a:link, +a:visited { + color: #038db3 !important; +} + +form { + width: 320px; + margin: 45px auto; +} + +textarea { + resize: none; +} + +input, +textarea { + background: none; + color: white; + font-size: 18px; + padding: 10px 10px 10px 5px; + display: block; + width: 320px; + border: none; + border-radius: 0; + border-bottom: 1px solid white; +} + +input:focus, +textarea:focus { + outline: none; +} + +input:focus~label, +input:not(:placeholder-shown)~label, +textarea:focus~label, +textarea:valid~label { + top: -14px; + font-size: 12px; + color: #03b381; +} + +input:focus~.bar:before, +textarea:focus~.bar:before { + width: 320px; +} + +input[type="password"] { + letter-spacing: 0.3em; +} + +input:invalid { + box-shadow: none; +} + +input:invalid~.bar:before { + background: #038db3; +} + +input:invalid~label { + color: #038db3; +} + +input[type="submit"] { + cursor: pointer; +} + +label { + color: white; + font-size: 16px; + font-weight: normal; + position: absolute; + pointer-events: none; + left: 5px; + top: 10px; + transition: 300ms ease all; +} + +*, +:before, +:after { + box-sizing: border-box; +} + +.center { + text-align: center; + margin-top: 2em; +} +.hidden { + visibility: hidden; + opacity: 0; +} + +.group { + position: relative; + margin: 45px 0; +} + +.bar { + position: relative; + display: block; + width: 320px; +} + +.bar:before { + content: ''; + height: 2px; + width: 0; + bottom: 0px; + position: absolute; + background: #03b381; + transition: 300ms ease all; + left: 0%; +} + +.btn { + background: white; + color: black; + border: none; + padding: 10px 20px; + border-radius: 3px; + letter-spacing: 0.06em; + text-transform: uppercase; + text-decoration: none; + outline: none; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); + transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); +} + +.btn:hover { + color: black; + box-shadow: 0 7px 14px rgba(0, 0, 0, 0.18), 0 5px 5px rgba(0, 0, 0, 0.12); +} + +.btn.btn-submit { + background: #03b381; + color: #bce0fb; +} + +.btn.btn-submit:hover { + background: #03b372; + color: #deeffd; +} + +.btn-box { + text-align: center; + margin: 50px 0; +} + +.info { + z-index: 2; + position: absolute; + bottom: .5vh; + right: 1vw; + text-align: left; + color: grey; + font-size: 0.8em; + opacity: 0.1; + transition: opacity 0.5s ease; +} + +.info:hover { + opacity: 1; +} + +.info a { + color: cyan; +} + +.widget { + position: absolute; + left: 50%; + top: 50%; + border: 0px solid; + border-radius: 5px; + overflow: hidden; + background-color: #1f1f1f; + z-index: 1; + box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.5); +} + +.widget::before { + position: absolute; + top: 0; + left: 0; + z-index: -1; + width: 100%; + height: 100%; + background-attachment: fixed; + background-size: cover; + opacity: 0.20; + content: ""; +} + +.blur:before { + content: ""; + position: absolute; + width: 100%; + height: 100%; + background: inherit; + z-index: -1; + transform: scale(1.03); + filter: blur(10px); +} + +.register { + margin-left: -15em; + margin-top: -25em; + width: 30em; + height: 50em; +} + +.modal { + margin-left: -12.5em; + margin-top: -7.5em; + width: 25em; + background-color: #f7f7f7; + transition: visibility .3s, opacity .3s linear; +} + +.modal article { + margin-top: -5em; +} + +.modal article, +.modal p, +.modal h2, +.modal h3 { + color: #1f1f1f; +} + +.error { + color: #b30335 !important; +} + +@media only screen and (max-width: 500px) { + .info { + bottom: -2vh; + } + + .widget { + margin-top: -40vh; + margin-left: -45vw; + width: 90vw; + min-width: 20em; + } + + .modal { + margin-top: -15vh; + margin-left: -35vw; + width: 70vw; + min-width: 15em; + } +} + +@media only screen and (max-height: 768px) { + body { + overflow-y: visible; + padding-bottom: -90vh; + } + + .blur:before { + filter: none; + transform: none; + padding-bottom: 50em; + } + + .info { + float: right; + padding-top: 57em; + position: static; + } + + .widget { + margin-top: -40vh; + } + + .modal { + margin-top: -15vh; + } +} \ No newline at end of file diff --git a/matrix_registration/static/images/element-logo.png b/matrix_registration/static/images/element-logo.png new file mode 100644 index 0000000..7de76f7 Binary files /dev/null and b/matrix_registration/static/images/element-logo.png differ diff --git a/matrix_registration/static/images/favicon.ico b/matrix_registration/static/images/favicon.ico new file mode 100644 index 0000000..ca7fb1c Binary files /dev/null and b/matrix_registration/static/images/favicon.ico differ diff --git a/matrix_registration/static/images/icon.png b/matrix_registration/static/images/icon.png new file mode 100644 index 0000000..ea4be06 Binary files /dev/null and b/matrix_registration/static/images/icon.png differ diff --git a/matrix_registration/static/images/icon32x32.png b/matrix_registration/static/images/icon32x32.png new file mode 100644 index 0000000..6035468 Binary files /dev/null and b/matrix_registration/static/images/icon32x32.png differ diff --git a/matrix_registration/static/images/riot.png b/matrix_registration/static/images/riot.png new file mode 100644 index 0000000..ea21306 Binary files /dev/null and b/matrix_registration/static/images/riot.png differ diff --git a/matrix_registration/static/images/tile.png b/matrix_registration/static/images/tile.png new file mode 100644 index 0000000..d347961 Binary files /dev/null and b/matrix_registration/static/images/tile.png differ diff --git a/matrix_registration/static/images/valley.jpg b/matrix_registration/static/images/valley.jpg new file mode 100644 index 0000000..4779c9d Binary files /dev/null and b/matrix_registration/static/images/valley.jpg differ diff --git a/matrix_registration/templates/register.html b/matrix_registration/templates/register.html new file mode 100644 index 0000000..9e3521a --- /dev/null +++ b/matrix_registration/templates/register.html @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + {{ server_name }} registration + + + + + +
+
+
+

{{ server_name }} registration

+

the registration requires to solve a captcha
+ registration does not require an email, just a username and a password that's longer than {{ pw_length }} + characters.

+
+
+
+ +
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ +
+
+ +
+
+
+
+
+ + + + + + + diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..36856ca --- /dev/null +++ b/poetry.lock @@ -0,0 +1,689 @@ +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "captcha" +version = "0.4" +description = "A captcha library that generates audio and image CAPTCHAs." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Pillow = "*" + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "flake8" +version = "4.0.1" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0" + +[[package]] +name = "flask" +version = "1.1.4" +description = "A simple framework for building complex web applications." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +click = ">=5.1,<8.0" +itsdangerous = ">=0.24,<2.0" +Jinja2 = ">=2.10.1,<3.0" +Werkzeug = ">=0.15,<2.0" + +[package.extras] +dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] +docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] +dotenv = ["python-dotenv"] + +[[package]] +name = "flask-cors" +version = "3.0.10" +description = "A Flask extension adding a decorator for CORS support" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Flask = ">=0.9" +Six = "*" + +[[package]] +name = "flask-httpauth" +version = "4.5.0" +description = "HTTP authentication for Flask routes" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +flask = "*" + +[[package]] +name = "flask-sqlalchemy" +version = "2.4.4" +description = "Adds SQLAlchemy support to your Flask application." +category = "main" +optional = false +python-versions = ">= 2.7, != 3.0.*, != 3.1.*, != 3.2.*, != 3.3.*" + +[package.dependencies] +Flask = ">=0.10" +SQLAlchemy = ">=0.8.0" + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "itsdangerous" +version = "1.1.0" +description = "Various helpers to pass data to untrusted environments and back." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "jinja2" +version = "2.11.3" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +name = "markupsafe" +version = "2.0.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "parameterized" +version = "0.8.1" +description = "Parameterized testing with any Python test framework" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +dev = ["jinja2"] + +[[package]] +name = "pillow" +version = "9.1.0" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "psycopg2-binary" +version = "2.9.3" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pycodestyle" +version = "2.8.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyflakes" +version = "2.4.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pyyaml" +version = "5.4.1" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[[package]] +name = "requests" +version = "2.27.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "sqlalchemy" +version = "1.3.24" +description = "Database Abstraction Library" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +mssql = ["pyodbc"] +mssql_pymssql = ["pymssql"] +mssql_pyodbc = ["pyodbc"] +mysql = ["mysqlclient"] +oracle = ["cx-oracle"] +postgresql = ["psycopg2"] +postgresql_pg8000 = ["pg8000 (<1.16.6)"] +postgresql_psycopg2binary = ["psycopg2-binary"] +postgresql_psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql (<1)", "pymysql"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "urllib3" +version = "1.26.9" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "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,<2.0)"] + +[[package]] +name = "waitress" +version = "1.4.4" +description = "Waitress WSGI server" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.extras] +docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.9)"] +testing = ["pytest", "pytest-cover", "coverage (>=5.0)"] + +[[package]] +name = "werkzeug" +version = "1.0.1" +description = "The comprehensive WSGI web application library." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] +watchdog = ["watchdog"] + +[[package]] +name = "wtforms" +version = "2.3.3" +description = "A flexible forms validation and rendering library for Python web development." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +MarkupSafe = "*" + +[package.extras] +email = ["email-validator"] +ipaddress = ["ipaddress"] +locale = ["Babel (>=1.3)"] + +[[package]] +name = "yapf" +version = "0.32.0" +description = "A formatter for Python code." +category = "dev" +optional = false +python-versions = "*" + +[extras] +postgres = [] + +[metadata] +lock-version = "1.1" +python-versions = '^3.8' +content-hash = "14688a3571abc43e9b65a9077b2f0a777d7d2c517c13732c4b2d4ecd37dda303" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +captcha = [ + {file = "captcha-0.4-py3-none-any.whl", hash = "sha256:529941705c01c20143d030805f82a362ba5a2af898e59426acc2c8d649ba034c"}, + {file = "captcha-0.4.tar.gz", hash = "sha256:2ae5e8daac4f1649b57b34328bcc45ba691b5c707fc6fbdd016e213aece9a8b8"}, +] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +flake8 = [ + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, +] +flask = [ + {file = "Flask-1.1.4-py2.py3-none-any.whl", hash = "sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22"}, + {file = "Flask-1.1.4.tar.gz", hash = "sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196"}, +] +flask-cors = [ + {file = "Flask-Cors-3.0.10.tar.gz", hash = "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"}, + {file = "Flask_Cors-3.0.10-py2.py3-none-any.whl", hash = "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438"}, +] +flask-httpauth = [ + {file = "Flask-HTTPAuth-4.5.0.tar.gz", hash = "sha256:395040fda2854df800d15e84bc4a81a5f32f1d4a5e91eee554936f36f330aa29"}, + {file = "Flask_HTTPAuth-4.5.0-py3-none-any.whl", hash = "sha256:e16067ba3378ea366edf8de4b9d55f38c0a0cbddefcc0f777a54b3fce1d99392"}, +] +flask-sqlalchemy = [ + {file = "Flask-SQLAlchemy-2.4.4.tar.gz", hash = "sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5"}, + {file = "Flask_SQLAlchemy-2.4.4-py2.py3-none-any.whl", hash = "sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +itsdangerous = [ + {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, + {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, +] +jinja2 = [ + {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, + {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, +] +markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +parameterized = [ + {file = "parameterized-0.8.1-py2.py3-none-any.whl", hash = "sha256:9cbb0b69a03e8695d68b3399a8a5825200976536fe1cb79db60ed6a4c8c9efe9"}, + {file = "parameterized-0.8.1.tar.gz", hash = "sha256:41bbff37d6186430f77f900d777e5bb6a24928a1c46fb1de692f8b52b8833b5c"}, +] +pillow = [ + {file = "Pillow-9.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea"}, + {file = "Pillow-9.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652"}, + {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a"}, + {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7"}, + {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e"}, + {file = "Pillow-9.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3"}, + {file = "Pillow-9.1.0-cp310-cp310-win32.whl", hash = "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160"}, + {file = "Pillow-9.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033"}, + {file = "Pillow-9.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c"}, + {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165"}, + {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a"}, + {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2"}, + {file = "Pillow-9.1.0-cp37-cp37m-win32.whl", hash = "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244"}, + {file = "Pillow-9.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef"}, + {file = "Pillow-9.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512"}, + {file = "Pillow-9.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477"}, + {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b"}, + {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378"}, + {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e"}, + {file = "Pillow-9.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5"}, + {file = "Pillow-9.1.0-cp38-cp38-win32.whl", hash = "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a"}, + {file = "Pillow-9.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34"}, + {file = "Pillow-9.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717"}, + {file = "Pillow-9.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581"}, + {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6"}, + {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4"}, + {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331"}, + {file = "Pillow-9.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8"}, + {file = "Pillow-9.1.0-cp39-cp39-win32.whl", hash = "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58"}, + {file = "Pillow-9.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595"}, + {file = "Pillow-9.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481"}, + {file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0"}, + {file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8"}, + {file = "Pillow-9.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458"}, + {file = "Pillow-9.1.0.tar.gz", hash = "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97"}, +] +psycopg2-binary = [ + {file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-win32.whl", hash = "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-win32.whl", hash = "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"}, +] +pycodestyle = [ + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +] +pyflakes = [ + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +pyyaml = [ + {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, + {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, + {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, + {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, + {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, + {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, + {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, + {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, + {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, +] +requests = [ + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +sqlalchemy = [ + {file = "SQLAlchemy-1.3.24-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:87a2725ad7d41cd7376373c15fd8bf674e9c33ca56d0b8036add2d634dba372e"}, + {file = "SQLAlchemy-1.3.24-cp27-cp27m-win32.whl", hash = "sha256:f597a243b8550a3a0b15122b14e49d8a7e622ba1c9d29776af741f1845478d79"}, + {file = "SQLAlchemy-1.3.24-cp27-cp27m-win_amd64.whl", hash = "sha256:fc4cddb0b474b12ed7bdce6be1b9edc65352e8ce66bc10ff8cbbfb3d4047dbf4"}, + {file = "SQLAlchemy-1.3.24-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:f1149d6e5c49d069163e58a3196865e4321bad1803d7886e07d8710de392c548"}, + {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:14f0eb5db872c231b20c18b1e5806352723a3a89fb4254af3b3e14f22eaaec75"}, + {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:e98d09f487267f1e8d1179bf3b9d7709b30a916491997137dd24d6ae44d18d79"}, + {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:fc1f2a5a5963e2e73bac4926bdaf7790c4d7d77e8fc0590817880e22dd9d0b8b"}, + {file = "SQLAlchemy-1.3.24-cp35-cp35m-win32.whl", hash = "sha256:f3c5c52f7cb8b84bfaaf22d82cb9e6e9a8297f7c2ed14d806a0f5e4d22e83fb7"}, + {file = "SQLAlchemy-1.3.24-cp35-cp35m-win_amd64.whl", hash = "sha256:0352db1befcbed2f9282e72843f1963860bf0e0472a4fa5cf8ee084318e0e6ab"}, + {file = "SQLAlchemy-1.3.24-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2ed6343b625b16bcb63c5b10523fd15ed8934e1ed0f772c534985e9f5e73d894"}, + {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:34fcec18f6e4b24b4a5f6185205a04f1eab1e56f8f1d028a2a03694ebcc2ddd4"}, + {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e47e257ba5934550d7235665eee6c911dc7178419b614ba9e1fbb1ce6325b14f"}, + {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:816de75418ea0953b5eb7b8a74933ee5a46719491cd2b16f718afc4b291a9658"}, + {file = "SQLAlchemy-1.3.24-cp36-cp36m-win32.whl", hash = "sha256:26155ea7a243cbf23287f390dba13d7927ffa1586d3208e0e8d615d0c506f996"}, + {file = "SQLAlchemy-1.3.24-cp36-cp36m-win_amd64.whl", hash = "sha256:f03bd97650d2e42710fbe4cf8a59fae657f191df851fc9fc683ecef10746a375"}, + {file = "SQLAlchemy-1.3.24-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a006d05d9aa052657ee3e4dc92544faae5fcbaafc6128217310945610d862d39"}, + {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1e2f89d2e5e3c7a88e25a3b0e43626dba8db2aa700253023b82e630d12b37109"}, + {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0d5d862b1cfbec5028ce1ecac06a3b42bc7703eb80e4b53fceb2738724311443"}, + {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:0172423a27fbcae3751ef016663b72e1a516777de324a76e30efa170dbd3dd2d"}, + {file = "SQLAlchemy-1.3.24-cp37-cp37m-win32.whl", hash = "sha256:d37843fb8df90376e9e91336724d78a32b988d3d20ab6656da4eb8ee3a45b63c"}, + {file = "SQLAlchemy-1.3.24-cp37-cp37m-win_amd64.whl", hash = "sha256:c10ff6112d119f82b1618b6dc28126798481b9355d8748b64b9b55051eb4f01b"}, + {file = "SQLAlchemy-1.3.24-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:861e459b0e97673af6cc5e7f597035c2e3acdfb2608132665406cded25ba64c7"}, + {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5de2464c254380d8a6c20a2746614d5a436260be1507491442cf1088e59430d2"}, + {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d375d8ccd3cebae8d90270f7aa8532fe05908f79e78ae489068f3b4eee5994e8"}, + {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:014ea143572fee1c18322b7908140ad23b3994036ef4c0d630110faf942652f8"}, + {file = "SQLAlchemy-1.3.24-cp38-cp38-win32.whl", hash = "sha256:6607ae6cd3a07f8a4c3198ffbf256c261661965742e2b5265a77cd5c679c9bba"}, + {file = "SQLAlchemy-1.3.24-cp38-cp38-win_amd64.whl", hash = "sha256:fcb251305fa24a490b6a9ee2180e5f8252915fb778d3dafc70f9cc3f863827b9"}, + {file = "SQLAlchemy-1.3.24-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:01aa5f803db724447c1d423ed583e42bf5264c597fd55e4add4301f163b0be48"}, + {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d0e3515ef98aa4f0dc289ff2eebb0ece6260bbf37c2ea2022aad63797eacf60"}, + {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:bce28277f308db43a6b4965734366f533b3ff009571ec7ffa583cb77539b84d6"}, + {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8110e6c414d3efc574543109ee618fe2c1f96fa31833a1ff36cc34e968c4f233"}, + {file = "SQLAlchemy-1.3.24-cp39-cp39-win32.whl", hash = "sha256:ee5f5188edb20a29c1cc4a039b074fdc5575337c9a68f3063449ab47757bb064"}, + {file = "SQLAlchemy-1.3.24-cp39-cp39-win_amd64.whl", hash = "sha256:09083c2487ca3c0865dc588e07aeaa25416da3d95f7482c07e92f47e080aa17b"}, + {file = "SQLAlchemy-1.3.24.tar.gz", hash = "sha256:ebbb777cbf9312359b897bf81ba00dae0f5cb69fba2a18265dcc18a6f5ef7519"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +urllib3 = [ + {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, + {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, +] +waitress = [ + {file = "waitress-1.4.4-py2.py3-none-any.whl", hash = "sha256:3d633e78149eb83b60a07dfabb35579c29aac2d24bb803c18b26fb2ab1a584db"}, + {file = "waitress-1.4.4.tar.gz", hash = "sha256:1bb436508a7487ac6cb097ae7a7fe5413aefca610550baf58f0940e51ecfb261"}, +] +werkzeug = [ + {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, + {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, +] +wtforms = [ + {file = "WTForms-2.3.3-py2.py3-none-any.whl", hash = "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c"}, + {file = "WTForms-2.3.3.tar.gz", hash = "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"}, +] +yapf = [ + {file = "yapf-0.32.0-py2.py3-none-any.whl", hash = "sha256:8fea849025584e486fd06d6ba2bed717f396080fd3cc236ba10cb97c4c51cf32"}, + {file = "yapf-0.32.0.tar.gz", hash = "sha256:a3f5085d37ef7e3e004c4ba9f9b3e40c54ff1901cd111f05145ae313a7c67d1b"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c3a668b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,47 @@ +[tool.poetry] +name = 'synapse-captcha' +version = '0.7.2.3' +readme = 'README.md' +repository = 'https://codeberg.org/deathrow/synapse-captcha' +packages = [{include = 'matrix_registration'}] +include = [ + 'matrix_registration/templates/*.html', + 'matrix_registration/static/css/*.css', + 'matrix_registration/static/images/*.jpg', + 'matrix_registration/static/images/*.png', + 'matrix_registration/static/images/*.ico' +] + +[tool.poetry.dependencies] +python = '^3.8' +appdirs = '~=1.4.3' +captcha = '^0.4' +Flask = '~=1.1' +Flask-SQLAlchemy = '~=2.4.1' +Flask-Cors = '~=3.0.7' +Flask-HTTPAuth = '^4.5.0' +python-dateutil = '~=2.8.1' +PyYAML = '~=5.1' +requests = '^2.27.1' +SQLAlchemy = '~=1.3.13' +waitress = '~=1.4.4' +WTForms = '~=2.1' +MarkupSafe = '2.0.1' +psycopg2-binary = "^2.9.3" + +[tool.poetry.dev-dependencies] +parameterized = '^0.8.1' +flake8 = '^4.0.1' +yapf = "^0.32.0" +toml = "^0.10.2" + +[tool.poetry.scripts] +matrix-registration = 'matrix_registration.app:cli' + +[tool.poetry.extras] +postgres = ['psycopg2-binary>=2.8.4'] + + +[build-system] +requires = ['poetry-core>=1.0.0'] +build-backend = 'poetry.core.masonry.api' diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/context.py b/tests/context.py new file mode 100644 index 0000000..377566a --- /dev/null +++ b/tests/context.py @@ -0,0 +1,7 @@ +import os +import sys + +sys.path.insert(0, + os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +import matrix_registration diff --git a/tests/test_registration.py b/tests/test_registration.py new file mode 100644 index 0000000..fe6f172 --- /dev/null +++ b/tests/test_registration.py @@ -0,0 +1,389 @@ +# -*- coding: utf-8 -*- +# Standard library imports... +import hashlib +import hmac +import logging +import logging.config +import json +import os +import yaml +import random +import re +from requests import exceptions +import string +import sys +import time +import unittest +from unittest.mock import patch +from urllib.parse import urlparse + +# Third-party imports... +from parameterized import parameterized +from dateutil import parser + +# Local imports... +try: + from .context import matrix_registration +except ModuleNotFoundError: + from context import matrix_registration +from matrix_registration.config import Config +from matrix_registration.app import create_app +from matrix_registration.captcha import db + +logger = logging.getLogger(__name__) + +LOGGING = { + "version": 1, + "root": { + "level": "NOTSET", + "handlers": ["console"] + }, + "formatters": { + "precise": { + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + } + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "level": "NOTSET", + "formatter": "precise", + "stream": "ext://sys.stdout" + } + } +} + +GOOD_CONFIG = { + 'server_location': 'https://righths.org', + 'shared_secret': 'coolsharesecret', + 'admin_secret': 'coolpassword', + 'db': 'sqlite:///%s/tests/db.sqlite' % (os.getcwd(), ), + 'port': 5000, + 'password': { + 'min_length': 8 + }, + 'logging': LOGGING +} + +BAD_CONFIG1 = dict( # wrong matrix server location -> 500 + GOOD_CONFIG.items(), + server_location='https://wronghs.org', +) + +BAD_CONFIG2 = dict( # wrong admin secret password -> 401 + GOOD_CONFIG.items(), + admin_secret='wrongpassword', +) + +BAD_CONFIG3 = dict( # wrong matrix shared password -> 500 + GOOD_CONFIG.items(), + shared_secret='wrongsecret', +) + +usernames = [] +nonces = [] +logging.config.dictConfig(LOGGING) + + +def mock_new_user(username): + access_token = ''.join( + random.choices(string.ascii_lowercase + string.digits, k=256)) + device_id = ''.join(random.choices(string.ascii_uppercase, k=8)) + home_server = matrix_registration.config.config.server_location + username = username.rsplit(":")[0].split("@")[-1] + user_id = "@{}:{}".format(username, home_server) + usernames.append(username) + + user = { + 'access_token': access_token, + 'device_id': device_id, + 'home_server': home_server, + 'user_id': user_id + } + return user + + +def mocked__get_nonce(server_location): + nonce = ''.join( + random.choices(string.ascii_lowercase + string.digits, k=129)) + nonces.append(nonce) + return nonce + + +def mocked_requests_post(*args, **kwargs): + + class MockResponse: + + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + + def json(self): + return self.json_data + + def raise_for_status(self): + if self.status_code == 200: + return self.status_code + else: + raise exceptions.HTTPError(response=self) + + # print(args[0]) + # print(matrix_registration.config.config.server_location) + domain = urlparse(GOOD_CONFIG['server_location']).hostname + re_mxid = r"^@?[a-zA-Z_\-=\.\/0-9]+(:" + \ + re.escape(domain) + \ + r")?$" + location = '_synapse/admin/v1/register' + + if args[0] == '%s/%s' % (GOOD_CONFIG['server_location'], location): + if kwargs: + req = kwargs['json'] + if not req['nonce'] in nonces: + return MockResponse( + {"'errcode': 'M_UNKOWN", "'error': 'unrecognised nonce'"}, + 400) + + mac = hmac.new( + key=str.encode(GOOD_CONFIG['shared_secret']), + digestmod=hashlib.sha1, + ) + + mac.update(req['nonce'].encode()) + mac.update(b'\x00') + mac.update(req['username'].encode()) + mac.update(b'\x00') + mac.update(req['password'].encode()) + mac.update(b'\x00') + mac.update(b'admin' if req['admin'] else b'notadmin') + mac = mac.hexdigest() + if not re.search(re_mxid, req['username']): + return MockResponse( + { + "'errcode': 'M_INVALID_USERNAME", + "'error': 'User ID can only contain" + + "characters a-z, 0-9, or '=_-./'" + }, 400) + if req['username'].rsplit(":")[0].split("@")[-1] in usernames: + return MockResponse( + { + 'errcode': 'M_USER_IN_USE', + 'error': 'User ID already taken.' + }, 400) + if req['mac'] != mac: + return MockResponse( + { + 'errcode': 'M_UNKNOWN', + 'error': 'HMAC incorrect' + }, 403) + return MockResponse(mock_new_user(req['username']), 200) + return MockResponse(None, 404) + + +class TokensTest(unittest.TestCase): + + def setUp(self): + matrix_registration.config.config = Config(GOOD_CONFIG) + app = create_app(testing=True) + with app.app_context(): + app.config.from_mapping( + SQLALCHEMY_DATABASE_URI=matrix_registration.config.config.db, + SQLALCHEMY_TRACK_MODIFICATIONS=False) + db.init_app(app) + db.create_all() + + self.app = app + + def tearDown(self): + os.remove(matrix_registration.config.config.db[10:]) + + def test_captcha_valid(self): + with self.app.app_context(): + test_captcha_gen = matrix_registration.captcha.CaptchaGenerator() + test_captcha = test_captcha_gen.generate() + # validate that the captcha is correct + self.assertTrue( + test_captcha_gen.validate(test_captcha['captcha_answer'], + test_captcha['captcha_token'])) + # captcha can only be used once + self.assertFalse( + test_captcha_gen.validate(test_captcha['captcha_answer'], + test_captcha['captcha_token'])) + + def test_captcha_empty(self): + with self.app.app_context(): + test_captcha_gen = matrix_registration.captcha.CaptchaGenerator() + # no captcha should exist at this point + self.assertFalse(test_captcha_gen.validate("", "")) + test_captcha = test_captcha_gen.generate() + # no empty captcha should have been created + self.assertFalse(test_captcha_gen.validate("", "")) + + def test_captcha_clean(self): + with self.app.app_context(): + test_captcha_gen = matrix_registration.captcha.CaptchaGenerator() + valid_captcha = test_captcha_gen.generate() + # validate a wrong captcha + self.assertFalse( + test_captcha_gen.validate("WRONG", + valid_captcha['captcha_token'])) + # valid captcha should be removed when it was wrong + self.assertFalse( + test_captcha_gen.validate(valid_captcha['captcha_answer'], + valid_captcha['captcha_token'])) + timeout = matrix_registration.captcha.CAPTCHA_TIMEOUT + matrix_registration.captcha.CAPTCHA_TIMEOUT = 0 + try: + valid_captcha = test_captcha_gen.generate() + time.sleep(1) + # captcha older than the timeout value should not be valid + self.assertFalse( + test_captcha_gen.validate(valid_captcha['captcha_answer'], + valid_captcha['captcha_token'])) + finally: + matrix_registration.captcha.CAPTCHA_TIMEOUT = timeout + + +class ApiTest(unittest.TestCase): + + def setUp(self): + matrix_registration.config.config = Config(GOOD_CONFIG) + app = create_app(testing=True) + with app.app_context(): + app.config.from_mapping( + SQLALCHEMY_DATABASE_URI=matrix_registration.config.config.db, + SQLALCHEMY_TRACK_MODIFICATIONS=False) + db.init_app(app) + db.create_all() + self.client = app.test_client() + self.app = app + + def tearDown(self): + os.remove(matrix_registration.config.config.db[10:]) + + @parameterized.expand( + [['test1', 'test1234', 'test1234', True, 200], + [None, 'test1234', 'test1234', True, 400], + ['test2', None, 'test1234', True, 400], + ['test3', 'test1234', None, True, 400], + ['test4', 'test1234', 'test1234', False, 400], + ['@test5:matrix.org', 'test1234', 'test1234', True, 200], + ['@test6:wronghs.org', 'test1234', 'test1234', True, 400], + ['test7', 'test1234', 'tet1234', True, 400], + ['teĆ¼st8', 'test1234', 'test1234', True, 400], + ['@test9@matrix.org', 'test1234', 'test1234', True, 400], + ['test11@matrix.org', 'test1234', 'test1234', True, 400], + ['', 'test1234', 'test1234', True, 400], + [ + ''.join(random.choices(string.ascii_uppercase, k=256)), + 'test1234', 'test1234', True, 400 + ]]) + # check form validators + @patch('matrix_registration.matrix_api._get_nonce', + side_effect=mocked__get_nonce) + @patch('matrix_registration.matrix_api.requests.post', + side_effect=mocked_requests_post) + def test_register(self, username, password, confirm, captcha, status, + mock_get, mock_nonce): + matrix_registration.config.config = Config(GOOD_CONFIG) + with self.app.app_context(): + matrix_registration.captcha.captcha = matrix_registration.captcha.CaptchaGenerator( + ) + test_captcha = matrix_registration.captcha.captcha.generate() + + # replace matrix with in config set hs + domain = urlparse( + matrix_registration.config.config.server_location).hostname + if username: + username = username.replace("matrix.org", domain) + + if not captcha: + test_captcha['captcha_answer'] = "" + rv = self.client.post( + '/register', + data=dict(username=username, + password=password, + confirm=confirm, + captcha_answer=test_captcha['captcha_answer'], + captcha_token=test_captcha['captcha_token'])) + if rv.status_code == 200: + account_data = json.loads( + rv.data.decode('utf8').replace("'", '"')) + # print(account_data) + self.assertEqual(rv.status_code, status) + + @patch('matrix_registration.matrix_api._get_nonce', + side_effect=mocked__get_nonce) + @patch('matrix_registration.matrix_api.requests.post', + side_effect=mocked_requests_post) + def test_register_wrong_hs(self, mock_get, mock_nonce): + matrix_registration.config.config = Config(BAD_CONFIG1) + + with self.app.app_context(): + matrix_registration.captcha.captcha = matrix_registration.captcha.CaptchaGenerator( + ) + test_captcha = matrix_registration.captcha.captcha.generate() + rv = self.client.post( + '/register', + data=dict(username='username', + password='password', + confirm='password', + captcha_answer=test_captcha['captcha_answer'], + captcha_token=test_captcha['captcha_token'])) + self.assertEqual(rv.status_code, 500) + + @patch('matrix_registration.matrix_api._get_nonce', + side_effect=mocked__get_nonce) + @patch('matrix_registration.matrix_api.requests.post', + side_effect=mocked_requests_post) + def test_register_wrong_secret(self, mock_get, mock_nonce): + matrix_registration.config.config = Config(BAD_CONFIG3) + + with self.app.app_context(): + matrix_registration.captcha.captcha = matrix_registration.captcha.CaptchaGenerator( + ) + test_captcha = matrix_registration.captcha.captcha.generate() + rv = self.client.post( + '/register', + data=dict(username='username', + password='password', + confirm='password', + captcha_answer=test_captcha['captcha_answer'], + captcha_token=test_captcha['captcha_token'])) + self.assertEqual(rv.status_code, 500) + + +class ConfigTest(unittest.TestCase): + + def test_config_update(self): + matrix_registration.config.config = Config(GOOD_CONFIG) + self.assertEqual(matrix_registration.config.config.port, + GOOD_CONFIG['port']) + self.assertEqual(matrix_registration.config.config.server_location, + GOOD_CONFIG['server_location']) + + matrix_registration.config.config.update(BAD_CONFIG1) + self.assertEqual(matrix_registration.config.config.port, + BAD_CONFIG1['port']) + self.assertEqual(matrix_registration.config.config.server_location, + BAD_CONFIG1['server_location']) + + def test_config_path(self): + # BAD_CONFIG1_path = "x" + good_config_path = "tests/test_config.yaml" + + with open(good_config_path, 'w') as outfile: + yaml.dump(GOOD_CONFIG, outfile, default_flow_style=False) + + matrix_registration.config.config = Config(good_config_path) + self.assertIsNotNone(matrix_registration.config.config) + os.remove(good_config_path) + + +# TODO: - tests for /token/ +# - a nonce is only valid for 60s + +if "logging" in sys.argv: + logging.basicConfig(level=logging.DEBUG) + +if __name__ == '__main__': + unittest.main()