2037: update python dependencies of admin container r=mergify[bot] a=ghostwheel42

## What type of PR?

updates python dependencies of admin container

## What does this PR do?

### Related issue(s)

## Prerequisites
Before we can consider review and merge, please make sure the following list is done and checked.
If an entry in not applicable, you can check it or remove it from the list.

- [X] In case of feature or enhancement: documentation updated accordingly
- [X] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
master
bors[bot] 3 years ago committed by GitHub
commit 1675399047
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -27,7 +27,7 @@ ENV TZ Etc/UTC
# python3 shared with most images
RUN set -eu \
&& apk add --no-cache python3 py3-pip git bash tzdata \
&& apk add --no-cache python3 py3-pip py3-wheel git bash tzdata \
&& pip3 install --upgrade pip
RUN mkdir -p /app
@ -37,13 +37,15 @@ COPY requirements-prod.txt requirements.txt
RUN set -eu \
&& apk add --no-cache libressl curl postgresql-libs mariadb-connector-c \
&& apk add --no-cache --virtual build-dep libressl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev cargo \
&& pip3 install -r requirements.txt \
&& pip install --upgrade pip \
&& pip install -r requirements.txt \
&& apk del --no-cache build-dep
COPY --from=assets static ./mailu/static
COPY mailu ./mailu
COPY migrations ./migrations
COPY start.py /start.py
COPY audit.py /audit.py
RUN pybabel compile -d mailu/translations

@ -52,3 +52,8 @@ fieldset:disabled .form-control:disabled {
.select2-container--default .select2-selection--multiple .select2-selection__choice {
color: black;
}
/* range input spacing */
.input-group-text {
margin-right: 1em;
}

@ -18,7 +18,7 @@ $('document').ready(function() {
$.post({
url: $(this).attr('href'),
success: function() {
location.reload();
window.location = window.location.href;
},
});
});
@ -28,10 +28,10 @@ $('document').ready(function() {
var fieldset = $(this).parents('fieldset');
if (this.checked) {
fieldset.removeAttr('disabled');
fieldset.find('input').not(this).removeAttr('disabled');
fieldset.find('input,textarea').not(this).removeAttr('disabled');
} else {
fieldset.attr('disabled', '');
fieldset.find('input').not(this).attr('disabled', '');
fieldset.find('input,textarea').not(this).attr('disabled', '');
}
});
@ -43,7 +43,9 @@ $('document').ready(function() {
var infinity = $(this).data('infinity');
var step = $(this).attr('step');
$(this).on('input', function() {
value_element.text((infinity && this.value == 0) ? '∞' : (this.value/step).toFixed(2));
var num = (infinity && this.value == 0) ? '∞' : (this.value/step).toFixed(2);
if (num.endsWith('.00')) num = num.substr(0, num.length - 3);
value_element.text(num);
}).trigger('input');
}
});

