Merge branch 'develop' into between-formatting

This commit is contained in:
Mohammad Hasnain 2021-10-20 19:37:43 +05:30
commit 08ac2eda0a
29 changed files with 231 additions and 112 deletions

View file

@ -50,6 +50,7 @@ if [ "$TYPE" == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; f
if [ "$TYPE" == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi
if [ "$TYPE" == "ui" ]; then bench setup requirements --node; fi
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
# install node-sass which is required for website theme test
cd ./apps/frappe || exit

View file

@ -131,3 +131,16 @@ rules:
key `$X` is uselessly assigned twice. This could be a potential bug.
languages: [python]
severity: ERROR
- id: frappe-using-db-sql
pattern-either:
- pattern: frappe.db.sql(...)
- pattern: frappe.db.sql_ddl(...)
- pattern: frappe.db.sql_list(...)
paths:
exclude:
- "test_*.py"
message: |
The PR contains a SQL query that may be re-written with frappe.qb (https://frappeframework.com/docs/user/en/api/query-builder) or the Database API (https://frappeframework.com/docs/user/en/api/database)
languages: [python]
severity: ERROR

View file

@ -3,6 +3,7 @@ codecov:
coverage:
status:
patch: off
project:
default: false
server:
@ -10,11 +11,6 @@ coverage:
threshold: 0.5%
flags:
- server
ui-tests:
target: auto
threshold: 0.5%
flags:
- ui-tests
comment:
layout: "diff, flags"
@ -28,4 +24,4 @@ flags:
ui-tests:
paths:
- ".*\\.js"
carryforward: true
carryforward: true

View file

@ -1,44 +1,47 @@
context('Relative Timeframe', () => {
before(() => {
cy.login();
cy.visit('/app/website');
cy.window().its('frappe').then(frappe => {
frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
});
});
it('sets relative timespan filter for last week and filters list', () => {
cy.visit('/app/List/ToDo/List');
cy.clear_filters();
cy.get('.list-row:contains("this is fourth todo")').should('exist');
cy.add_filter();
cy.get('.fieldname-select-area').should('exist');
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
cy.get('select.condition.form-control').select("Timespan");
cy.get('.filter-field select.input-with-feedback.form-control').select("last week");
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
cy.get('.filter-popover .apply-filters').click({ force: true });
cy.wait('@list_refresh');
cy.get('.list-row-container').its('length').should('eq', 1);
cy.get('.list-row-container').should('contain', 'this is second todo');
cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
.as('save_user_settings');
cy.clear_filters();
cy.wait('@save_user_settings');
});
it('sets relative timespan filter for next week and filters list', () => {
cy.visit('/app/List/ToDo/List');
cy.clear_filters();
cy.get('.list-row:contains("this is fourth todo")').should('exist');
cy.add_filter();
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
cy.get('select.condition.form-control').select("Timespan");
cy.get('.filter-field select.input-with-feedback.form-control').select("next week");
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
cy.get('.filter-popover .apply-filters').click({ force: true });
cy.wait('@list_refresh');
cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
.as('save_user_settings');
cy.clear_filters();
cy.wait('@save_user_settings');
});
});
// TODO: Enable this again
// currently this is flaky possibly because of different timezone in CI
// context('Relative Timeframe', () => {
// before(() => {
// cy.login();
// cy.visit('/app/website');
// cy.window().its('frappe').then(frappe => {
// frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
// });
// });
// it('sets relative timespan filter for last week and filters list', () => {
// cy.visit('/app/List/ToDo/List');
// cy.clear_filters();
// cy.get('.list-row:contains("this is fourth todo")').should('exist');
// cy.add_filter();
// cy.get('.fieldname-select-area').should('exist');
// cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
// cy.get('select.condition.form-control').select("Timespan");
// cy.get('.filter-field select.input-with-feedback.form-control').select("last week");
// cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
// cy.get('.filter-popover .apply-filters').click({ force: true });
// cy.wait('@list_refresh');
// cy.get('.list-row-container').its('length').should('eq', 1);
// cy.get('.list-row-container').should('contain', 'this is second todo');
// cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
// .as('save_user_settings');
// cy.clear_filters();
// cy.wait('@save_user_settings');
// });
// it('sets relative timespan filter for next week and filters list', () => {
// cy.visit('/app/List/ToDo/List');
// cy.clear_filters();
// cy.get('.list-row:contains("this is fourth todo")').should('exist');
// cy.add_filter();
// cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
// cy.get('select.condition.form-control').select("Timespan");
// cy.get('.filter-field select.input-with-feedback.form-control').select("next week");
// cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
// cy.get('.filter-popover .apply-filters').click({ force: true });
// cy.wait('@list_refresh');
// cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
// .as('save_user_settings');
// cy.clear_filters();
// cy.wait('@save_user_settings');
// });
// });

View file

@ -50,8 +50,8 @@ context('Timeline', () => {
cy.click_modal_primary_button('Yes');
//Deleting the added ToDo
cy.get('.menu-btn-group [data-original-title="Menu"]').click();
cy.get('.menu-btn-group .dropdown-item').contains('Delete').click();
cy.get('[id="page-ToDo"] .menu-btn-group [data-original-title="Menu"]').click();
cy.get('[id="page-ToDo"] .menu-btn-group .dropdown-item').contains('Delete').click();
cy.findByRole('button', {name: 'Yes'}).click();
});

3
dev-requirements.txt Normal file
View file

@ -0,0 +1,3 @@
Faker~=8.1.0
pyngrok~=5.0.5
unittest-xml-reporting~=3.0.4

View file

@ -44,6 +44,11 @@ let argv = yargs
type: "boolean",
description: "Run in watch mode and rebuild on file changes"
})
.option("live-reload", {
type: "boolean",
description: `Automatically reload web pages when assets are rebuilt.
Can only be used with the --watch flag.`
})
.option("production", {
type: "boolean",
description: "Run build in production mode"
@ -478,7 +483,8 @@ async function notify_redis({ error, success }) {
}
if (success) {
payload = {
success: true
success: true,
live_reload: argv["live-reload"]
};
}

View file

@ -30,9 +30,6 @@ from .utils.lazy_loader import lazy_import
from frappe.query_builder import get_query_builder, patch_query_execute
# Lazy imports
faker = lazy_import('faker')
__version__ = '14.0.0-dev'
__title__ = "Frappe Framework"
@ -1480,7 +1477,10 @@ def get_value(*args, **kwargs):
def as_json(obj, indent=1):
from frappe.utils.response import json_handler
return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler, separators=(',', ': '))
try:
return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler, separators=(',', ': '))
except TypeError:
return json.dumps(obj, indent=indent, default=json_handler, separators=(',', ': '))
def are_emails_muted():
from frappe.utils import cint
@ -1835,6 +1835,7 @@ def parse_json(val):
return parse_json(val)
def mock(type, size=1, locale='en'):
import faker
results = []
fake = faker.Faker(locale)
if type not in dir(fake):

View file

@ -16,7 +16,6 @@ from frappe.utils.minify import JavascriptMinify
import click
import psutil
from urllib.parse import urlparse
from simple_chalk import green
from semantic_version import Version
from requests import head
from requests.exceptions import HTTPError
@ -108,7 +107,7 @@ def fetch_assets(url, frappe_head):
if not assets_archive:
raise AssetsNotDownloadedError(f"Assets could not be retrived from {url}")
print(f"\n{green('')} Downloaded Frappe assets from {url}")
click.echo(click.style("", fg="green") + f" Downloaded Frappe assets from {url}")
return assets_archive
@ -131,7 +130,7 @@ def setup_assets(assets_archive):
directories_created.add(asset_directory)
tar.makefile(file, dest)
print("{0} Restored {1}".format(green(''), show))
click.echo(click.style("", fg="green") + f" Restored {show}")
return directories_created
@ -257,6 +256,13 @@ def watch(apps=None):
if apps:
command += " --apps {apps}".format(apps=apps)
live_reload = frappe.utils.cint(
os.environ.get("LIVE_RELOAD", frappe.conf.live_reload)
)
if live_reload:
command += " --live-reload"
check_node_executable()
frappe_app_path = frappe.get_app_path("frappe", "..")
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
@ -372,7 +378,7 @@ def make_asset_dirs(hard_link=False):
except Exception:
print(fail_message, end="\r")
print(unstrip(f"{green('')} Application Assets Linked") + "\n")
click.echo(unstrip(click.style("", fg="green") + " Application Assets Linked") + "\n")
def link_assets_dir(source, target, hard_link=False):

View file

@ -22,7 +22,6 @@ class NavbarSettings(Document):
if not frappe.flags.in_patch and (len(before_save_items) > len(after_save_items)):
frappe.throw(_("Please hide the standard navbar items instead of deleting them"))
@frappe.whitelist(allow_guest=True)
def get_app_logo():
app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo', cache=True)
if not app_logo:

View file

@ -54,7 +54,7 @@ class UserPermission(Document):
ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name)
frappe.throw(_("{0} has already assigned default value for {1}.").format(ref_link, self.allow))
@frappe.whitelist(allow_guest=True)
@frappe.whitelist()
def get_user_permissions(user=None):
'''Get all users permissions for the user as a dict of doctype'''
# if this is called from client-side,

