Add Code
31
Dockerfile
Normal file
@ -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"]
|
59
README.md
Normal file
@ -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
|
||||
```
|
38
config.sample.yaml
Normal file
@ -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
|
5
matrix_registration/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from . import api
|
||||
from . import config
|
||||
from . import captcha
|
||||
|
||||
name = 'matrix_registration'
|
194
matrix_registration/api.py
Normal file
@ -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())
|
64
matrix_registration/app.py
Normal file
@ -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()
|
71
matrix_registration/captcha.py
Normal file
@ -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
|
128
matrix_registration/config.py
Normal file
@ -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
|
19
matrix_registration/constants.py
Normal file
@ -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/')
|
75
matrix_registration/matrix_api.py
Normal file
@ -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']
|
300
matrix_registration/static/css/style.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
BIN
matrix_registration/static/images/element-logo.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
matrix_registration/static/images/favicon.ico
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
matrix_registration/static/images/icon.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
matrix_registration/static/images/icon32x32.png
Normal file
After Width: | Height: | Size: 849 B |
BIN
matrix_registration/static/images/riot.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
matrix_registration/static/images/tile.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
matrix_registration/static/images/valley.jpg
Normal file
After Width: | Height: | Size: 530 KiB |
234
matrix_registration/templates/register.html
Normal file
@ -0,0 +1,234 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width; initial-scale=1.0;" />
|
||||
<meta property="og:title" content="{{ server_name }} registration">
|
||||
<meta property="og:site_name" content="{{ server_name }}">
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="og:description" content="registrate an account on {{ server_name }}" />
|
||||
<meta name="og:image" content="{{ url_for('static', filename='images/icon.png') }}" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='images/icon.png') }}">
|
||||
<link rel="icon" type="image/png" href="{{ url_for('static', filename='images/icon32x32.png') }}" sizes="32x32">
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='images/favicon.ico') }}">
|
||||
<meta name="msapplication-TileImage" content="{{ url_for('static', filename='images/tile.png') }}">
|
||||
<meta name="msapplication-TileColor" content="#fff">
|
||||
<title>{{ server_name }} registration</title>
|
||||
<style>
|
||||
body, .widget::before {
|
||||
background-image: url("https://{{ server_name }}/background.jpg");
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
|
||||
<body class="blur">
|
||||
<article class="widget register">
|
||||
<div class="center">
|
||||
<header>
|
||||
<h1>{{ server_name }} registration</h1>
|
||||
<p>the registration requires to solve a captcha<br>
|
||||
registration does not require an email, just a username and a password that's longer than {{ pw_length }}
|
||||
characters.</p>
|
||||
</header>
|
||||
<section>
|
||||
<form id="registration" action="{{ base_url }}/register" method="post">
|
||||
<input type="hidden" id="captcha_token" name="captcha_token" value="{{ captcha_token }}" />
|
||||
<div class="group">
|
||||
<input id="username" name="username" type="text" placeholder=" " required
|
||||
pattern="^@?[a-zA-Z_\-=\.\/0-9]+(:{{ server_name|replace('.', '\.') }})?$" required minlength="1"
|
||||
maxlength="200">
|
||||
<span class="highlight"></span>
|
||||
<span class="bar"></span>
|
||||
<label for="username">Username</label>
|
||||
</div>
|
||||
<div class="group">
|
||||
<input id="password" name="password" type="password" placeholder=" " required minlength="{{ pw_length }}"
|
||||
maxlength="128">
|
||||
<span class="highlight"></span>
|
||||
<span class="bar"></span>
|
||||
<label for="password">Password</label>
|
||||
</div>
|
||||
<div class="group">
|
||||
<input id="confirm_password" name="confirm" type="password" placeholder=" " required>
|
||||
<span class="highlight"></span>
|
||||
<span class="bar"></span>
|
||||
<label for="confirm_password">Confirm</label>
|
||||
</div>
|
||||
<div class="group">
|
||||
<input id="captcha_answer" name="captcha_answer" type="text" placeholder=" " required >
|
||||
<span class="highlight"></span>
|
||||
<span class="bar"></span>
|
||||
<label for="captcha">Captcha</label>
|
||||
</div>
|
||||
<div>
|
||||
<img id="captcha_image" src="data:image/png;base64,{{ captcha_image }}" />
|
||||
</div>
|
||||
<div class="btn-box">
|
||||
<input class="btn btn-submit" type="submit" value="register">
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
<article id="success" class="widget modal hidden">
|
||||
<div class="center">
|
||||
<header>
|
||||
<h2 id="welcome"></h2>
|
||||
</header>
|
||||
<section>
|
||||
<p>Click here to login in:</p>
|
||||
<h3><a href="{{ riot_instance }}"><img src="static/images/element-logo.png" height="100px"></a></h3>
|
||||
<p>or choose one of the many other clients here: <a href="https://matrix.org/docs/projects/clients-matrix"
|
||||
a>https://matrix.org/docs/projects/clients-matrix</a></p>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
<article id="error" class="widget modal hidden">
|
||||
<div class="center">
|
||||
<header>
|
||||
<h2>Error</h2>
|
||||
</header>
|
||||
<section>
|
||||
<p>There was an error while trying to register you.</p>
|
||||
<h3 id="error_message" class="error"></h3>
|
||||
<p id="error_dialog"></p>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<script>
|
||||
// all javascript here is optional, the registration form works fine without
|
||||
/*
|
||||
What this script does:
|
||||
- confirm password validator needs javascript, otherwise always valid as long as not empty
|
||||
- set token with ?token query parameter
|
||||
- set custom validity messages
|
||||
*/
|
||||
|
||||
// see https://stackoverflow.com/a/3028037
|
||||
function hideOnClickOutside(element) {
|
||||
const outsideClickListener = event => {
|
||||
if (!element.contains(event.target) && isVisible(
|
||||
element)) {
|
||||
element.classList.add("hidden");
|
||||
removeClickListener()
|
||||
}
|
||||
}
|
||||
|
||||
const removeClickListener = () => {
|
||||
document.removeEventListener("click", outsideClickListener)
|
||||
}
|
||||
document.addEventListener("click", outsideClickListener)
|
||||
}
|
||||
|
||||
const isVisible = elem => !!elem && !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length)
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// html5 validators
|
||||
var username = document.getElementById("username");
|
||||
var password = document.getElementById("password");
|
||||
var confirm_password = document.getElementById("confirm_password");
|
||||
|
||||
username.addEventListener("input", function (event) {
|
||||
if (username.validity.typeMismatch) {
|
||||
username.setCustomValidity("format: @username:{{ server_name }}");
|
||||
} else {
|
||||
username.setCustomValidity("");
|
||||
}
|
||||
});
|
||||
|
||||
password.addEventListener("input", function (event) {
|
||||
if (password.validity.typeMismatch) {
|
||||
password.setCustomValidity("atleast {{ pw_length }} characters long");
|
||||
} else {
|
||||
password.setCustomValidity("");
|
||||
}
|
||||
});
|
||||
|
||||
function validatePassword() {
|
||||
if (password.value != confirm_password.value) {
|
||||
confirm_password.setCustomValidity("passwords don't match");
|
||||
} else {
|
||||
confirm_password.setCustomValidity("");
|
||||
}
|
||||
}
|
||||
|
||||
password.onchange = validatePassword;
|
||||
confirm_password.onkeyup = validatePassword;
|
||||
|
||||
function showError(message, dialog) {
|
||||
document.getElementById("error_message").innerHTML = message;
|
||||
document.getElementById("error_dialog").innerHTML = dialog;
|
||||
let error = document.getElementById("error");
|
||||
error.classList.remove("hidden");
|
||||
hideOnClickOutside(error);
|
||||
}
|
||||
|
||||
// hijack the submit button to display the json response in a neat modal
|
||||
var form = document.getElementById("registration");
|
||||
|
||||
function sendData() {
|
||||
let XHR = new XMLHttpRequest();
|
||||
|
||||
// Bind the FormData object and the form element
|
||||
let FD = new FormData(form);
|
||||
|
||||
// Define what happens on successful data submission
|
||||
XHR.addEventListener("load", function (event) {
|
||||
console.log(XHR.responseText);
|
||||
let response = JSON.parse(XHR.responseText);
|
||||
try {
|
||||
console.log(response);
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
showError("Internal Server Error!", "Please contact the server admin about this.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ("errcode" in response) {
|
||||
if (response["errcode"] == "MR_BAD_USER_REQUEST") {
|
||||
if ("captcha_answer" in response["error"]) {
|
||||
showError("Captcha Error", response["error"]["captcha_answer"][0]);
|
||||
} else if ("password" in response["error"]) {
|
||||
showError("Password Error", response["error"]["password"][0]);
|
||||
} else if ("username" in response["error"]) {
|
||||
showError("Username Error", response["error"]["username"][0]);
|
||||
}
|
||||
document.getElementById("captcha_image").src = "data:image/png;base64," + response["captcha_image"]
|
||||
document.getElementById("captcha_token").value = response["captcha_token"]
|
||||
return;
|
||||
} else {
|
||||
showError("Homeserver Error", response["error"]);
|
||||
}
|
||||
} else {
|
||||
document.getElementById("welcome").innerHTML = "Welcome " + response['user_id'];
|
||||
document.getElementById("success").classList.remove("hidden");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Define what happens in case of error
|
||||
XHR.addEventListener("error", function (event) {
|
||||
showError("Internal Server Error!", "Please contact the server admin about this.");
|
||||
});
|
||||
|
||||
// Set up our request
|
||||
XHR.open("POST", "{{ base_url }}/register");
|
||||
|
||||
// The data sent is what the user provided in the form
|
||||
XHR.send(FD);
|
||||
}
|
||||
|
||||
// take over its submit event.
|
||||
form.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
sendData();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
689
poetry.lock
generated
Normal file
@ -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"},
|
||||
]
|
47
pyproject.toml
Normal file
@ -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'
|
0
tests/__init__.py
Normal file
7
tests/context.py
Normal file
@ -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
|
389
tests/test_registration.py
Normal file
@ -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/<token>
|
||||
# - a nonce is only valid for 60s
|
||||
|
||||
if "logging" in sys.argv:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|