@ -1,14 +1,19 @@
from mailu import app
#!/usr/bin/python3
import sys
import tabulate
sys.path[0:0] = ['/app']
import mailu
app = mailu.create_app()
# Known endpoints without permissions
known_missing_permissions = [
"index",
"static", "bootstrap.static",
"admin.static", "admin.login"
'index',
'static', 'bootstrap.static',
'admin.static', 'admin.login'
]
@ -16,7 +21,7 @@ known_missing_permissions = [
missing_permissions = []
permissions = {}
for endpoint, function in app.view_functions.items():
audit = function.__dict__.get("_audit_permissions")
audit = function.__dict__.get('_audit_permissions')
if audit:
handler, args = audit
if args:
@ -28,16 +33,15 @@ for endpoint, function in app.view_functions.items():
elif endpoint not in known_missing_permissions:
missing_permissions.append(endpoint)
# Fail if any endpoint is missing a permission check
if missing_permissions:
print("The following endpoints are missing permission checks:")
print(missing_permissions.join(","))
sys.exit(1)
# Display the permissions table
print(tabulate.tabulate([
[route, *permissions[route.endpoint]]
for route in app.url_map.iter_rules() if route.endpoint in permissions
]))
# Warn if any endpoint is missing a permission check
if missing_permissions:
print()
print('The following endpoints are missing permission checks:')
print(','.join(missing_permissions))

@ -33,7 +33,7 @@ def create_app_from_config(config):
app.srs_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('SRS_KEY', 'utf-8'), 'sha256').digest()
# Initialize list of translations
config.translations = {
app.config.translations = {
str(locale): locale
for locale in sorted(
utils.babel.list_translations(),
@ -57,6 +57,15 @@ def create_app_from_config(config):
config = app.config,
)
# Jinja filters
@app.template_filter()
def format_date(value):
return utils.flask_babel.format_date(value) if value else ''
@app.template_filter()
def format_datetime(value):
return utils.flask_babel.format_datetime(value) if value else ''
# Import views
from mailu import ui, internal, sso
app.register_blueprint(ui.ui, url_prefix=app.config['WEB_ADMIN'])

@ -90,7 +90,7 @@ DEFAULT_CONFIG = {
'POD_ADDRESS_RANGE': None
}
class ConfigManager(dict):
class ConfigManager:
""" Naive configuration manager that uses environment only
"""
@ -105,19 +105,16 @@ class ConfigManager(dict):
def get_host_address(self, name):
# if MYSERVICE_ADDRESS is defined, use this
if '{}_ADDRESS'.format(name) in os.environ:
return os.environ.get('{}_ADDRESS'.format(name))
if f'{name}_ADDRESS' in os.environ:
return os.environ.get(f'{name}_ADDRESS')
# otherwise use the host name and resolve it
return system.resolve_address(self.config['HOST_{}'.format(name)])
return system.resolve_address(self.config[f'HOST_{name}'])
def resolve_hosts(self):
self.config["IMAP_ADDRESS"] = self.get_host_address("IMAP")
self.config["POP3_ADDRESS"] = self.get_host_address("POP3")
self.config["AUTHSMTP_ADDRESS"] = self.get_host_address("AUTHSMTP")
self.config["SMTP_ADDRESS"] = self.get_host_address("SMTP")
self.config["REDIS_ADDRESS"] = self.get_host_address("REDIS")
if self.config["WEBMAIL"] != "none":
self.config["WEBMAIL_ADDRESS"] = self.get_host_address("WEBMAIL")
for key in ['IMAP', 'POP3', 'AUTHSMTP', 'SMTP', 'REDIS']:
self.config[f'{key}_ADDRESS'] = self.get_host_address(key)
if self.config['WEBMAIL'] != 'none':
self.config['WEBMAIL_ADDRESS'] = self.get_host_address('WEBMAIL')
def __get_env(self, key, value):
key_file = key + "_FILE"
@ -136,6 +133,7 @@ class ConfigManager(dict):
return value
def init_app(self, app):
# get current app config
self.config.update(app.config)
# get environment variables
self.config.update({
@ -149,9 +147,9 @@ class ConfigManager(dict):
template = self.DB_TEMPLATES[self.config['DB_FLAVOR']]
self.config['SQLALCHEMY_DATABASE_URI'] = template.format(**self.config)
self.config['RATELIMIT_STORAGE_URL'] = 'redis://{0}/2'.format(self.config['REDIS_ADDRESS'])
self.config['QUOTA_STORAGE_URL'] = 'redis://{0}/1'.format(self.config['REDIS_ADDRESS'])
self.config['SESSION_STORAGE_URL'] = 'redis://{0}/3'.format(self.config['REDIS_ADDRESS'])
self.config['RATELIMIT_STORAGE_URL'] = f'redis://{self.config["REDIS_ADDRESS"]}/2'
self.config['QUOTA_STORAGE_URL'] = f'redis://{self.config["REDIS_ADDRESS"]}/1'
self.config['SESSION_STORAGE_URL'] = f'redis://{self.config["REDIS_ADDRESS"]}/3'
self.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
self.config['SESSION_COOKIE_HTTPONLY'] = True
self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME']))
@ -160,25 +158,7 @@ class ConfigManager(dict):
self.config['MESSAGE_RATELIMIT_EXEMPTION'] = set([s for s in self.config['MESSAGE_RATELIMIT_EXEMPTION'].lower().replace(' ', '').split(',') if s])
self.config['HOSTNAMES'] = ','.join(hostnames)
self.config['HOSTNAME'] = hostnames[0]
# update the app config itself
app.config = self
def setdefault(self, key, value):
if key not in self.config:
self.config[key] = value
return self.config[key]
# update the app config
app.config.update(self.config)
def get(self, *args):
return self.config.get(*args)
def keys(self):
return self.config.keys()
def __getitem__(self, key):
return self.config.get(key)
def __setitem__(self, key, value):
self.config[key] = value
def __contains__(self, key):
return key in self.config

@ -1,6 +1,6 @@
import flask_debugtoolbar
from werkzeug.contrib import profiler as werkzeug_profiler
from werkzeug.middleware.profiler import ProfilerMiddleware
# Debugging toolbar
@ -10,7 +10,7 @@ toolbar = flask_debugtoolbar.DebugToolbarExtension()
# Profiler
class Profiler(object):
def init_app(self, app):
app.wsgi_app = werkzeug_profiler.ProfilerMiddleware(
app.wsgi_app = ProfilerMiddleware(
app.wsgi_app, restrictions=[30]
)

@ -19,7 +19,8 @@ import os
import hmac
import smtplib
import idna
import dns
import dns.resolver
import dns.exception
from flask import current_app as app
from sqlalchemy.ext import declarative
@ -38,6 +39,8 @@ class IdnaDomain(db.TypeDecorator):
"""
impl = db.String(80)
cache_ok = True
python_type = str
def process_bind_param(self, value, dialect):
""" encode unicode domain name to punycode """
@ -47,13 +50,13 @@ class IdnaDomain(db.TypeDecorator):
""" decode punycode domain name to unicode """
return idna.decode(value)
python_type = str
class IdnaEmail(db.TypeDecorator):
""" Stores a Unicode string in it's IDNA representation (ASCII only)
"""
impl = db.String(255)
cache_ok = True
python_type = str
def process_bind_param(self, value, dialect):
""" encode unicode domain part of email address to punycode """
@ -69,13 +72,13 @@ class IdnaEmail(db.TypeDecorator):
localpart, domain_name = value.rsplit('@', 1)
return f'{localpart}@{idna.decode(domain_name)}'
python_type = str
class CommaSeparatedList(db.TypeDecorator):
""" Stores a list as a comma-separated string, compatible with Postfix.
"""
impl = db.String
cache_ok = True
python_type = list
def process_bind_param(self, value, dialect):
""" join list of items to comma separated string """
@ -90,13 +93,13 @@ class CommaSeparatedList(db.TypeDecorator):
""" split comma separated string to list """
return list(filter(bool, (item.strip() for item in value.split(',')))) if value else []
python_type = list
class JSONEncoded(db.TypeDecorator):
""" Represents an immutable structure as a json-encoded string.
"""
impl = db.String
cache_ok = True
python_type = str
def process_bind_param(self, value, dialect):
""" encode data as json """
@ -106,8 +109,6 @@ class JSONEncoded(db.TypeDecorator):
""" decode json to data """
return json.loads(value) if value else None
python_type = str
class Base(db.Model):
""" Base class for all models
"""

@ -145,6 +145,11 @@ class Logger:
if history.has_changes() and history.deleted:
before = history.deleted[-1]
after = getattr(target, attr.key)
# we don't have ordered lists
if isinstance(before, list):
before = set(before)
if isinstance(after, list):
after = set(after)
# TODO: this can be removed when comment is not nullable in model
if attr.key == 'comment' and not before and not after:
pass

@ -5,7 +5,7 @@
<form class="form" method="post" role="form">
{{ macros.form_field(form.email) }}
{{ macros.form_field(form.pw) }}
{{ macros.form_fields(fields, label=False, class="btn btn-default", spacing=False) }}
{{ macros.form_fields(fields, label=False, class="btn btn-default") }}
</form>
{%- endcall %}
{%- endblock %}

@ -19,6 +19,7 @@ def login():
fields.append(form.submitAdmin)
if str(app.config["WEBMAIL"]).upper() != "NONE":
fields.append(form.submitWebmail)
fields = [fields]
if form.validate_on_submit():
if form.submitAdmin.data:

@ -34,8 +34,8 @@
<td>{{ alias }}</td>
<td>{{ alias.destination|join(', ') or '-' }}</td>
<td>{{ alias.comment or '' }}</td>
<td>{{ alias.created_at }}</td>
<td>{{ alias.updated_at or '' }}</td>
<td>{{ alias.created_at | format_date }}</td>
<td>{{ alias.updated_at | format_date }}</td>
</tr>
{%- endfor %}
</tbody>

@ -19,6 +19,7 @@
<th>{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Name{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th>
</tr>
</thead>
<tbody>
@ -28,7 +29,8 @@
<a href="{{ url_for('.alternative_delete', alternative=alternative.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>
<td>{{ alternative }}</td>
<td>{{ alternative.created_at }}</td>
<td>{{ alternative.created_at | format_date }}</td>
<td>{{ alternative.updated_at | format_date }}</td>
</tr>
{%- endfor %}
</tbody>

@ -46,8 +46,8 @@
<td>{{ domain.users | count }} / {{ '∞' if domain.max_users == -1 else domain.max_users }}</td>
<td>{{ domain.aliases | count }} / {{ '∞' if domain.max_aliases == -1 else domain.max_aliases }}</td>
<td>{{ domain.comment or '' }}</td>
<td>{{ domain.created_at }}</td>
<td>{{ domain.updated_at or '' }}</td>
<td>{{ domain.created_at | format_date }}</td>
<td>{{ domain.updated_at | format_date }}</td>
</tr>
{%- endfor %}
</tbody>

@ -36,10 +36,10 @@
<td>{{ fetch.protocol }}{{ 's' if fetch.tls else '' }}://{{ fetch.host }}:{{ fetch.port }}</td>
<td>{{ fetch.username }}</td>
<td>{% if fetch.keep %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td>
<td>{{ fetch.last_check or '-' }}</td>
<td>{{ fetch.last_check | format_datetime or '-' }}</td>
<td>{{ fetch.error or '-' }}</td>
<td>{{ fetch.created_at }}</td>
<td>{{ fetch.updated_at or '' }}</td>
<td>{{ fetch.created_at | format_date }}</td>
<td>{{ fetch.updated_at | format_date }}</td>
</tr>
{%- endfor %}
</tbody>

@ -18,17 +18,19 @@
{%- endif %}
{%- endmacro %}
{%- macro form_fields(fields, prepend='', append='', label=True, spacing=True) %}
{%- if spacing %}
{%- macro form_fields(fields, prepend='', append='', label=True) %}
{%- set width = (12 / fields|length)|int %}
{%- else %}
{%- set width = 0 %}
{% endif %}
<div class="form-group">
<div class="row">
{%- for field in fields %}
<div class="col-lg-{{ width }} col-xs-12 {{ 'has-error' if field.errors else '' }}">
{{ form_individual_field(field, prepend=prepend, append=append, label=label, **kwargs) }}
{%- if field.__class__.__name__ == 'list' %}
{%- for subfield in field %}
{{ form_individual_field(subfield, prepend=prepend, append=append, label=label, **kwargs) }}
{%- endfor %}
{%- else %}
{{ form_individual_field(field, prepend=prepend, append=append, label=label, **kwargs) }}
{%- endif %}
</div>
{%- endfor %}
</div>

@ -32,8 +32,8 @@
<td>{{ relay.name }}</td>
<td>{{ relay.smtp or '-' }}</td>
<td>{{ relay.comment or '' }}</td>
<td>{{ relay.created_at }}</td>
<td>{{ relay.updated_at or '' }}</td>
<td>{{ relay.created_at | format_date }}</td>
<td>{{ relay.updated_at | format_date }}</td>
</tr>
{%- endfor %}
</tbody>

@ -20,6 +20,7 @@
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Authorized IP{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th>
</tr>
</thead>
<tbody>
@ -30,7 +31,8 @@
</td>
<td>{{ token.comment }}</td>
<td>{{ token.ip or "any" }}</td>
<td>{{ token.created_at }}</td>
<td>{{ token.created_at | format_date }}</td>
<td>{{ token.updated_at | format_date }}</td>
</tr>
{%- endfor %}
</tbody>

@ -45,8 +45,8 @@
</td>
<td>{{ user.quota_bytes_used | filesizeformat }} / {{ (user.quota_bytes | filesizeformat) if user.quota_bytes else '∞' }}</td>
<td>{{ user.comment or '-' }}</td>
<td>{{ user.created_at }}</td>
<td>{{ user.updated_at or '' }}</td>
<td>{{ user.created_at | format_date }}</td>
<td>{{ user.updated_at | format_date }}</td>
</tr>
{%- endfor %}
</tbody>

@ -5,7 +5,6 @@ from flask import current_app as app
import flask
import flask_login
import wtforms_components
import dns.resolver
@ui.route('/domain', methods=['GET'])

@ -6,18 +6,21 @@ try:
except ImportError:
import pickle
import dns
import dns.resolver
import dns.exception
import dns.flags
import dns.rdtypes
import dns.rdatatype
import dns.rdataclass
import hmac
import secrets
import time
from multiprocessing import Value
from mailu import limiter
from flask import current_app as app
import flask
import flask_login
import flask_migrate
@ -28,7 +31,7 @@ import redis
from flask.sessions import SessionMixin, SessionInterface
from itsdangerous.encoding import want_bytes
from werkzeug.datastructures import CallbackDict
from werkzeug.contrib import fixers
from werkzeug.middleware.proxy_fix import ProxyFix
# Login configuration
login = flask_login.LoginManager()
@ -106,7 +109,7 @@ class PrefixMiddleware(object):
return self.app(environ, start_response)
def init_app(self, app):
self.app = fixers.ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
self.app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
app.wsgi_app = self
proxy = PrefixMiddleware()
@ -265,7 +268,7 @@ class MailuSession(CallbackDict, SessionMixin):
# set uid from dict data
if self._uid is None:
self._uid = self.app.session_config.gen_uid(self.get('user_id', ''))
self._uid = self.app.session_config.gen_uid(self.get('_user_id', ''))
# create new session id for new or regenerated sessions and force setting the cookie
if self._sid is None:

@ -1,10 +1,12 @@
from __future__ import with_statement
import logging
import tenacity
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
import logging
import tenacity
from tenacity import retry
from flask import current_app
from mailu import models
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
@ -17,20 +19,12 @@ logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option('sqlalchemy.url',
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
#target_metadata = current_app.extensions['migrate'].db.metadata
from mailu import models
config.set_main_option(
'sqlalchemy.url',
current_app.config.get('SQLALCHEMY_DATABASE_URI')
)
target_metadata = models.Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
@ -44,7 +38,7 @@ def run_migrations_offline():
script output.
"""
url = config.get_main_option("sqlalchemy.url")
url = config.get_main_option('sqlalchemy.url')
context.configure(url=url)
with context.begin_transaction():
@ -69,28 +63,35 @@ def run_migrations_online():
directives[:] = []
logger.info('No changes in schema detected.')
engine = engine_from_config(config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
engine = engine_from_config(
config.get_section(config.config_ini_section),
prefix = 'sqlalchemy.',
poolclass = pool.NullPool
)
connection = tenacity.Retrying(
stop=tenacity.stop_after_attempt(100),
wait=tenacity.wait_random(min=2, max=5),
before=tenacity.before_log(logging.getLogger("tenacity.retry"), logging.DEBUG),
before_sleep=tenacity.before_sleep_log(logging.getLogger("tenacity.retry"), logging.INFO),
after=tenacity.after_log(logging.getLogger("tenacity.retry"), logging.DEBUG)
).call(engine.connect)
@tenacity.retry(
stop = tenacity.stop_after_attempt(100),
wait = tenacity.wait_random(min=2, max=5),
before = tenacity.before_log(logging.getLogger('tenacity.retry'), logging.DEBUG),
before_sleep = tenacity.before_sleep_log(logging.getLogger('tenacity.retry'), logging.INFO),
after = tenacity.after_log(logging.getLogger('tenacity.retry'), logging.DEBUG)
)
def try_connect(db):
return db.connect()
context.configure(connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args)
with try_connect(engine) as connection:
context.configure(
connection = connection,
target_metadata = target_metadata,
process_revision_directives = process_revision_directives,
**current_app.extensions['migrate'].configure_args
)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
connection.close()
if context.is_offline_mode():
run_migrations_offline()

@ -1,56 +1,75 @@
alembic==1.0.10
asn1crypto==0.24.0
Babel==2.6.0
bcrypt==3.1.6
alembic==1.7.4
appdirs==1.4.4
Babel==2.9.1
bcrypt==3.2.0
blinker==1.4
cffi==1.12.3
Click==7.0
cryptography==3.4.7
decorator==4.4.0
dnspython==1.16.0
dominate==2.3.5
Flask==1.0.2
Flask-Babel==0.12.2
CacheControl==0.12.9
certifi==2021.10.8
cffi==1.15.0
chardet==4.0.0
click==8.0.3
colorama==0.4.4
contextlib2==21.6.0
cryptography==35.0.0
decorator==5.1.0
# distlib==0.3.1
# distro==1.5.0
dnspython==2.1.0
dominate==2.6.0
email-validator==1.1.3
Flask==2.0.2
Flask-Babel==2.0.0
Flask-Bootstrap==3.3.7.1
Flask-DebugToolbar==0.10.1
Flask-Limiter==1.0.1
Flask-Login==0.4.1
Flask-DebugToolbar==0.11.0
Flask-Limiter==1.4
Flask-Login==0.5.0
flask-marshmallow==0.14.0
Flask-Migrate==2.4.0
Flask-Migrate==3.1.0
Flask-Script==2.0.6
Flask-SQLAlchemy==2.4.0
Flask-WTF==0.14.2
Flask-SQLAlchemy==2.5.1
Flask-WTF==0.15.1
greenlet==1.1.2
gunicorn==20.1.0
idna==2.8
infinity==1.4
intervals==0.8.1
itsdangerous==1.1.0
Jinja2==2.11.3
limits==1.3
Mako==1.0.9
MarkupSafe==1.1.1
mysqlclient==1.4.2.post1
marshmallow==3.10.0
marshmallow-sqlalchemy==0.24.1
html5lib==1.1
idna==3.3
infinity==1.5
intervals==0.9.2
itsdangerous==2.0.1
Jinja2==3.0.2
limits==1.5.1
lockfile==0.12.2
Mako==1.1.5
MarkupSafe==2.0.1
marshmallow==3.14.0
marshmallow-sqlalchemy==0.26.1
msgpack==1.0.2
mysqlclient==2.0.3
ordered-set==4.0.2
# packaging==20.9
passlib==1.7.4
psycopg2==2.8.2
pycparser==2.19
Pygments==2.8.1
pyOpenSSL==20.0.1
python-dateutil==2.8.0
python-editor==1.0.4
pytz==2019.1
PyYAML==5.4.1
redis==3.2.1
#alpine3:12 provides six==1.15.0
#six==1.12.0
socrate==0.1.1
SQLAlchemy==1.3.3
# pep517==0.10.0
progress==1.6
psycopg2==2.9.1
pycparser==2.20
Pygments==2.10.0
pyOpenSSL==21.0.0
pyparsing==3.0.4
pytz==2021.3
PyYAML==6.0
redis==3.5.3
requests==2.26.0
retrying==1.3.3
# six==1.15.0
socrate==0.2.0
SQLAlchemy==1.4.26
srslib==0.1.4
tabulate==0.8.3
tenacity==5.0.4
validators==0.12.6
tabulate==0.8.9
tenacity==8.0.1
toml==0.10.2
urllib3==1.26.7
validators==0.18.2
visitor==0.1.3
Werkzeug==0.15.5
WTForms==2.2.1
WTForms-Components==0.10.4
webencodings==0.5.1
Werkzeug==2.0.2
WTForms==2.3.3
WTForms-Components==0.10.5

@ -18,10 +18,8 @@ PyYAML
PyOpenSSL
Pygments
dnspython
bcrypt
tenacity
mysqlclient
psycopg2
idna
srslib
marshmallow

Loading…
Cancel
Save