View file

@ -13,7 +13,7 @@ from frappe.desk.form.document_follow import is_document_followed
from frappe import _
from urllib.parse import quote
@frappe.whitelist(allow_guest=True)
@frappe.whitelist()
def getdoc(doctype, name, user=None):
"""
Loads a doclist for a given document. This method is called directly from the client.
@ -52,7 +52,7 @@ def getdoc(doctype, name, user=None):
frappe.response.docs.append(doc)
@frappe.whitelist(allow_guest=True)
@frappe.whitelist()
def getdoctype(doctype, with_parent=False, cached_timestamp=None):
"""load doctype"""

View file

@ -2,7 +2,7 @@
# License: MIT. See LICENSE
import frappe
@frappe.whitelist(allow_guest=True)
@frappe.whitelist()
def get_list_settings(doctype):
try:
return frappe.get_cached_doc("List View Settings", doctype)

View file

@ -14,7 +14,7 @@ from frappe.utils import cstr, format_duration
from frappe.model.base_document import get_controller
@frappe.whitelist(allow_guest=True)
@frappe.whitelist()
@frappe.read_only()
def get():
args = get_form_params()

View file

@ -99,8 +99,8 @@ class IncompatibleApp(ValidationError): pass
class InvalidDates(ValidationError): pass
class DataTooLongException(ValidationError): pass
class FileAlreadyAttachedException(Exception): pass
class DocumentAlreadyRestored(Exception): pass
class AttachmentLimitReached(Exception): pass
class DocumentAlreadyRestored(ValidationError): pass
class AttachmentLimitReached(ValidationError): pass
# OAuth exceptions
class InvalidAuthorizationHeader(CSRFTokenError): pass
class InvalidAuthorizationPrefix(CSRFTokenError): pass

View file

@ -336,7 +336,6 @@ def dropbox_auth_finish(return_access_token=False):
_("Dropbox access is approved!") + close,
indicator_color='green')
@frappe.whitelist(allow_guest=True)
def set_dropbox_access_token(access_token):
frappe.db.set_value("Dropbox Settings", None, 'dropbox_access_token', access_token)
frappe.db.commit()

View file

@ -597,8 +597,8 @@ class DatabaseQuery(object):
self.conditions.append(self.get_share_condition())
else:
#if has if_owner permission skip user perm check
if role_permissions.get("has_if_owner_enabled") and role_permissions.get("if_owner", {}):
# skip user perm check if owner constraint is required
if requires_owner_constraint(role_permissions):
self.match_conditions.append(
f"`tab{self.doctype}`.`owner` = {frappe.db.escape(self.user, percent=False)}"
)
@ -895,3 +895,22 @@ def get_date_range(operator, value):
timespan = period_map[operator] + ' ' + timespan_map[value] if operator != 'timespan' else value
return get_timespan_date_range(timespan)
def requires_owner_constraint(role_permissions):
"""Returns True if "select" or "read" isn't available without being creator."""
if not role_permissions.get("has_if_owner_enabled"):
return
if_owner_perms = role_permissions.get("if_owner")
if not if_owner_perms:
return
# has select or read without if owner, no need for constraint
for perm_type in ("select", "read"):
if role_permissions.get(perm_type) and perm_type not in if_owner_perms:
return
# not checking if either select or read if present in if_owner_perms
# because either of those is required to perform a query
return True

View file

@ -107,13 +107,9 @@ def get_doc_permissions(doc, user=None, ptype=None):
meta = frappe.get_meta(doc.doctype)
def is_user_owner():
doc_owner = doc.get('owner') or ''
doc_owner = doc_owner.lower()
session_user = frappe.session.user.lower()
return doc_owner == session_user
return (doc.get("owner") or "").lower() == frappe.session.user.lower()
if has_controller_permissions(doc, ptype, user=user) == False :
if has_controller_permissions(doc, ptype, user=user) is False:
push_perm_check_log('Not allowed via controller permission check')
return {ptype: 0}
@ -182,22 +178,23 @@ def get_role_permissions(doctype_meta, user=None, is_owner=None):
applicable_permissions = list(filter(is_perm_applicable, getattr(doctype_meta, 'permissions', [])))
has_if_owner_enabled = any(p.get('if_owner', 0) for p in applicable_permissions)
perms['has_if_owner_enabled'] = has_if_owner_enabled
for ptype in rights:
pvalue = any(p.get(ptype, 0) for p in applicable_permissions)
# check if any perm object allows perm type
perms[ptype] = cint(pvalue)
if (pvalue
and has_if_owner_enabled
and not has_permission_without_if_owner_enabled(ptype)
and ptype != 'create'):
if (
pvalue
and has_if_owner_enabled
and not has_permission_without_if_owner_enabled(ptype)
and ptype != 'create'
):
perms['if_owner'][ptype] = cint(pvalue and is_owner)
# has no access if not owner
# only provide select or read access so that user is able to at-least access list
# (and the documents will be filtered based on owner sin further checks)
perms[ptype] = 1 if ptype in ['select', 'read'] else 0
perms[ptype] = 1 if ptype in ('select', 'read') else 0
frappe.local.role_permissions[cache_key] = perms

View file

@ -134,7 +134,7 @@ frappe.ui.form.PrintView = class {
add_sidebar_item(df, is_dynamic) {
if (df.fieldtype == 'Select') {
df.input_class = 'btn btn-default btn-sm';
df.input_class = 'btn btn-default btn-sm text-left';
}
let field = frappe.ui.form.make_control({

View file

@ -3,8 +3,11 @@
v-if="is_shown"
class="flex justify-between build-success-message align-center"
>
<div class="mr-4">Compiled successfully</div>
<a class="text-white underline" href="/" @click.prevent="reload">
Compiled successfully
<a
v-if="!live_reload"
class="ml-4 text-white underline" href="/" @click.prevent="reload"
>
Refresh
</a>
</div>
@ -14,11 +17,17 @@ export default {
name: "BuildSuccess",
data() {
return {
is_shown: false
is_shown: false,
live_reload: false,
};
},
methods: {
show() {
show(data) {
if (data.live_reload) {
this.live_reload = true;
this.reload();
}
this.is_shown = true;
if (this.timeout) {
clearTimeout(this.timeout);

View file

@ -13,10 +13,11 @@ frappe.realtime.on("build_event", data => {
}
});
function show_build_success() {
function show_build_success(data) {
if (error) {
error.hide();
}
if (!success) {
let target = $('<div class="build-success-container">')
.appendTo($container)
@ -27,7 +28,7 @@ function show_build_success() {
});
success = vm.$children[0];
}
success.show();
success.show(data);
}
function show_build_error(data) {

View file

@ -302,9 +302,20 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
refresh(refresh_header=false) {
super.refresh().then(() => {
this.render_header(refresh_header);
this.update_checkbox();
});
}
update_checkbox(target) {
let $check_all_checkbox = this.$checkbox_actions.find(".list-check-all");
if ($check_all_checkbox.prop("checked") && target && !target.prop("checked")) {
$check_all_checkbox.prop("checked", false);
}
$check_all_checkbox.prop("checked", this.$checks.length === this.data.length);
}
setup_freeze_area() {
this.$freeze = $(
`<div class="freeze flex justify-center align-center text-muted">${__(
@ -1253,6 +1264,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
this.$checkbox_cursor = $target;
this.update_checkbox($target);
});
}
@ -1398,6 +1411,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
this.$checkbox_actions.show();
this.$list_head_subject.hide();
}
this.update_checkbox();
this.toggle_actions_menu_button(this.$checks.length > 0);
}

View file

@ -1049,18 +1049,20 @@ Object.assign(frappe.utils, {
return duration;
},
seconds_to_duration(value, duration_options) {
let secs = value;
let total_duration = {
days: Math.floor(secs / (3600 * 24)),
hours: Math.floor(secs % (3600 * 24) / 3600),
minutes: Math.floor(secs % 3600 / 60),
seconds: Math.floor(secs % 60)
seconds_to_duration(seconds, duration_options) {
const round = seconds > 0 ? Math.floor : Math.ceil;
const total_duration = {
days: round(seconds / 86400), // 60 * 60 * 24
hours: round(seconds % 86400 / 3600),
minutes: round(seconds % 3600 / 60),
seconds: round(seconds % 60)
};
if (duration_options.hide_days) {
total_duration.hours = Math.floor(secs / 3600);
total_duration.hours = round(seconds / 3600);
total_duration.days = 0;
}
return total_duration;
},

View file

@ -107,7 +107,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}
if (this.report_name !== frappe.get_route()[1]) {
// this.toggle_loading(true);
// different report
this.load_report();
}
@ -556,6 +555,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
refresh() {
this.toggle_message(true);
this.toggle_report(false);
this.show_loading_screen();
let filters = this.get_filter_values(true);
// only one refresh at a time
@ -645,6 +645,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.show_footer_message();
frappe.hide_progress();
}).finally(() => {
this.hide_loading_screen();
});
}
@ -869,6 +871,24 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}
}
show_loading_screen() {
const loading_state = `<div class="msg-box no-border">
<div>
<img src="/assets/frappe/images/ui-states/list-empty-state.svg" alt="Generic Empty State" class="null-state">
</div>
<p>${__('Loading')}...</p>
</div>`;
this.$loading.find('div').html(loading_state);
this.$report.hide();
this.$loading.show();
}
hide_loading_screen() {
this.$loading.hide();
this.$report.show();
}
get_chart_options(data) {
let options = this.report_settings.get_chart_data
? this.report_settings.get_chart_data(data.columns, data.result)
@ -1679,6 +1699,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
.hide().appendTo(this.page.main);
this.$chart = $('<div class="chart-wrapper">').hide().appendTo(this.page.main);
this.$loading = $(this.message_div('')).hide().appendTo(this.page.main);
this.$report = $('<div class="report-wrapper">').appendTo(this.page.main);
this.$message = $(this.message_div('')).hide().appendTo(this.page.main);
}
@ -1738,11 +1760,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.refresh();
}
toggle_loading(flag) {
this.toggle_message(flag, __('Loading') + '...');
}
toggle_nothing_to_show(flag) {
let message = this.prepared_report
? __('This is a background report. Please set the appropriate filters and then generate a new one.')

View file

@ -147,7 +147,6 @@
.list-row-head {
@extend .list-row;
padding: 15px;
cursor: default;
.list-subject {
@ -214,6 +213,10 @@ input.list-check-all, input.list-row-checkbox {
--checkbox-right-margin: calc(var(--checkbox-size) / 2 + #{$level-margin-right});
}
input.list-check-all {
margin-left: 15px;
}
.render-list-checkbox {
margin-left: 15px;
}

View file

@ -17,8 +17,8 @@ import redis
from urllib.parse import unquote
from frappe.cache_manager import clear_user_cache
@frappe.whitelist(allow_guest=True)
def clear(user=None):
@frappe.whitelist()
def clear():
frappe.local.session_obj.update(force=True)
frappe.local.db.commit()
clear_user_cache(frappe.session.user)

View file

@ -493,6 +493,34 @@ class TestPermissions(unittest.TestCase):
frappe.set_user("test2@example.com")
self.assertRaises(frappe.PermissionError, getdoc, 'Blog Post', doc.name)
def test_if_owner_permission_on_get_list(self):
doc = frappe.get_doc({
"doctype": "Blog Post",
"blog_category": "-test-blog-category",
"blogger": "_Test Blogger 1",
"title": "_Test If Owner Permissions on Get List",
"content": "_Test Blog Post Content"
})
doc.insert(ignore_if_duplicate=True)
update('Blog Post', 'Blogger', 0, 'if_owner', 1)
update('Blog Post', 'Blogger', 0, 'read', 1)
user = frappe.get_doc("User", "test2@example.com")
user.add_roles("Website Manager")
frappe.clear_cache(doctype="Blog Post")
frappe.set_user("test2@example.com")
self.assertIn(doc.name, frappe.get_list("Blog Post", pluck="name"))
# Become system manager to remove role
frappe.set_user("test1@example.com")
user.remove_roles("Website Manager")
frappe.clear_cache(doctype="Blog Post")
frappe.set_user("test2@example.com")
self.assertNotIn(doc.name, frappe.get_list("Blog Post", pluck="name"))
def test_if_owner_permission_on_delete(self):
update('Blog Post', 'Blogger', 0, 'if_owner', 1)
update('Blog Post', 'Blogger', 0, 'read', 1)

View file

@ -4,6 +4,7 @@ import frappe
from frappe.utils import set_request
from frappe.website.serve import get_response, get_response_content
from frappe.website.utils import (build_response, clear_website_cache, get_home_page)
from tenacity import retry, stop_after_attempt, retry_if_exception_type
class TestWebsite(unittest.TestCase):
@ -196,6 +197,11 @@ class TestWebsite(unittest.TestCase):
delattr(frappe.hooks, 'page_renderer')
frappe.cache().delete_key('app_hooks')
# TODO: Get rid of this retry logic
# Added since test is flaky and we can't figure out why at this point
@retry(
stop=stop_after_attempt(5), retry=retry_if_exception_type(AssertionError),
)
def test_printview_page(self):
content = get_response_content('/Language/en')
self.assertIn('<div class="print-format">', content)

View file

@ -12,7 +12,6 @@ croniter~=1.0.11
cryptography~=3.4.7
dropbox~=11.7.0
email-reply-parser~=0.5.12
Faker~=8.1.0
git-url-parse~=1.2.2
gitdb~=4.0.7
GitPython~=3.1.14
@ -44,7 +43,6 @@ pyasn1~=0.4.8
pycryptodome~=3.10.1
PyJWT~=2.0.1
PyMySQL~=1.0.2
pyngrok~=5.0.5
pyOpenSSL~=20.0.1
pyotp~=2.6.0
PyPDF2~=1.26.0
@ -64,12 +62,10 @@ rq~=1.8.0
rsa>=4.1 # not directly required, pinned by Snyk to avoid a vulnerability
schedule~=1.1.0
semantic-version~=2.8.5
simple-chalk~=0.1.0
six~=1.15.0
sqlparse~=0.4.1
stripe~=2.56.0
terminaltables~=3.1.0
unittest-xml-reporting~=3.0.4
urllib3~=1.26.4
Werkzeug~=0.16.1
Whoosh~=2.7.4