diff --git a/.env.example b/.env.example index bdb593a99e..31f34d5ab7 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,10 @@ CONTACT_EMAIL='PhysioNet Contact ' SERVER_EMAIL='PhysioNet System ' ERROR_EMAIL='contact@physionet.org' +# Contact address for project editors. This address may be viewable by authors. +# Optionally, add "PROJECT-SLUG" to include the project slug. +PROJECT_EDITOR_EMAIL='editor+PROJECT-SLUG@dev.physionet.org' + # Admins ADMINS_NAME=PhysioNet Technical ADMINS_MAIL=technical@dev.physionet.org @@ -60,6 +64,18 @@ AWS_VALUE=secret AWS_VALUE2=secret AWS_CLOUD_FORMATION=url +# AWS credentials (Access Key and Secret Key): Configure AWS credentials in the AWS CLI profile using the 'aws configure' command. +AWS_PROFILE= +# AWS account ID +AWS_ACCOUNT_ID= +# Path to the file containing credentials for AWS +# (https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html#shared-credentials-file) +AWS_SHARED_CREDENTIALS_FILE= +# The default bucket name to store projects with an 'OPEN' access policy. +S3_OPEN_ACCESS_BUCKET= +# The default bucket name to store logs and metrics related to project usage. +S3_SERVER_ACCESS_LOG_BUCKET= + # Datacite # Used to assign the DOIs # Changing the password can be done at the settings tab in DataCite website @@ -158,6 +174,10 @@ GRADIENT_85 = 'rgba(42, 47, 52, 0.85)' # maximum number of emails that can be associated to a user model MAX_EMAILS_PER_USER = 10 +# maximum number of active projects that can be created by a submitting author at any time. +# if MAX_SUBMITTABLE_PROJECTS is reached, the user must wait for a project to be archived or published before starting another. +MAX_SUBMITTABLE_PROJECTS = 10 + # Max training report size in bytes MAX_TRAINING_REPORT_UPLOAD_SIZE = 1048576 ENABLE_LIGHTWAVE=True diff --git a/README.md b/README.md index a8222726fc..1a9c3328e7 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,12 @@ The code should dynamically reload in development, however, if there are any iss Docker-compose uses volumes to persist the database contents and data directories (media and static files). To clean up the created containers, networks and volumes stop `docker-compose up` and run `docker-compose down -v`. Do not run `docker-compose down -v` if you want to retain current database contents. +## Background tasks + +Background tasks are managed by [Django Q2](https://django-q2.readthedocs.io/en/master/), "a native Django task queue, scheduler and worker application using Python multiprocessing". + +If you would like to run background tasks on your development server, you will need to start the task manager with `python manage.py qcluster` + ## Using a debugger with Docker To access a debug prompt raised using `breakpoint()`: diff --git a/demo-files/aws_credentials b/demo-files/aws_credentials new file mode 100644 index 0000000000..5c0206fd22 --- /dev/null +++ b/demo-files/aws_credentials @@ -0,0 +1,3 @@ +[default] +aws_access_key_id = AKIAZZZZZZZZZZZZZZZZ +aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/deploy/common/etc/sudoers.d/physionet b/deploy/common/etc/sudoers.d/physionet index fd522bcee2..3a0260ec37 100644 --- a/deploy/common/etc/sudoers.d/physionet +++ b/deploy/common/etc/sudoers.d/physionet @@ -16,3 +16,7 @@ pn ALL=(root:root) NOPASSWD: \ # Restart django-background-tasks.service (if it is running) pn ALL=(root:root) NOPASSWD: \ /bin/systemctl try-restart django-background-tasks.service + +# Restart django-q2-tasks.service (if it is running) +pn ALL=(root:root) NOPASSWD: \ + /bin/systemctl try-restart django-q2-tasks.service diff --git a/deploy/post-receive b/deploy/post-receive index f4edc973cf..aec4db3227 100755 --- a/deploy/post-receive +++ b/deploy/post-receive @@ -112,6 +112,7 @@ else fi # Restart the django-background-tasks server +# Background tasks will soon be migrated to Django Q2 ( cd $working_dir/physionet-django if cmp -s OLD-TARGETS LATE-TARGETS; then @@ -130,6 +131,25 @@ fi fi ) +# Restart the Django Q2 server +( + cd $working_dir/physionet-django + if cmp -s OLD-TARGETS LATE-TARGETS; then + # assume that if there are no migrations, no need to restart + # background tasks (and don't even bother showing a message) + : + elif [ -n "$no_bgtasks" ]; then + echo "- SKIPPING restarting django-q2-tasks due to" + echo " --push-option=no-bgtasks" + elif [ -n "$no_install" ]; then + echo "- SKIPPING restarting django-q2-tasks due to" + echo " --push-option=no-install" + else + echo "* Restarting django-q2-tasks..." + sudo systemctl try-restart django-q2-tasks.service + fi +) + # Run late migrations (as well as early migrations, if they were # skipped before) ( diff --git a/deploy/production/etc/rsyslog.d/django_q.conf b/deploy/production/etc/rsyslog.d/django_q.conf new file mode 100644 index 0000000000..e440311714 --- /dev/null +++ b/deploy/production/etc/rsyslog.d/django_q.conf @@ -0,0 +1,3 @@ +# Filter to get all background tasks and send them to a custom location +:programname, isequal, "django-q2-tasks" -/data/log/background_tasks/django_q_tasks.log +& stop diff --git a/deploy/production/etc/systemd/system/django-q2-tasks.service b/deploy/production/etc/systemd/system/django-q2-tasks.service new file mode 100644 index 0000000000..facb33eee3 --- /dev/null +++ b/deploy/production/etc/systemd/system/django-q2-tasks.service @@ -0,0 +1,18 @@ +[Unit] +Description=Command that runs Django Q2 tasks +After=emperor.uwsgi.service + +[Service] +Environment=DJANGO_SETTINGS_MODULE=physionet.settings.production +ExecStart=/physionet/python-env/physionet/bin/python /physionet/physionet-build/physionet-django/manage.py qcluster +StandardError=syslog +SyslogIdentifier=django-q2-tasks +Restart=always +KillSignal=SIGINT +Type=simple +NotifyAccess=all +User=www-data +Group=www-data + +[Install] +WantedBy=multi-user.target diff --git a/deploy/staging/etc/rsyslog.d/django_q.conf b/deploy/staging/etc/rsyslog.d/django_q.conf new file mode 100644 index 0000000000..e440311714 --- /dev/null +++ b/deploy/staging/etc/rsyslog.d/django_q.conf @@ -0,0 +1,3 @@ +# Filter to get all background tasks and send them to a custom location +:programname, isequal, "django-q2-tasks" -/data/log/background_tasks/django_q_tasks.log +& stop diff --git a/deploy/staging/etc/systemd/system/django-q2-tasks.service b/deploy/staging/etc/systemd/system/django-q2-tasks.service new file mode 100644 index 0000000000..6cfbfc233f --- /dev/null +++ b/deploy/staging/etc/systemd/system/django-q2-tasks.service @@ -0,0 +1,18 @@ +[Unit] +Description=Command that runs Django Q2 tasks +After=emperor.uwsgi.service + +[Service] +Environment=DJANGO_SETTINGS_MODULE=physionet.settings.staging +ExecStart=/physionet/python-env/physionet/bin/python /physionet/physionet-build/physionet-django/manage.py qcluster +StandardError=syslog +SyslogIdentifier=django-q2-tasks +Restart=always +KillSignal=SIGINT +Type=simple +NotifyAccess=all +User=www-data +Group=www-data + +[Install] +WantedBy=multi-user.target diff --git a/deploy/test-server/install-pn-test-server b/deploy/test-server/install-pn-test-server index 68e9c9d23c..a48652a0cf 100755 --- a/deploy/test-server/install-pn-test-server +++ b/deploy/test-server/install-pn-test-server @@ -234,9 +234,14 @@ systemctl daemon-reload systemctl enable emperor.uwsgi systemctl restart emperor.uwsgi systemctl restart nginx + +# django-background-tasks will be replaced with Django Q2 soon systemctl enable django-background-tasks systemctl restart django-background-tasks +systemctl enable django-q2-tasks +systemctl restart django-q2-tasks + su pn -c ' cd /physionet/physionet-build.git echo diff --git a/physionet-django/.coveragerc b/physionet-django/.coveragerc new file mode 100644 index 0000000000..4f8710d568 --- /dev/null +++ b/physionet-django/.coveragerc @@ -0,0 +1,5 @@ +[run] +plugins = django_coverage_plugin + +[django_coverage_plugin] +template_extensions = html, txt, json, xml diff --git a/physionet-django/console/forms.py b/physionet-django/console/forms.py index c085efaf12..943bd5c33f 100644 --- a/physionet-django/console/forms.py +++ b/physionet-django/console/forms.py @@ -223,9 +223,6 @@ def save(self): # Reject if edit_log.decision == 0: project.reject() - # Have to reload this object which is changed by the reject - # function - edit_log = EditLog.objects.get(id=edit_log.id) # Resubmit with revisions elif edit_log.decision == 1: project.submission_status = SubmissionStatus.NEEDS_RESUBMISSION @@ -635,7 +632,7 @@ class NewsForm(forms.ModelForm): class Meta: model = News - fields = ('title', 'content', 'url', 'project', 'front_page_banner') + fields = ('slug', 'title', 'content', 'url', 'project', 'link_all_versions', 'front_page_banner') class FeaturedForm(forms.Form): diff --git a/physionet-django/console/navbar.py b/physionet-django/console/navbar.py new file mode 100644 index 0000000000..1a06ec823d --- /dev/null +++ b/physionet-django/console/navbar.py @@ -0,0 +1,220 @@ +import functools + +from django.conf import settings +from django.urls import get_resolver, reverse +from django.utils.translation import gettext_lazy as _ + +from physionet.settings.base import StorageTypes + + +class NavLink: + """ + A link to be displayed in the navigation menu. + + The exact URL of the link is determined by reversing the view + name. + + The use of view_args is deprecated, and provided for compatibility + with existing views that require a static URL argument. Don't use + view_args for newly added items. + + The link will only be displayed in the menu if the logged-in user + has permission to access that URL. This means that the + corresponding view function must be decorated with the + console_permission_required decorator. + + The link will appear as "active" if the request URL matches the + link URL or a descendant. + """ + def __init__(self, title, view_name, icon=None, *, + enabled=True, view_args=()): + self.title = title + self.icon = icon + self.enabled = enabled + self.view_name = view_name + self.view_args = view_args + if self.view_args: + self.name = self.view_name + '__' + '_'.join(self.view_args) + else: + self.name = self.view_name + + @functools.cached_property + def url(self): + return reverse(self.view_name, args=self.view_args) + + @functools.cached_property + def required_permission(self): + view = get_resolver().resolve(self.url).func + return view.required_permission + + def is_visible(self, request): + return self.enabled and request.user.has_perm(self.required_permission) + + def is_active(self, request): + return request.path.startswith(self.url) + + +class NavHomeLink(NavLink): + """ + Variant of NavLink that does not include sub-pages. + """ + def is_active(self, request): + return request.path == self.url + + +class NavSubmenu: + """ + A collection of links to be displayed as a submenu. + """ + def __init__(self, title, name, icon=None, items=[]): + self.title = title + self.name = name + self.icon = icon + self.items = items + + +class NavMenu: + """ + A collection of links and submenus for navigation. + """ + def __init__(self, items): + self.items = items + + def get_menu_items(self, request): + """ + Return the navigation menu items for an HTTP request. + + This returns a list of dictionaries, each of which represents + either a page link or a submenu. + + For a page link, the dictionary contains: + - 'title': human readable title + - 'name': unique name (corresponding to the view name) + - 'icon': icon name + - 'url': page URL + - 'active': true if this page is currently active + + For a submenu, the dictionary contains: + - 'title': human readable title + - 'name': unique name + - 'icon': icon name + - 'subitems': list of page links + - 'active': true if this submenu is currently active + """ + visible_items = [] + + for item in self.items: + if isinstance(item, NavSubmenu): + subitems = item.items + elif isinstance(item, NavLink): + subitems = [item] + else: + raise TypeError(item) + + visible_subitems = [] + active = False + for subitem in subitems: + if subitem.is_visible(request): + subitem_active = subitem.is_active(request) + active = active or subitem_active + visible_subitems.append({ + 'title': subitem.title, + 'name': subitem.name, + 'icon': subitem.icon, + 'url': subitem.url, + 'active': subitem_active, + }) + + if visible_subitems: + if isinstance(item, NavSubmenu): + visible_items.append({ + 'title': item.title, + 'name': item.name, + 'icon': item.icon, + 'subitems': visible_subitems, + 'active': active, + }) + else: + visible_items += visible_subitems + + return visible_items + + +CONSOLE_NAV_MENU = NavMenu([ + NavHomeLink(_('Home'), 'console_home', 'book-open'), + + NavLink(_('Editor Home'), 'editor_home', 'book-open'), + + NavSubmenu(_('Projects'), 'projects', 'clipboard-list', [ + NavLink(_('Unsubmitted'), 'unsubmitted_projects'), + NavLink(_('Submitted'), 'submitted_projects'), + NavLink(_('Published'), 'published_projects'), + NavLink(_('Archived'), 'archived_submissions'), + ]), + + NavLink(_('Storage'), 'storage_requests', 'cube'), + + NavSubmenu(_('Cloud'), 'cloud', 'cloud', [ + NavLink(_('Mirrors'), 'cloud_mirrors'), + ]), + + NavSubmenu(_('Identity check'), 'identity', 'hand-paper', [ + NavLink(_('Processing'), 'credential_processing'), + NavLink(_('All Applications'), 'credential_applications', + view_args=['successful']), + NavLink(_('Known References'), 'known_references'), + ]), + + NavLink(_('Training check'), 'training_list', 'school', + view_args=['review']), + + NavLink(_('Courses'), 'courses', 'chalkboard-teacher'), + + NavSubmenu(_('Events'), 'events', 'clipboard-list', [ + NavLink(_('Active'), 'event_active'), + NavLink(_('Archived'), 'event_archive'), + ]), + + NavSubmenu(_('Legal'), 'legal', 'handshake', [ + NavLink(_('Licenses'), 'license_list'), + NavLink(_('DUAs'), 'dua_list'), + NavLink(_('Code of Conduct'), 'code_of_conduct_list'), + NavLink(_('Event Agreements'), 'event_agreement_list'), + ]), + + NavSubmenu(_('Logs'), 'logs', 'fingerprint', [ + NavLink(_('Project Logs'), 'project_access_logs'), + NavLink(_('Access Requests'), 'project_access_requests_list'), + NavLink(_('User Logs'), 'user_access_logs'), + NavLink(_('GCP Logs'), 'gcp_signed_urls_logs', + enabled=(settings.STORAGE_TYPE == StorageTypes.GCP)), + ]), + + NavSubmenu(_('Users'), 'users', 'user-check', [ + NavLink(_('Active Users'), 'users', view_args=['active']), + NavLink(_('Inactive Users'), 'users', view_args=['inactive']), + NavLink(_('All Users'), 'users', view_args=['all']), + NavLink(_('User Groups'), 'user_groups'), + NavLink(_('Administrators'), 'users', view_args=['admin']), + ]), + + NavLink(_('Featured Content'), 'featured_content', 'star'), + + NavSubmenu(_('Guidelines'), 'guidelines', 'book', [ + NavLink(_('Project review'), 'guidelines_review'), + ]), + + NavSubmenu(_('Usage Stats'), 'stats', 'chart-area', [ + NavLink(_('Editorial'), 'editorial_stats'), + NavLink(_('Credentialing'), 'credentialing_stats'), + NavLink(_('Submissions'), 'submission_stats'), + ]), + + NavSubmenu(_('Pages'), 'pages', 'window-maximize', [ + NavLink(_('Static Pages'), 'static_pages'), + NavLink(_('Frontpage Buttons'), 'frontpage_buttons'), + NavLink(_('Redirects'), 'redirects'), + ]), + + NavLink(_('News'), 'news_console', 'newspaper'), +]) diff --git a/physionet-django/console/services.py b/physionet-django/console/services.py index 18218cc088..9edcf9651e 100644 --- a/physionet-django/console/services.py +++ b/physionet-django/console/services.py @@ -4,7 +4,7 @@ from typing import Optional from pdfminer.high_level import extract_text -from pdfminer.pdfparser import PDFSyntaxError +from pdfminer.pdfparser import PDFException from django.conf import settings from user.models import Training @@ -29,7 +29,7 @@ def _get_regex_value_from_text(text: str, regex: str) -> Optional[str]: def _parse_pdf_to_string(training_path: str) -> str: try: text = extract_text(training_path) - except PDFSyntaxError: + except PDFException: text = '' logging.error(f'Failed to extract text from {training_path}') return ' '.join(text.split()) diff --git a/physionet-django/console/static/console/css/cloud-mirrors.css b/physionet-django/console/static/console/css/cloud-mirrors.css new file mode 100644 index 0000000000..d01bc8fc12 --- /dev/null +++ b/physionet-django/console/static/console/css/cloud-mirrors.css @@ -0,0 +1,91 @@ +.table-cloud-status { + table-layout: fixed; + width: 100%; +} + +.table-cloud-status .col-project-version { + width: 7rem; +} +.table-cloud-status .col-project-site-status, +.table-cloud-status .col-project-cloud-status { + width: 3rem; + text-align: center; + overflow-x: hidden; +} + +.project-site-status-title, +.project-cloud-status-title { + font-size: 0; +} +.project-site-status-title::before, +.project-cloud-status-title::before { + font-size: 1rem; + display: inline-block; + width: 1.25em; + font-family: "Font Awesome 5 Free"; + font-weight: 900; +} +.project-site-status-title::before { + content: "\f019"; /* download */ +} +.project-cloud-status-title::before { + content: "\f381"; /* cloud-download-alt */ +} +.col-gcp .project-cloud-status-title::before { + font-family: "Font Awesome 5 Brands"; + font-weight: 400; + content: "\f1a0"; /* google */ +} +.col-aws .project-cloud-status-title::before { + font-family: "Font Awesome 5 Brands"; + font-weight: 400; + content: "\f375"; /* aws */ +} + +.project-site-status-open, +.project-site-status-restricted, +.project-site-status-embargo, +.project-site-status-forbidden, +.project-cloud-status-public, +.project-cloud-status-private, +.project-cloud-status-pending, +.project-cloud-status-none { + font-size: 0; +} +.project-site-status-open::before, +.project-site-status-restricted::before, +.project-site-status-embargo::before, +.project-site-status-forbidden::before, +.project-cloud-status-public::before, +.project-cloud-status-private::before, +.project-cloud-status-pending::before, +.project-cloud-status-none::before { + font-size: 1rem; + display: inline-block; + width: 1.25em; + font-family: "Font Awesome 5 Free"; + font-weight: 900; +} +.project-site-status-open::before, +.project-cloud-status-public::before { + content: "\f058"; /* check-circle */ + color: #0a0; +} +.project-site-status-restricted::before, +.project-cloud-status-private::before { + content: "\f2bd"; /* user-circle */ + color: #50f; +} +.project-site-status-embargo::before { + content: "\f28b"; /* pause-circle */ + color: #fa0; +} +.project-site-status-forbidden::before { + content: "\f057"; /* times-circle */ + color: #c00; +} +.project-cloud-status-pending::before { + content: "\f017"; /* clock */ + font-weight: 400; + color: #888; +} diff --git a/physionet-django/console/static/console/css/console-noscript.css b/physionet-django/console/static/console/css/console-noscript.css new file mode 100644 index 0000000000..ea7eb868d6 --- /dev/null +++ b/physionet-django/console/static/console/css/console-noscript.css @@ -0,0 +1,8 @@ +.navbar-sidenav .nav-item:focus-within .collapse, +.navbar-sidenav .nav-link-collapse:focus + .collapse { + display: block; +} + +#mainNav .navbar-collapse .navbar-sidenav .nav-item:focus-within .nav-link-collapse.collapsed::after { + content: "\f107"; /* angle-down */ +} diff --git a/physionet-django/console/templates/console/base_console.html b/physionet-django/console/templates/console/base_console.html index 9cef68aa44..8207ecbe77 100644 --- a/physionet-django/console/templates/console/base_console.html +++ b/physionet-django/console/templates/console/base_console.html @@ -7,6 +7,7 @@ {% include "base_css.html" %} + {% block local_css %}{% endblock %} {% include "base_js_top.html" %} {% block local_js_top %}{% endblock %} diff --git a/physionet-django/console/templates/console/cloud_mirrors.html b/physionet-django/console/templates/console/cloud_mirrors.html new file mode 100644 index 0000000000..eafa471146 --- /dev/null +++ b/physionet-django/console/templates/console/cloud_mirrors.html @@ -0,0 +1,96 @@ +{% extends "console/base_console.html" %} + +{% load static %} + +{% block title %}Cloud Mirrors{% endblock %} + +{% block local_css %} + +{% endblock %} + +{% block content %} +
+
+ +
+
+ + + + + + + {% for platform in cloud_platforms %} + + {% endfor %} + + + + {% for project, mirrors in project_mirrors.items %} + + + + + {% for platform_mirror in mirrors %} + + {% endfor %} + + {% endfor %} + +
ProjectVersion + + {{ SITE_NAME }} + + + + {{ platform.name }} + +
+ + {{ project.title }} + + {{ project.version }} + {% if project.deprecated_files %} + Deprecated + {% elif not project.allow_file_downloads %} + Forbidden + {% elif project.embargo_active %} + Embargo + {% elif project.access_policy != AccessPolicy.OPEN %} + Restricted + {% else %} + Open + {% endif %} + + {% if not platform_mirror %} + + {% elif not platform_mirror.sent_files %} + Pending + {% elif platform_mirror.is_private %} + Private + {% else %} + Public + {% endif %} +
+
+
+{% endblock %} diff --git a/physionet-django/console/templates/console/console_navbar.html b/physionet-django/console/templates/console/console_navbar.html index 3ffd116272..61f96aceaf 100644 --- a/physionet-django/console/templates/console/console_navbar.html +++ b/physionet-django/console/templates/console/console_navbar.html @@ -1,4 +1,5 @@ {% load static %} +{% load console_templatetags %} diff --git a/physionet-django/console/templates/console/event_management.html b/physionet-django/console/templates/console/event_management.html index 1ec760e079..2e97085ae4 100644 --- a/physionet-django/console/templates/console/event_management.html +++ b/physionet-django/console/templates/console/event_management.html @@ -38,42 +38,53 @@

{{ event.title }}

{% endif %} + + {% for info in applicant_info %}
-
Total participants:
+
{{ info.title }}
-
{{ event.participants.count }}
+ data-target="#{{ info.id }}">{{ info.count }} - View
+ {% endfor %} +
Description:
-
{{ event.description }}
+
{{ event.description|striptags }}
+ {% include 'console/event_management_manage_dataset.html' %} + + {% for info in applicant_info %} + {% endfor %} + {% endblock %} diff --git a/physionet-django/console/templates/console/guidelines_course.html b/physionet-django/console/templates/console/guidelines_course.html new file mode 100644 index 0000000000..12737f25b9 --- /dev/null +++ b/physionet-django/console/templates/console/guidelines_course.html @@ -0,0 +1,162 @@ +{% extends "console/base_console.html" %} +{% load static %} +{% block title %}Guidelines for Courses{% endblock %} + +{% block content %} +
+
+ Guidelines for creating and updating courses +
+
+
+

To create a new course or update a course, you will need to organize all the course content in a json file + and upload via the + Courses page.

+

Here is the course schema with typing info that you can + follow to create a new course or update a course. Similarly, here is an example json file to create a new course or to update a course

+ +

General Schema Explanation

+
+{
+  "name": "string",
+  "description": "string",
+  "valid_duration": "string",
+  "courses": [{
+    "version": "string",
+    "modules": [
+      {
+        "contents": [
+          {
+            "body": "string",
+            "order": "integer"
+          }
+        ],
+        "quizzes": [
+          {
+            "question": "string",
+            "order": "integer",
+            "choices": [
+              {
+                "body": "string",
+                "is_correct": "boolean"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }]
+}
+      
+
    +
  1. Course Information: Fill out all fields in the JSON file with the appropriate information + about your course, including: +
  2. +
      +
    • The name of the course
    • +
    • A description of the course in html
    • +
    • The valid duration of the course
    • +
    • The version of the course
    • +
    + Note the importance of versioning: It's essential to keep track of the version number + of your course. + Please make sure the version number is unique for each course update. Even minor changes, such as a spelling + correction, + require an update to the version number in the new JSON file (refer to the create and update JSON examples + above). + Please use the Semantic Versioning system (Major.Minor) for version numbers. + If you update a course's major version (e.g., from 1.5 to 2.0), all existing training certificates provided + to + users will expire after a month. To regain access to resources, users must retake the new version of the + training. + +
+
    +
  1. Modules: A course may have one or more modules. Each module contains the course content and quizzes. + Modules must include: + section of the course. Each module should include: +
  2. +
      +
    • A name for the module
    • +
    • A description of the module in html
    • +
    • An order for the module
    • +
    • One or more content items in the module
    • +
    • One or more quizzes in the module
    • +
    +
+
    +
  1. Content: A module may have one or more content blocks. Each content block should include:
  2. +
      +
    • A body for the content in html
    • +
    • An order for the content
    • +
    +
+
    +
  1. Quizzes: A module may have one or more quiz. Each Quiz should include:
  2. +
      +
    • A question for the quiz
    • +
    • An order for the quiz
    • +
    • One or more choices for the quiz, including the correct answer
    • + Order: Note that the order is used to determine which content or quiz comes first, + so please + make sure you order the content and quiz properly.
      For example, here the user will see the content + and quiz in the following order + content1, quiz 1, content 2, quiz 2. +
      
      +"contents": [
      +    {
      +      "body": "This content will display first for the given module.",
      +      "order": 1
      +    },
      +    {
      +      "body": "This content will appear third (order = 3), after the quiz below.",
      +      "order": 3
      +    }
      +],
      +"quizzes": [
      +  {
      +    "question": "This quiz will appear after the first content block (order = 2).",
      +    "order": 2,
      +    "choices": [
      +      {
      +        "body": "This answer is correct. The user will proceed to the next block if they answer it.",
      +        "is_correct": true
      +      },
      +      {
      +        "body": "This choice is incorrect. The user will return to the beginning of the module if they answer it.",
      +        "is_correct": false
      +      }
      +    ]
      +  },
      +  {
      +    "question": "This quiz will appear last, after the final content block (order = 4).",
      +    "order": 4,
      +    "choices": [
      +      {
      +        "body": "choice 1",
      +        "is_correct": true
      +      },
      +      {
      +        "body": "choice 2",
      +        "is_correct": false
      +      }
      +    ]
      +  }
      +]
      +          
      +
      +
    +
+
    +
  1. Choices: A quiz may have one or more choices. Each choice should include:
  2. +
      +
    • A body for the choice
    • +
    • A boolean value of true for the correct answer
    • +
    +
+
+
+
+{% endblock %} diff --git a/physionet-django/console/templates/console/manage_published_project.html b/physionet-django/console/templates/console/manage_published_project.html index de933752d6..fda1899ba0 100644 --- a/physionet-django/console/templates/console/manage_published_project.html +++ b/physionet-django/console/templates/console/manage_published_project.html @@ -408,6 +408,32 @@
Google Cloud

If this message is here for a long time, check the Django "process_tasks"

{% endif %} +
  • +
    AWS
    + {% if not has_s3_credentials %} +

    + Support for Amazon S3 is not enabled. (Check that + AWS_PROFILE and S3_OPEN_ACCESS_BUCKET + are set.) +

    + {% elif not project.aws.bucket_name %} +

    Create a bucket on AWS to store the files associated with this project.

    +
    + {% csrf_token %} +

    +
    + {% elif project.aws.sent_files and project.aws.bucket_name %} +

    The files have been sent to AWS. The bucket name is: {{project.aws.bucket_name}}.

    +
    + {% csrf_token %} +

    +
    + {% endif %} +
  • diff --git a/physionet-django/console/templates/console/news_list.html b/physionet-django/console/templates/console/news_list.html index 64e1348268..c20b0360ae 100644 --- a/physionet-django/console/templates/console/news_list.html +++ b/physionet-django/console/templates/console/news_list.html @@ -4,6 +4,6 @@ {{ news.publish_datetime }} {% if news.url %}{{ news.url }}{% else %}None{% endif %} {% if news.front_page_banner %}{% endif %} - Edit + Edit {% endfor %} diff --git a/physionet-django/console/templates/console/training_type/course_details.html b/physionet-django/console/templates/console/training_type/course_details.html new file mode 100644 index 0000000000..67ca59d067 --- /dev/null +++ b/physionet-django/console/templates/console/training_type/course_details.html @@ -0,0 +1,126 @@ +{% extends "console/base_console.html" %} + +{% load static %} + +{% block title %}{{ training_type }}{% endblock %} + +{% block content %} + + +
    +
    + {{ training_type }} +
    + +
    +
    +
    + +
    +
    + +
    +

    Active Versions

    +
    + + + + + + + + + + + {% for course in active_course_versions %} + + + + + + + + {% endfor %} + +
    NameVersionDownloadExpire
    {{ training_type.name|title }}{{ course.version }} + Download + +
    + {% csrf_token %} + + +
    +
    +

    Note: Users that have taken the particular version of the course that is getting expired, + will need to retake the course or else they will loose credentialing after the number of + days specified above while expiring the course.

    +
    +

    Archived Versions

    +
    + + + + + + + + + + {% for course in inactive_course_versions %} + + + + + + + {% endfor %} + +
    NameVersionAction
    {{ training_type.name|title }}{{ course.version }} + Download +
    +
    +
    +{% endblock %} + + \ No newline at end of file diff --git a/physionet-django/console/templates/console/training_type/index.html b/physionet-django/console/templates/console/training_type/index.html new file mode 100644 index 0000000000..76f43e176c --- /dev/null +++ b/physionet-django/console/templates/console/training_type/index.html @@ -0,0 +1,80 @@ +{% extends "console/base_console.html" %} + +{% load static %} + +{% block title %}{{ SITE_NAME }} Courses{% endblock %} + +{% block content %} + + + +
    +
    + Courses {{ training_types|length }} +
    +
    +
    +
    + + + + + + + + + + {% for training in training_types %} + + + + + + + {% endfor %} + +
    NameValid DurationLatest Version
    {{ training.name|title }}{{ training.valid_duration.days }} days{{ training.courses.last.version }}
    +
    + +
    +
    +{% endblock %} diff --git a/physionet-django/console/templatetags/console_templatetags.py b/physionet-django/console/templatetags/console_templatetags.py index d61424db7e..09d44f46a1 100644 --- a/physionet-django/console/templatetags/console_templatetags.py +++ b/physionet-django/console/templatetags/console_templatetags.py @@ -1,10 +1,33 @@ from django import template +from console.navbar import CONSOLE_NAV_MENU import notification.utility as notification register = template.Library() +@register.simple_tag +def console_nav_menu_items(request): + """ + Get a list of menu items to be shown in the navigation bar. + + Each menu item is a dictionary, representing either a link or a + submenu that contains one or more links. The argument should be + the original HTTP request object; request.user determines which + menu items are visible, and request.path determines which menu + items are marked as "active". + + Typically the return value of this tag will be assigned to a + template variable, e.g.: + + {% console_nav_menu_items request as nav_menu_items %} + {% for item in nav_menu_items %} + ... + {% endfor %} + """ + return CONSOLE_NAV_MENU.get_menu_items(request) + + @register.filter(name='task_count_badge') def task_count_badge(item): """ diff --git a/physionet-django/console/test_views.py b/physionet-django/console/test_views.py index c5be229c41..a1f41a7ac1 100644 --- a/physionet-django/console/test_views.py +++ b/physionet-django/console/test_views.py @@ -11,7 +11,6 @@ from events.models import EventAgreement from project.models import ( ActiveProject, - ArchivedProject, Author, AuthorInvitation, License, @@ -97,8 +96,10 @@ def test_edit_reject(self): 'data_machine_readable':0, 'reusable':1, 'no_phi':0, 'pn_suitable':1, 'editor_comments':'Just bad.', 'decision':0 }) - self.assertTrue(ArchivedProject.objects.filter(slug=project.slug)) - self.assertFalse(ActiveProject.objects.filter(slug=project.slug)) + self.assertTrue(ActiveProject.objects.filter(slug=project.slug, + submission_status=SubmissionStatus.ARCHIVED)) + self.assertFalse(ActiveProject.objects.filter(slug=project.slug, + submission_status=SubmissionStatus.NEEDS_DECISION)) def test_edit(self): """ diff --git a/physionet-django/console/urls.py b/physionet-django/console/urls.py index 11e91f4cdc..af98a9beaf 100644 --- a/physionet-django/console/urls.py +++ b/physionet-django/console/urls.py @@ -1,4 +1,5 @@ from console import views +from training import views as training_views from django.urls import path urlpatterns = [ @@ -19,6 +20,8 @@ path('published-projects///', views.manage_published_project, name='manage_published_project'), path('data-access-request//', views.access_request, name='access_request'), + path('cloud/mirrors/', views.cloud_mirrors, + name='cloud_mirrors'), # Logs path('data-access-logs/', views.project_access_requests_list, name='project_access_requests_list'), @@ -79,21 +82,21 @@ path('users/groups/', views.user_groups, name='user_groups'), path('users/groups//', views.user_group, name='user_group'), path('users//', views.users, name='users'), - path('users/aws-access-list.json', views.users_aws_access_list_json, - name='users_aws_access_list_json'), path('user/manage//', views.user_management, name='user_management'), path('news/', views.news_console, name='news_console'), path('news/add/', views.news_add, name='news_add'), path('news/search/', views.news_search, name='news_search'), - path('news/edit//', views.news_edit, name='news_edit'), + path('news/edit//', views.news_edit, name='news_edit'), path('featured/', views.featured_content, name='featured_content'), path('featured/add', views.add_featured, name='add_featured'), # guidelines path('guidelines/review/', views.guidelines_review, name='guidelines_review'), + path('guidelines/course/', views.guidelines_course, name='guidelines_course'), + path('user-autocomplete/', views.UserAutocomplete.as_view(), name='user-autocomplete'), path('project-autocomplete/', views.ProjectAutocomplete.as_view(), name='project-autocomplete'), @@ -157,6 +160,14 @@ path('event_agreements//new-version/', views.event_agreement_new_version, name='event_agreement_new_version'), path('console/user/manage//', views.event_invite_host, name='event_invite_host'), + + # Courses/On Platform Training + path('courses/', training_views.courses, name='courses'), + path('courses//', training_views.course_details, name='course_details'), + path('courses//download/', + training_views.download_course, name='download_course_version'), + path('courses//expire/', + training_views.expire_course, name='expire_course_version'), ] # Parameters for testing URLs (see physionet/test_urls.py) @@ -170,6 +181,9 @@ 'section_pk': 1, 'news_id': 1, 'username': 'rgmark', + 'news_slug': 'cloud-migration', + 'version': '1.0', + 'training_slug': 'world-101-introduction-to-continents-and-countries', } TEST_CASES = { 'manage_published_project': { diff --git a/physionet-django/console/utility.py b/physionet-django/console/utility.py index e3f531c90d..e6b703db6c 100644 --- a/physionet-django/console/utility.py +++ b/physionet-django/console/utility.py @@ -13,7 +13,6 @@ from django.contrib.sites.models import Site from django.conf import settings from django.urls import reverse - from project.validators import validate_doi import logging @@ -31,6 +30,7 @@ class DOIExistsError(Exception): class DOICreationError(Exception): pass +# Manage GCP buckets def check_bucket_exists(project, version): """ @@ -42,7 +42,6 @@ def check_bucket_exists(project, version): return True return False - def create_bucket(project, version, title, protected=True): """ Create a bucket with either public or private permissions. @@ -489,6 +488,7 @@ def generate_doi_payload(project, core_project=False, event="draft"): author_metadata = {"givenName": author.first_names, "familyName": author.last_name, "name": author.get_full_name(reverse=True)} + author_metadata["affiliation"] = [{"name": a.name} for a in author.affiliations.all()] if author.user.has_orcid(): author_metadata["nameIdentifiers"] = [{ "nameIdentifier": f'https://orcid.org/{author.user.get_orcid_id()}', diff --git a/physionet-django/console/views.py b/physionet-django/console/views.py index 171d2890fa..8714ac4dc4 100644 --- a/physionet-django/console/views.py +++ b/physionet-django/console/views.py @@ -1,7 +1,6 @@ import csv import logging import os -import re from collections import OrderedDict from datetime import datetime from itertools import chain @@ -28,7 +27,7 @@ from django.utils import timezone from django.core.exceptions import PermissionDenied from events.forms import EventAgreementForm, EventDatasetForm -from events.models import Event, EventAgreement, EventDataset +from events.models import Event, EventAgreement, EventDataset, EventApplication from notification.models import News from physionet.forms import set_saved_fields_cookie from physionet.middleware.maintenance import ServiceUnavailable @@ -38,10 +37,10 @@ from project.models import ( GCP, GCPLog, + AWS, AccessLog, AccessPolicy, ActiveProject, - ArchivedProject, DataAccess, DUA, DataAccessRequest, @@ -75,7 +74,14 @@ from physionet.enums import LogCategory from console import forms, utility, services from console.forms import ProjectFilterForm, UserFilterForm - +from project.cloud.s3 import ( + create_s3_bucket, + upload_project_to_S3, + get_bucket_name, + check_s3_bucket_exists, + update_bucket_policy, + has_s3_credentials, +) LOGGER = logging.getLogger(__name__) @@ -121,16 +127,35 @@ def handling_view(request, *args, **kwargs): raise Http404('Unable to access page') return handling_view + +def console_permission_required(perm): + """ + Decorator for a view that requires user permissions. + + If the client is not logged in, or the user doesn't have the + specified permission, the view raises PermissionDenied. + + The required permission name is also stored as an attribute for + introspection purposes. + """ + def wrapper(view): + view = permission_required(perm, raise_exception=True)(view) + view.required_permission = perm + return view + return wrapper + + # ------------------------- Views begin ------------------------- # +@console_permission_required('user.can_view_admin_console') def console_home(request): if not request.user.is_authenticated or not request.user.has_access_to_admin_console(): raise PermissionDenied - return render(request, 'console/console_home.html', {'console_home_nav': True}) + return render(request, 'console/console_home.html') -@permission_required('project.change_activeproject', raise_exception=True) +@console_permission_required('project.change_activeproject') def submitted_projects(request): """ List of active submissions. Editors are assigned here. @@ -147,7 +172,7 @@ def submitted_projects(request): messages.success(request, 'The editor has been assigned') # Submitted projects - projects = ActiveProject.objects.filter(submission_status__gt=SubmissionStatus.UNSUBMITTED).order_by( + projects = ActiveProject.objects.filter(submission_status__gt=SubmissionStatus.ARCHIVED).order_by( 'submission_datetime') # Separate projects by submission status # Awaiting editor assignment @@ -196,11 +221,10 @@ def submitted_projects(request): 'copyedit_projects': copyedit_projects, 'approval_projects': approval_projects, 'publish_projects': publish_projects, - 'submitted_projects_nav': True, 'yesterday': yesterday}) -@permission_required('project.change_activeproject', raise_exception=True) +@console_permission_required('project.change_activeproject') def editor_home(request): """ List of submissions the editor is responsible for @@ -246,7 +270,7 @@ def submission_info_redirect(request, project_slug): return redirect('submission_info', project_slug=project_slug) -@permission_required('project.change_activeproject', raise_exception=True) +@console_permission_required('project.change_activeproject') def submission_info(request, project_slug): """ View information about a project under submission @@ -300,8 +324,7 @@ def submission_info(request, project_slug): 'anonymous_url': anonymous_url, 'url_prefix': url_prefix, 'bulk_url_prefix': bulk_url_prefix, 'reassign_editor_form': reassign_editor_form, - 'embargo_form': embargo_form, - 'project_info_nav': True}) + 'embargo_form': embargo_form}) @handling_editor @@ -334,7 +357,8 @@ def edit_submission(request, project_slug, *args, **kwargs): edit_log.set_quality_assurance_results() # The original object will be deleted if the decision is reject if edit_log.decision == 0: - project = ArchivedProject.objects.get(slug=project_slug) + project = ActiveProject.objects.get(slug=project_slug, + submission_status=SubmissionStatus.ARCHIVED) # Notify the authors notification.edit_decision_notify(request, project, edit_log) return render(request, 'console/edit_complete.html', @@ -670,7 +694,7 @@ def publish_submission(request, project_slug, *args, **kwargs): 'embargo_form': embargo_form}) -@permission_required('project.change_storagerequest', raise_exception=True) +@console_permission_required('project.change_storagerequest') def process_storage_response(request, storage_response_formset): """ Implement the response to a storage request. @@ -699,7 +723,7 @@ def process_storage_response(request, storage_response_formset): f"{notification.RESPONSE_ACTIONS[storage_request.response]}")) -@permission_required('project.change_storagerequest', raise_exception=True) +@console_permission_required('project.change_storagerequest') def storage_requests(request): """ Page for listing and responding to project storage requests @@ -718,11 +742,10 @@ def storage_requests(request): queryset=StorageRequest.objects.filter(is_active=True)) return render(request, 'console/storage_requests.html', - {'storage_response_formset': storage_response_formset, - 'storage_requests_nav': True}) + {'storage_response_formset': storage_response_formset}) -@permission_required('project.change_activeproject', raise_exception=True) +@console_permission_required('project.change_activeproject') def unsubmitted_projects(request): """ List of unsubmitted projects @@ -731,10 +754,10 @@ def unsubmitted_projects(request): 'creation_datetime') projects = paginate(request, projects, 50) return render(request, 'console/unsubmitted_projects.html', - {'projects': projects, 'unsubmitted_projects_nav': True}) + {'projects': projects}) -@permission_required('project.change_publishedproject', raise_exception=True) +@console_permission_required('project.change_publishedproject') def published_projects(request): """ List of published projects @@ -742,7 +765,7 @@ def published_projects(request): projects = PublishedProject.objects.all().order_by('-publish_datetime') projects = paginate(request, projects, 50) return render(request, 'console/published_projects.html', - {'projects': projects, 'published_projects_nav': True}) + {'projects': projects}) @associated_task(PublishedProject, 'pid', read_only=True) @@ -764,7 +787,74 @@ def send_files_to_gcp(pid): project.gcp.save() -@permission_required('project.change_publishedproject', raise_exception=True) +@associated_task(PublishedProject, "pid", read_only=True) +@background() +def send_files_to_aws(pid): + """ + Upload project files to AWS S3 buckets. + + This function retrieves the project identified by 'pid' and uploads + its files to the appropriate AWS S3 bucket. It utilizes the + 'upload_project_to_S3' function from the 'utility' module. + + Args: + pid (int): The unique identifier (ID) of the project to upload. + + Returns: + None + + Note: + - Verify that AWS credentials and configurations are correctly set + up for the S3 client. + """ + project = PublishedProject.objects.get(id=pid) + upload_project_to_S3(project) + project.aws.sent_files = True + project.aws.finished_datetime = timezone.now() + if project.compressed_storage_size: + project.aws.sent_zip = True + project.aws.save() + + +@associated_task(PublishedProject, "pid", read_only=True) +@background() +def update_aws_bucket_policy(pid): + """ + Update the AWS S3 bucket's access policy based on the + project's access policy. + + This function determines the access policy of the project identified + by 'pid' and updates the AWS S3 bucket's access policy accordingly. + It checks if the bucket exists, retrieves its name, and uses the + 'utility.update_bucket_policy' function for the update. + + Args: + pid (int): The unique identifier (ID) of the project for which to + update the bucket policy. + + Returns: + bool: True if the bucket policy was updated successfully, + False otherwise. + + Note: + - Verify that AWS credentials and configurations are correctly set up + for the S3 client. + - The 'updated_policy' variable indicates whether the policy was + updated successfully. + """ + updated_policy = False + project = PublishedProject.objects.get(id=pid) + exists = check_s3_bucket_exists(project) + if exists: + bucket_name = get_bucket_name(project) + update_bucket_policy(project, bucket_name) + updated_policy = True + else: + updated_policy = False + return updated_policy + + +@console_permission_required('project.change_publishedproject') def manage_doi_request(request, project): """ Manage a request to register or update a Digital Object Identifier (DOI). @@ -809,7 +899,7 @@ def manage_doi_request(request, project): return message -@permission_required('project.change_publishedproject', raise_exception=True) +@console_permission_required('project.change_publishedproject') def manage_published_project(request, project_slug, version): """ Manage a published project @@ -892,6 +982,11 @@ def manage_published_project(request, project_slug, version): messages.error(request, 'Project has tasks pending.') else: gcp_bucket_management(request, project, user) + elif 'aws-bucket' in request.POST and has_s3_credentials(): + if any(get_associated_tasks(project, read_only=False)): + messages.error(request, 'Project has tasks pending.') + else: + aws_bucket_management(request, project, user) elif 'platform' in request.POST: data_access_form = forms.DataAccessForm(project=project, data=request.POST) if data_access_form.is_valid(): @@ -953,13 +1048,15 @@ def manage_published_project(request, project_slug, version): 'topic_form': topic_form, 'deprecate_form': deprecate_form, 'has_credentials': has_credentials, + 'has_s3_credentials': has_s3_credentials(), + # 'aws_bucket_exists': s3_bucket_exists, + # 's3_bucket_name': s3_bucket_name, 'data_access_form': data_access_form, 'data_access': data_access, 'rw_tasks': rw_tasks, 'ro_tasks': ro_tasks, 'anonymous_url': anonymous_url, 'passphrase': passphrase, - 'published_projects_nav': True, 'url_prefix': url_prefix, 'bulk_url_prefix': bulk_url_prefix, 'contact_form': contact_form, @@ -971,7 +1068,7 @@ def manage_published_project(request, project_slug, version): ) -@permission_required('project.change_publishedproject', raise_exception=True) +@console_permission_required('project.change_publishedproject') def gcp_bucket_management(request, project, user): """ Create the database object and cloud bucket if they do not exist, and send @@ -1013,18 +1110,109 @@ def gcp_bucket_management(request, project, user): send_files_to_gcp(project.id, verbose_name='GCP - {}'.format(project), creator=user) -@permission_required('project.change_archivedproject', raise_exception=True) +@console_permission_required('project.change_publishedproject') +def aws_bucket_management(request, project, user): + """ + Manage AWS S3 bucket for a project. + + This function is responsible for sending the project's files + to that bucket. It orchestrates the necessary steps to set up + the bucket and populate it with the project's data. + + Args: + project (PublishedProject): The project for which to create and + populate the AWS S3 bucket. + + Returns: + None + + Note: + - Ensure that AWS credentials and configurations are correctly set + up for the S3 client. + """ + is_private = True + + if project.access_policy == AccessPolicy.OPEN: + is_private = False + + bucket_name = get_bucket_name(project) + + if not AWS.objects.filter(project=project).exists(): + AWS.objects.create( + project=project, bucket_name=bucket_name, is_private=is_private + ) + + send_files_to_aws(project.id, verbose_name='AWS - {}'.format(project), creator=user) + + +@console_permission_required('project.change_publishedproject') +def cloud_mirrors(request): + """ + Page for viewing the status of cloud mirrors. + """ + projects = PublishedProject.objects.order_by('-publish_datetime') + + group = request.GET.get('group', 'open') + if group == 'open': + projects = projects.filter(access_policy=AccessPolicy.OPEN) + else: + projects = projects.exclude(access_policy=AccessPolicy.OPEN) + + cloud_platforms = [] + if settings.GOOGLE_APPLICATION_CREDENTIALS: + cloud_platforms.append({ + 'field_name': 'gcp', + 'name': 'GCP', + 'long_name': 'Google Cloud Platform', + }) + if has_s3_credentials(): + cloud_platforms.append({ + 'field_name': 'aws', + 'name': 'AWS', + 'long_name': 'Amazon Web Services', + }) + + # Relevant fields for the status table (see + # templates/console/cloud_mirrors.html) + field_names = [platform['field_name'] for platform in cloud_platforms] + projects = projects.select_related(*field_names).only( + 'slug', + 'title', + 'version', + 'access_policy', + 'allow_file_downloads', + 'deprecated_files', + 'embargo_files_days', + *(f'{field}__is_private' for field in field_names), + *(f'{field}__sent_files' for field in field_names), + ) + + project_mirrors = { + project: [ + getattr(project, field, None) for field in field_names + ] for project in projects + } + + return render(request, 'console/cloud_mirrors.html', { + 'group': group, + 'cloud_platforms': cloud_platforms, + 'project_mirrors': project_mirrors, + }) + + +@console_permission_required('project.change_activeproject') def archived_submissions(request): """ List of archived submissions """ - projects = ArchivedProject.objects.all().order_by('archive_datetime') + projects = ActiveProject.objects.filter(submission_status=SubmissionStatus.ARCHIVED + ).order_by('creation_datetime') projects = paginate(request, projects, 50) return render(request, 'console/archived_submissions.html', - {'projects': projects, 'archived_projects_nav': True}) + {'projects': projects}) -@permission_required('user.view_user', raise_exception=True) +@console_permission_required('user.view_user') def users(request, group='all'): """ List of users @@ -1037,7 +1225,6 @@ def users(request, group='all'): return render(request, 'console/users_admin.html', { 'admin_users': admin_users, 'group': group, - 'user_nav': True, }) elif group == 'active': user_list = user_list.filter(is_active=True) @@ -1046,10 +1233,10 @@ def users(request, group='all'): users = paginate(request, user_list, 50) - return render(request, 'console/users.html', {'users': users, 'group': group, 'user_nav': True}) + return render(request, 'console/users.html', {'users': users, 'group': group}) -@permission_required('user.view_user', raise_exception=True) +@console_permission_required('user.view_user') def user_groups(request): """ List of all user groups @@ -1057,10 +1244,10 @@ def user_groups(request): groups = Group.objects.all().order_by('name') for group in groups: group.user_count = User.objects.filter(groups=group).count() - return render(request, 'console/user_groups.html', {'groups': groups, 'user_nav': True, 'user_groups_nav': True}) + return render(request, 'console/user_groups.html', {'groups': groups}) -@permission_required('user.view_user', raise_exception=True) +@console_permission_required('user.view_user') def user_group(request, group): """ Shows details of a user group, lists users in the group, lists permissions for the group @@ -1071,11 +1258,11 @@ def user_group(request, group): return render( request, 'console/user_group.html', - {'group': group, 'users': users, 'permissions': permissions, 'user_nav': True, 'user_groups_nav': True} + {'group': group, 'users': users, 'permissions': permissions} ) -@permission_required('user.view_user', raise_exception=True) +@console_permission_required('user.view_user') def user_management(request, username): """ Admin page for managing an individual user account. @@ -1113,9 +1300,11 @@ def user_management(request, username): authors__user=user, submission_status=SubmissionStatus.UNSUBMITTED ).order_by("-creation_datetime") projects["Submitted"] = ActiveProject.objects.filter( - authors__user=user, submission_status__gt=SubmissionStatus.UNSUBMITTED + authors__user=user, submission_status__gt=SubmissionStatus.ARCHIVED ).order_by("-submission_datetime") - projects['Archived'] = ArchivedProject.objects.filter(authors__user=user).order_by('-archive_datetime') + projects['Archived'] = ActiveProject.objects.filter(authors__user=user, + submission_status=SubmissionStatus.ARCHIVED + ).order_by('-creation_datetime') projects['Published'] = PublishedProject.objects.filter(authors__user=user).order_by('-publish_datetime') credentialing_app = CredentialApplication.objects.filter(user=user).order_by("application_datetime") @@ -1133,7 +1322,7 @@ def user_management(request, username): 'gcp_info': gcp_info}) -@permission_required('user.view_user', raise_exception=True) +@console_permission_required('user.view_user') def users_search(request, group): """ Search user list. @@ -1169,39 +1358,7 @@ def users_search(request, group): raise Http404() -@permission_required('user.view_user', raise_exception=True) -def users_aws_access_list_json(request): - """ - Generate JSON list of currently authorized AWS accounts. - - This is a temporary kludge to support an upcoming event (November - 2022). Don't rely on this function; it will go away. - """ - projects_datathon = [ - "mimiciv-2.2" - ] - published_projects = PublishedProject.objects.all() - users_with_awsid = User.objects.filter(cloud_information__aws_id__isnull=False) - datasets = {} - datasets['datasets'] = [] - aws_id_pattern = r"\b\d{12}\b" - - for project in published_projects: - dataset = {} - project_name = project.slug + "-" + project.version - if project_name in projects_datathon: - dataset['name'] = project_name - dataset['accounts'] = [] - for user in users_with_awsid: - if can_view_project_files(project, user): - if re.search(aws_id_pattern, user.cloud_information.aws_id): - dataset['accounts'].append(user.cloud_information.aws_id) - datasets['datasets'].append(dataset) - - return JsonResponse(datasets) - - -@permission_required('user.change_credentialapplication', raise_exception=True) +@console_permission_required('user.change_credentialapplication') def known_references_search(request): """ Search credential applications and user list. @@ -1229,7 +1386,7 @@ def known_references_search(request): raise Http404() -@permission_required('user.change_credentialapplication', raise_exception=True) +@console_permission_required('user.change_credentialapplication') def complete_credential_applications(request): """ Legacy page for processing credentialing applications. @@ -1237,7 +1394,7 @@ def complete_credential_applications(request): return redirect(credential_processing) -@permission_required('user.change_credentialapplication', raise_exception=True) +@console_permission_required('user.change_credentialapplication') def complete_list_credentialed_people(request): """ Legacy page that displayed a list of all approved MIMIC users. @@ -1245,7 +1402,7 @@ def complete_list_credentialed_people(request): return redirect(credential_applications, "successful") -@permission_required('user.change_credentialapplication', raise_exception=True) +@console_permission_required('user.change_credentialapplication') def process_credential_application(request, application_slug): """ Process a credential application. View details, advance to next stage, @@ -1386,12 +1543,12 @@ def process_credential_application(request, application_slug): {'application': application, 'app_user': application.user, 'intermediate_credential_form': intermediate_credential_form, 'credential_review_form': credential_review_form, - 'processing_credentials_nav': True, 'page_title': page_title, + 'page_title': page_title, 'contact_cred_ref_form': contact_cred_ref_form, 'training_list': training}) -@permission_required('user.change_credentialapplication', raise_exception=True) +@console_permission_required('user.change_credentialapplication') def credential_processing(request): """ Process applications for credentialed access. @@ -1435,11 +1592,10 @@ def credential_processing(request): 'personal_applications': personal_applications, 'reference_applications': reference_applications, 'response_applications': response_applications, - 'final_applications': final_applications, - 'processing_credentials_nav': True}) + 'final_applications': final_applications}) -@permission_required('user.change_credentialapplication', raise_exception=True) +@console_permission_required('user.change_credentialapplication') def view_credential_application(request, application_slug): """ View a credential application in any status. @@ -1459,10 +1615,10 @@ def view_credential_application(request, application_slug): return render(request, 'console/view_credential_application.html', {'application': application, 'app_user': application.user, - 'form': form, 'past_credentials_nav': True, 'CredentialApplication': CredentialApplication}) + 'form': form, 'CredentialApplication': CredentialApplication}) -@permission_required('user.change_credentialapplication', raise_exception=True) +@console_permission_required('user.change_credentialapplication') def credential_applications(request, status): """ Inactive credential applications. Split into successful and @@ -1536,12 +1692,12 @@ def credential_applications(request, status): pending_apps = paginate(request, pending_apps, 50) return render(request, 'console/credential_applications.html', - {'applications': all_successful_apps, 'past_credentials_nav': True, + {'applications': all_successful_apps, 'u_applications': unsuccessful_apps, 'p_applications': pending_apps}) -@permission_required('user.change_credentialapplication', raise_exception=True) +@console_permission_required('user.change_credentialapplication') def search_credential_applications(request): """ Search past credentialing applications. @@ -1593,7 +1749,7 @@ def search_credential_applications(request): return all_successful_apps, unsuccessful_apps, pending_apps -@permission_required('user.change_credentialapplication', raise_exception=True) +@console_permission_required('user.change_credentialapplication') def credentialed_user_info(request, username): try: c_user = User.objects.get(username__iexact=username) @@ -1605,7 +1761,7 @@ def credentialed_user_info(request, username): 'CredentialApplication': CredentialApplication}) -@permission_required('user.can_review_training', raise_exception=True) +@console_permission_required('user.can_review_training') def training_list(request, status): """ List all training applications. @@ -1657,7 +1813,6 @@ def training_list(request, status): 'valid_count': valid_training.count(), 'expired_count': expired_training.count(), 'rejected_count': rejected_training.count(), - 'training_nav': True, }, ) @@ -1685,7 +1840,7 @@ def search_training_applications(request, display_training): return display_training -@permission_required('user.can_review_training', raise_exception=True) +@console_permission_required('user.can_review_training') def training_process(request, pk): training = get_object_or_404(Training.objects.select_related('training_type', 'user__profile').get_review(), pk=pk) @@ -1761,14 +1916,14 @@ def training_process(request, pk): ) -@permission_required('user.can_review_training', raise_exception=True) +@console_permission_required('user.can_review_training') def training_detail(request, pk): training = get_object_or_404(Training.objects.prefetch_related('training_type'), pk=pk) return render(request, 'console/training_detail.html', {'training': training}) -@permission_required('notification.change_news', raise_exception=True) +@console_permission_required('notification.change_news') def news_console(request): """ List of news items @@ -1776,10 +1931,10 @@ def news_console(request): news_items = News.objects.all().order_by('-publish_datetime') news_items = paginate(request, news_items, 50) return render(request, 'console/news_console.html', - {'news_items': news_items, 'news_nav': True}) + {'news_items': news_items}) -@permission_required('notification.change_news', raise_exception=True) +@console_permission_required('notification.change_news') def news_add(request): if request.method == 'POST': form = forms.NewsForm(data=request.POST) @@ -1791,11 +1946,10 @@ def news_add(request): else: form = forms.NewsForm() - return render(request, 'console/news_add.html', {'form': form, - 'news_nav': True}) + return render(request, 'console/news_add.html', {'form': form}) -@permission_required('notification.change_news', raise_exception=True) +@console_permission_required('notification.change_news') def news_search(request): """ Filtered list of news items @@ -1810,10 +1964,10 @@ def news_search(request): raise Http404() -@permission_required('notification.change_news', raise_exception=True) -def news_edit(request, news_id): +@console_permission_required('notification.change_news') +def news_edit(request, news_slug): try: - news = News.objects.get(id=news_id) + news = News.objects.get(slug=news_slug) except News.DoesNotExist: raise Http404() saved = False @@ -1832,13 +1986,13 @@ def news_edit(request, news_id): form = forms.NewsForm(instance=news) response = render(request, 'console/news_edit.html', {'news': news, - 'form': form, 'news_nav': True}) + 'form': form}) if saved: set_saved_fields_cookie(form, request.path, response) return response -@permission_required('project.can_edit_featured_content', raise_exception=True) +@console_permission_required('project.can_edit_featured_content') def featured_content(request): """ List of news items @@ -1882,10 +2036,10 @@ def featured_content(request): ).order_by('featured') return render(request, 'console/featured_content.html', - {'featured_content': featured_content, 'featured_content_nav': True}) + {'featured_content': featured_content}) -@permission_required('project.can_edit_featured_content', raise_exception=True) +@console_permission_required('project.can_edit_featured_content') def add_featured(request): """ List of news items @@ -1914,11 +2068,10 @@ def add_featured(request): return render(request, 'console/add_featured.html', {'title': title, 'projects': projects, 'form': form, - 'valid_search': valid_search, - 'featured_content_nav': True}) + 'valid_search': valid_search}) -@permission_required('project.can_view_project_guidelines', raise_exception=True) +@console_permission_required('project.can_view_project_guidelines') def guidelines_review(request): """ Guidelines for reviewers. @@ -1927,7 +2080,16 @@ def guidelines_review(request): {'guidelines_review_nav': True}) -@permission_required('project.can_view_stats', raise_exception=True) +@permission_required('training.can_view_course_guidelines', raise_exception=True) +def guidelines_course(request): + """ + Guidelines for course creators. + """ + return render(request, 'console/guidelines_course.html', + {'guidelines_course_nav': True}) + + +@console_permission_required('project.can_view_stats') def editorial_stats(request): """ Editorial stats for reviewers. @@ -1966,11 +2128,11 @@ def editorial_stats(request): except StatisticsError: stats[y].append(None) - return render(request, 'console/editorial_stats.html', {'stats_nav': True, + return render(request, 'console/editorial_stats.html', { 'submenu': 'editorial', 'stats': stats}) -@permission_required('project.can_view_stats', raise_exception=True) +@console_permission_required('project.can_view_stats') def credentialing_stats(request): """ Credentialing metrics. @@ -2033,16 +2195,15 @@ def credentialing_stats(request): stats[y]['time_to_decision'] = None return render(request, 'console/credentialing_stats.html', - {'stats_nav': True, 'submenu': 'credential', + {'submenu': 'credential', 'stats': stats}) -@permission_required('project.can_view_stats', raise_exception=True) +@console_permission_required('project.can_view_stats') def submission_stats(request): stats = OrderedDict() todays_date = datetime.today() - all_projects = [PublishedProject.objects.filter(is_legacy=False), ActiveProject.objects.all(), - ArchivedProject.objects.all()] + all_projects = [PublishedProject.objects.filter(is_legacy=False), ActiveProject.objects.all()] cur_year = todays_date.year cur_month = todays_date.month @@ -2088,10 +2249,10 @@ def submission_stats(request): pass return render(request, 'console/submission_stats.html', - {'stats_nav': True, 'submenu': 'submission', 'stats': stats}) + {'submenu': 'submission', 'stats': stats}) -@permission_required('project.can_view_access_logs', raise_exception=True) +@console_permission_required('project.can_view_access_logs') def download_credentialed_users(request): """ CSV create and download for database access. @@ -2153,17 +2314,17 @@ def download_credentialed_users(request): return response -@permission_required('project.can_view_access_logs', raise_exception=True) +@console_permission_required('project.can_view_access_logs') def project_access_manage(request, pid): projects = PublishedProject.objects.prefetch_related('duasignature_set__user__profile') c_project = get_object_or_404(projects, id=pid, access_policy=AccessPolicy.CREDENTIALED) return render(request, 'console/project_access_manage.html', { 'c_project': c_project, 'project_members': c_project.duasignature_set.all(), - 'project_access_logs_nav': True}) + }) -@permission_required('project.can_view_access_logs', raise_exception=True) +@console_permission_required('project.can_view_access_logs') def project_access_requests_list(request): projects = PublishedProject.objects.filter(access_policy=AccessPolicy.CONTRIBUTOR_REVIEW).annotate( access_requests_count=Count('data_access_requests') @@ -2176,11 +2337,11 @@ def project_access_requests_list(request): projects = paginate(request, projects, 50) return render(request, 'console/project_access_requests_list.html', { - 'access_requests_nav': True, 'projects': projects + 'projects': projects }) -@permission_required('project.can_view_access_logs', raise_exception=True) +@console_permission_required('project.can_view_access_logs') def project_access_requests_detail(request, pk): project = get_object_or_404(PublishedProject, access_policy=AccessPolicy.CONTRIBUTOR_REVIEW, pk=pk) access_requests = DataAccessRequest.objects.filter(project=project) @@ -2193,18 +2354,18 @@ def project_access_requests_detail(request, pk): access_requests = paginate(request, access_requests, 50) return render(request, 'console/project_access_requests_detail.html', { - 'access_requests_nav': True, 'project': project, 'access_requests': access_requests + 'project': project, 'access_requests': access_requests }) -@permission_required('project.can_view_access_logs', raise_exception=True) +@console_permission_required('project.can_view_access_logs') def access_request(request, pk): access_request = get_object_or_404(DataAccessRequest, pk=pk) return render(request, 'console/access_request.html', {'access_request': access_request}) -@permission_required('project.can_view_access_logs', raise_exception=True) +@console_permission_required('project.can_view_access_logs') def project_access_logs(request): c_projects = PublishedProject.objects.annotate( log_count=Count('logs', filter=Q(logs__category=LogCategory.ACCESS))) @@ -2220,11 +2381,11 @@ def project_access_logs(request): c_projects = paginate(request, c_projects, 50) return render(request, 'console/project_access_logs.html', { - 'c_projects': c_projects, 'project_access_logs_nav': True, + 'c_projects': c_projects, }) -@permission_required('project.can_view_access_logs', raise_exception=True) +@console_permission_required('project.can_view_access_logs') def project_access_logs_detail(request, pid): c_project = get_object_or_404(PublishedProject, id=pid) logs = ( @@ -2249,11 +2410,11 @@ def project_access_logs_detail(request, pid): return render(request, 'console/project_access_logs_detail.html', { 'c_project': c_project, 'logs': logs, - 'project_access_logs_nav': True, 'user_filter_form': user_filter_form + 'user_filter_form': user_filter_form }) -@permission_required('project.can_view_access_logs', raise_exception=True) +@console_permission_required('project.can_view_access_logs') def download_project_accesses(request, pk): headers = ['User', 'Email address', 'First access', 'Last access', 'Duration', 'Count'] @@ -2284,7 +2445,7 @@ def download_project_accesses(request, pk): return response -@permission_required('project.can_view_access_logs', raise_exception=True) +@console_permission_required('project.can_view_access_logs') def user_access_logs(request): users = ( User.objects.filter(is_active=True) @@ -2304,11 +2465,11 @@ def user_access_logs(request): users = paginate(request, users, 50) return render(request, 'console/user_access_logs.html', { - 'users': users, 'user_access_logs_nav': True, + 'users': users, }) -@permission_required('project.can_view_access_logs', raise_exception=True) +@console_permission_required('project.can_view_access_logs') def user_access_logs_detail(request, pid): user = get_object_or_404(User, id=pid, is_active=True) logs = ( @@ -2332,12 +2493,12 @@ def user_access_logs_detail(request, pid): project_filter_form = ProjectFilterForm() return render(request, 'console/user_access_logs_detail.html', { - 'user': user, 'logs': logs, 'user_access_logs_nav': True, + 'user': user, 'logs': logs, 'project_filter_form': project_filter_form }) -@permission_required('project.can_view_access_logs', raise_exception=True) +@console_permission_required('project.can_view_access_logs') def download_user_accesses(request, pk): headers = ['Project name', 'First access', 'Last access', 'Duration', 'Count'] @@ -2364,7 +2525,7 @@ def download_user_accesses(request, pk): return response -@permission_required('project.can_view_access_logs', raise_exception=True) +@console_permission_required('project.can_view_access_logs') def gcp_signed_urls_logs(request): projects = ActiveProject.objects.annotate( log_count=Count('logs', filter=Q(logs__category=LogCategory.GCP))) @@ -2376,11 +2537,11 @@ def gcp_signed_urls_logs(request): projects = paginate(request, projects, 50) return render(request, 'console/gcp_logs.html', { - 'projects': projects, 'gcp_logs_nav': True, + 'projects': projects, }) -@permission_required('project.can_view_access_logs', raise_exception=True) +@console_permission_required('project.can_view_access_logs') def gcp_signed_urls_logs_detail(request, pk): project = get_object_or_404(ActiveProject, pk=pk) logs = project.logs.order_by('-creation_datetime').prefetch_related('project').annotate( @@ -2390,11 +2551,10 @@ def gcp_signed_urls_logs_detail(request, pk): return render(request, 'console/gcp_logs_detail.html', { 'project': project, 'logs': logs, - 'gcp_logs_nav': True, }) -@permission_required('project.can_view_access_logs', raise_exception=True) +@console_permission_required('project.can_view_access_logs') def download_signed_urls_logs(request, pk): headers = ['User', 'Email address', 'First access', 'Last access', 'Duration', 'Data', 'Count'] @@ -2458,7 +2618,7 @@ def get_queryset(self): return qs -@permission_required('user.change_credentialapplication', raise_exception=True) +@console_permission_required('user.change_credentialapplication') def known_references(request): """ List all known references witht he option of removing the contact date @@ -2484,10 +2644,11 @@ def known_references(request): all_known_ref = paginate(request, all_known_ref, 50) return render(request, 'console/known_references.html', { - 'all_known_ref': all_known_ref, 'known_ref_nav': True}) + 'all_known_ref': all_known_ref, + }) -@permission_required('physionet.view_redirect', raise_exception=True) +@console_permission_required('redirects.view_redirect') def view_redirects(request): """ Display a list of redirected URLs. @@ -2496,10 +2657,10 @@ def view_redirects(request): return render( request, 'console/redirects.html', - {'redirects': redirects, 'redirects_nav': True}) + {'redirects': redirects}) -@permission_required('physionet.change_frontpagebutton', raise_exception=True) +@console_permission_required('physionet.change_frontpagebutton') def frontpage_buttons(request): if request.method == 'POST': @@ -2518,10 +2679,10 @@ def frontpage_buttons(request): return render( request, 'console/frontpage_button/index.html', - {'frontpage_buttons': frontpage_buttons, 'frontpage_buttons_nav': True}) + {'frontpage_buttons': frontpage_buttons}) -@permission_required('physionet.change_frontpagebutton', raise_exception=True) +@console_permission_required('physionet.change_frontpagebutton') def frontpage_button_add(request): if request.method == 'POST': frontpage_button_form = forms.FrontPageButtonForm(data=request.POST) @@ -2539,7 +2700,7 @@ def frontpage_button_add(request): ) -@permission_required('physionet.change_frontpagebutton', raise_exception=True) +@console_permission_required('physionet.change_frontpagebutton') def frontpage_button_edit(request, button_pk): frontpage_button = get_object_or_404(FrontPageButton, pk=button_pk) @@ -2562,7 +2723,7 @@ def frontpage_button_edit(request, button_pk): ) -@permission_required('physionet.change_frontpagebutton', raise_exception=True) +@console_permission_required('physionet.change_frontpagebutton') def frontpage_button_delete(request, button_pk): frontpage_button = get_object_or_404(FrontPageButton, pk=button_pk) if request.method == 'POST': @@ -2572,7 +2733,7 @@ def frontpage_button_delete(request, button_pk): return HttpResponseRedirect(reverse('frontpage_buttons')) -@permission_required('physionet.change_staticpage', raise_exception=True) +@console_permission_required('physionet.change_staticpage') def static_pages(request): if request.method == 'POST': up = request.POST.get('up') @@ -2590,10 +2751,10 @@ def static_pages(request): return render( request, 'console/static_page/index.html', - {'pages': pages, 'static_pages_nav': True}) + {'pages': pages}) -@permission_required('physionet.change_staticpage', raise_exception=True) +@console_permission_required('physionet.change_staticpage') def static_page_add(request): if request.method == 'POST': static_page_form = forms.StaticPageForm(data=request.POST) @@ -2611,7 +2772,7 @@ def static_page_add(request): ) -@permission_required('physionet.change_staticpage', raise_exception=True) +@console_permission_required('physionet.change_staticpage') def static_page_edit(request, page_pk): static_page = get_object_or_404(StaticPage, pk=page_pk) @@ -2631,7 +2792,7 @@ def static_page_edit(request, page_pk): ) -@permission_required('physionet.change_staticpage', raise_exception=True) +@console_permission_required('physionet.change_staticpage') def static_page_delete(request, page_pk): static_page = get_object_or_404(StaticPage, pk=page_pk) if request.method == 'POST': @@ -2641,7 +2802,7 @@ def static_page_delete(request, page_pk): return HttpResponseRedirect(reverse('static_pages')) -@permission_required('physionet.change_staticpage', raise_exception=True) +@console_permission_required('physionet.change_staticpage') def static_page_sections(request, page_pk): static_page = get_object_or_404(StaticPage, pk=page_pk) if request.method == 'POST': @@ -2666,11 +2827,11 @@ def static_page_sections(request, page_pk): return render( request, 'console/static_page_sections.html', - {'sections': sections, 'page': static_page, 'section_form': section_form, 'static_pages_nav': True}, + {'sections': sections, 'page': static_page, 'section_form': section_form}, ) -@permission_required('physionet.change_staticpage', raise_exception=True) +@console_permission_required('physionet.change_staticpage') def static_page_sections_delete(request, page_pk, section_pk): static_page = get_object_or_404(StaticPage, pk=page_pk) if request.method == 'POST': @@ -2681,7 +2842,7 @@ def static_page_sections_delete(request, page_pk, section_pk): return redirect('static_page_sections', page_pk=static_page.pk) -@permission_required('physionet.change_staticpage', raise_exception=True) +@console_permission_required('physionet.change_staticpage') def static_page_sections_edit(request, page_pk, section_pk): static_page = get_object_or_404(StaticPage, pk=page_pk) section = get_object_or_404(Section, static_page=static_page, pk=section_pk) @@ -2696,11 +2857,11 @@ def static_page_sections_edit(request, page_pk, section_pk): return render( request, 'console/static_page_sections_edit.html', - {'section_form': section_form, 'static_pages_nav': True, 'page': static_page, 'section': section}, + {'section_form': section_form, 'page': static_page, 'section': section}, ) -@permission_required('project.add_license', raise_exception=True) +@console_permission_required('project.add_license') def license_list(request): if request.method == 'POST': license_form = forms.LicenseForm(data=request.POST) @@ -2719,11 +2880,11 @@ def license_list(request): return render( request, 'console/license_list.html', - {'license_nav': True, 'licenses': licenses, 'license_form': license_form} + {'licenses': licenses, 'license_form': license_form} ) -@permission_required('project.add_license', raise_exception=True) +@console_permission_required('project.add_license') def license_detail(request, pk): license = get_object_or_404(License, pk=pk) @@ -2741,11 +2902,11 @@ def license_detail(request, pk): return render( request, 'console/license_detail.html', - {'license_nav': True, 'license': license, 'license_form': license_form} + {'license': license, 'license_form': license_form} ) -@permission_required('project.add_license', raise_exception=True) +@console_permission_required('project.add_license') def license_delete(request, pk): if request.method == 'POST': license = get_object_or_404(License, pk=pk) @@ -2754,7 +2915,7 @@ def license_delete(request, pk): return redirect('license_list') -@permission_required('project.add_license', raise_exception=True) +@console_permission_required('project.add_license') def license_new_version(request, pk): license = get_object_or_404(License, pk=pk) @@ -2775,11 +2936,11 @@ def license_new_version(request, pk): return render( request, 'console/license_new_version.html', - {'license_nav': True, 'license': license, 'license_form': license_form} + {'license': license, 'license_form': license_form} ) -@permission_required('project.add_dua', raise_exception=True) +@console_permission_required('project.add_dua') def dua_list(request): if request.method == 'POST': dua_form = forms.DUAForm(data=request.POST) @@ -2795,10 +2956,10 @@ def dua_list(request): duas = DUA.objects.order_by('access_policy', 'name') duas = paginate(request, duas, 20) - return render(request, 'console/dua_list.html', {'dua_nav': True, 'duas': duas, 'dua_form': dua_form}) + return render(request, 'console/dua_list.html', {'duas': duas, 'dua_form': dua_form}) -@permission_required('project.add_dua', raise_exception=True) +@console_permission_required('project.add_dua') def dua_detail(request, pk): dua = get_object_or_404(DUA, pk=pk) @@ -2813,10 +2974,10 @@ def dua_detail(request, pk): else: dua_form = forms.DUAForm(instance=dua) - return render(request, 'console/dua_detail.html', {'dua_nav': True, 'dua': dua, 'dua_form': dua_form}) + return render(request, 'console/dua_detail.html', {'dua': dua, 'dua_form': dua_form}) -@permission_required('project.add_dua', raise_exception=True) +@console_permission_required('project.add_dua') def dua_delete(request, pk): if request.method == 'POST': dua = get_object_or_404(DUA, pk=pk) @@ -2825,7 +2986,7 @@ def dua_delete(request, pk): return redirect("dua_list") -@permission_required('project.add_dua', raise_exception=True) +@console_permission_required('project.add_dua') def dua_new_version(request, pk): dua = get_object_or_404(DUA, pk=pk) @@ -2843,10 +3004,10 @@ def dua_new_version(request, pk): dua_data['version'] = None dua_form = forms.DUAForm(initial=dua_data) - return render(request, 'console/dua_new_version.html', {'dua_nav': True, 'dua': dua, 'dua_form': dua_form}) + return render(request, 'console/dua_new_version.html', {'dua': dua, 'dua_form': dua_form}) -@permission_required('project.add_codeofconduct', raise_exception=True) +@console_permission_required('project.add_codeofconduct') def code_of_conduct_list(request): if request.method == 'POST': code_of_conduct_form = forms.CodeOfConductForm(data=request.POST) @@ -2866,14 +3027,13 @@ def code_of_conduct_list(request): request, 'console/code_of_conduct_list.html', { - 'code_of_conduct_nav': True, 'code_of_conducts': code_of_conducts, 'code_of_conduct_form': code_of_conduct_form, }, ) -@permission_required('project.add_codeofconduct', raise_exception=True) +@console_permission_required('project.add_codeofconduct') def code_of_conduct_detail(request, pk): code_of_conduct = get_object_or_404(CodeOfConduct, pk=pk) if request.method == 'POST': @@ -2891,14 +3051,13 @@ def code_of_conduct_detail(request, pk): request, 'console/code_of_conduct_detail.html', { - 'code_of_conduct_nav': True, 'code_of_conduct': code_of_conduct, 'code_of_conduct_form': code_of_conduct_form, }, ) -@permission_required('project.add_codeofconduct', raise_exception=True) +@console_permission_required('project.add_codeofconduct') def code_of_conduct_delete(request, pk): if request.method == 'POST': code_of_conduct = get_object_or_404(CodeOfConduct, pk=pk) @@ -2907,7 +3066,7 @@ def code_of_conduct_delete(request, pk): return redirect("code_of_conduct_list") -@permission_required('project.add_codeofconduct', raise_exception=True) +@console_permission_required('project.add_codeofconduct') def code_of_conduct_new_version(request, pk): code_of_conduct = get_object_or_404(CodeOfConduct, pk=pk) if request.method == 'POST': @@ -2928,14 +3087,13 @@ def code_of_conduct_new_version(request, pk): request, 'console/code_of_conduct_new_version.html', { - 'code_of_conduct_nav': True, 'code_of_conduct': code_of_conduct, 'code_of_conduct_form': code_of_conduct_form, }, ) -@permission_required('project.add_codeofconduct', raise_exception=True) +@console_permission_required('project.add_codeofconduct') def code_of_conduct_activate(request, pk): CodeOfConduct.objects.filter(is_active=True).update(is_active=False) @@ -2948,7 +3106,7 @@ def code_of_conduct_activate(request, pk): return redirect("code_of_conduct_list") -@permission_required('user.view_all_events', raise_exception=True) +@console_permission_required('user.view_all_events') def event_active(request): """ List of events @@ -2962,7 +3120,7 @@ def event_active(request): }) -@permission_required('user.view_all_events', raise_exception=True) +@console_permission_required('user.view_all_events') def event_archive(request): """ List of archived events @@ -2976,7 +3134,7 @@ def event_archive(request): }) -@permission_required('user.view_all_events', raise_exception=True) +@console_permission_required('user.view_all_events') def event_management(request, event_slug): """ Admin page for managing an individual Event. @@ -2985,44 +3143,90 @@ def event_management(request, event_slug): # handle the add dataset form(s) if request.method == "POST": - if 'add-event-dataset' in request.POST.keys(): + if "add-event-dataset" in request.POST.keys(): event_dataset_form = EventDatasetForm(request.POST) if event_dataset_form.is_valid(): - if selected_event.datasets.filter( - dataset=event_dataset_form.cleaned_data['dataset'], - access_type=event_dataset_form.cleaned_data['access_type'], - is_active=True).count() == 0: + active_datasets = selected_event.datasets.filter( + dataset=event_dataset_form.cleaned_data["dataset"], + access_type=event_dataset_form.cleaned_data["access_type"], + is_active=True) + if active_datasets.count() == 0: event_dataset_form.instance.event = selected_event event_dataset_form.save() - messages.success(request, "The dataset has been added to the event.") + messages.success( + request, "The dataset has been added to the event." + ) else: - messages.error(request, "The dataset has already been added to the event.") + messages.error( + request, "The dataset has already been added to the event." + ) else: messages.error(request, event_dataset_form.errors) - return redirect('event_management', event_slug=event_slug) - elif 'remove-event-dataset' in request.POST.keys(): - event_dataset_id = request.POST['remove-event-dataset'] + return redirect("event_management", event_slug=event_slug) + elif "remove-event-dataset" in request.POST.keys(): + event_dataset_id = request.POST["remove-event-dataset"] event_dataset = get_object_or_404(EventDataset, pk=event_dataset_id) event_dataset.revoke_dataset_access() messages.success(request, "The dataset has been removed from the event.") - return redirect('event_management', event_slug=event_slug) + return redirect("event_management", event_slug=event_slug) else: event_dataset_form = EventDatasetForm() + participants = selected_event.participants.all() + pending_applications = selected_event.applications.filter( + status=EventApplication.EventApplicationStatus.WAITLISTED + ) + rejected_applications = selected_event.applications.filter( + status=EventApplication.EventApplicationStatus.NOT_APPROVED + ) + withdrawn_applications = selected_event.applications.filter( + status=EventApplication.EventApplicationStatus.WITHDRAWN + ) + event_datasets = selected_event.datasets.filter(is_active=True) + applicant_info = [ + { + "id": "participants", + "title": "Total participants:", + "count": len(participants), + "objects": participants, + }, + { + "id": "pending_applications", + "title": "Pending applications:", + "count": len(pending_applications), + "objects": pending_applications, + }, + { + "id": "rejected_applications", + "title": "Rejected applications:", + "count": len(rejected_applications), + "objects": rejected_applications, + }, + { + "id": "withdrawn_applications", + "title": "Withdrawn applications:", + "count": len(withdrawn_applications), + "objects": withdrawn_applications, + }, + ] + return render( request, - 'console/event_management.html', + "console/event_management.html", { - 'event': selected_event, - 'event_dataset_form': event_dataset_form, - 'event_datasets': event_datasets, - }) + "event": selected_event, + "event_dataset_form": event_dataset_form, + "event_datasets": event_datasets, + "applicant_info": applicant_info, + "participants": participants, + }, + ) -@permission_required('events.add_eventagreement', raise_exception=True) +@console_permission_required('events.add_eventagreement') def event_agreement_list(request): if request.method == 'POST': event_agreement_form = EventAgreementForm(data=request.POST) @@ -3043,14 +3247,13 @@ def event_agreement_list(request): request, 'console/event_agreement_list.html', { - 'event_agreement_nav': True, 'event_agreements': event_agreements, 'event_agreement_form': event_agreement_form } ) -@permission_required('events.add_eventagreement', raise_exception=True) +@console_permission_required('events.add_eventagreement') def event_agreement_new_version(request, pk): event_agreement = get_object_or_404(EventAgreement, pk=pk) @@ -3073,14 +3276,13 @@ def event_agreement_new_version(request, pk): request, 'console/event_agreement_new_version.html', { - 'event_agreement_nav': True, 'event_agreement': event_agreement, 'event_agreement_form': event_agreement_form } ) -@permission_required('events.add_eventagreement', raise_exception=True) +@console_permission_required('events.add_eventagreement') def event_agreement_detail(request, pk): event_agreement = get_object_or_404(EventAgreement, pk=pk) @@ -3099,14 +3301,13 @@ def event_agreement_detail(request, pk): request, 'console/event_agreement_detail.html', { - 'event_agreement_nav': True, 'event_agreement': event_agreement, 'event_agreement_form': event_agreement_form } ) -@permission_required('events.add_eventagreement', raise_exception=True) +@console_permission_required('events.add_eventagreement') def event_agreement_delete(request, pk): if request.method == 'POST': event_agreement = get_object_or_404(EventAgreement, pk=pk) diff --git a/physionet-django/events/templates/events/event_applications.html b/physionet-django/events/templates/events/event_applications.html new file mode 100644 index 0000000000..e501081eb2 --- /dev/null +++ b/physionet-django/events/templates/events/event_applications.html @@ -0,0 +1,27 @@ +{% load participation_status %} + diff --git a/physionet-django/events/templates/events/event_entries.html b/physionet-django/events/templates/events/event_entries.html index 60d0135bd0..3fc91e2ce1 100644 --- a/physionet-django/events/templates/events/event_entries.html +++ b/physionet-django/events/templates/events/event_entries.html @@ -1,30 +1,35 @@ -
    - - - - - - - - - - - - {% for participant in event.participants.all %} - - - - - - - - {% endfor %} - -
    UsernameFull nameEmailCredentialedCohost
    {{ participant.user.username }}{{ participant.user.get_full_name }}{{ participant.user.email }}{{ participant.user.get_credentialing_status }} - {% if participant.is_cohost %} - - {% else %} - - {% endif %} -
    -
    +{% load participation_status %} + diff --git a/physionet-django/events/templates/events/event_home.html b/physionet-django/events/templates/events/event_home.html index bd62b980eb..ec33a693d2 100644 --- a/physionet-django/events/templates/events/event_home.html +++ b/physionet-django/events/templates/events/event_home.html @@ -85,7 +85,10 @@

    {{ event.title }}

    {% if event.host == user %} Share the class code: {{ url_prefix }}{% url 'event_detail' event.slug %}

    - + + + + Edit Event {% endif %} @@ -99,23 +102,24 @@

    {{ event.title }}


    - {% if events_active %} - {% for event in events_active %} - - + {# No decision yet #} + {% else %} +
    Currently
    +
    Waiting for editor decision
    + {% endif %} + + + {% if e.is_resubmission %} +
  • +
    +
    {{ e.submission_datetime|date }}
    +
    +

    The project was resubmitted for review.

    + {% if e.author_comments %} +

    The submitting author included the following comments:

    + {{ e.author_comments|linebreaks }} + {% endif %} +
    +
    +
  • + {% endif %} {% endfor %} diff --git a/physionet-django/project/test_s3.py b/physionet-django/project/test_s3.py new file mode 100644 index 0000000000..572bf68983 --- /dev/null +++ b/physionet-django/project/test_s3.py @@ -0,0 +1,141 @@ +import os +from unittest import mock + +from django.conf import settings +from django.test import TestCase, override_settings +from moto import mock_s3 + +from project.cloud.s3 import ( + check_s3_bucket_exists, + create_s3_client, + create_s3_server_access_log_bucket, + get_bucket_name, + has_s3_credentials, + upload_project_to_S3, +) +from project.models import PublishedProject +from user.test_views import TestMixin + + +@override_settings( + AWS_PROFILE='default', + AWS_ACCOUNT_ID='123456789012', + S3_OPEN_ACCESS_BUCKET='datashare-public', + S3_SERVER_ACCESS_LOG_BUCKET='datashare-logs', +) +class TestS3(TestMixin): + """ + Test cases for S3 project uploads. + """ + maxDiff = None + + def setUp(self): + super().setUp() + + # The following environment variables are used by boto3, and + # should be set to avoid unpredictable behavior when testing. + # They need to be set before calling mock_s3(). This list + # might be incomplete. + self.mock_env = mock.patch.dict(os.environ, { + 'AWS_SHARED_CREDENTIALS_FILE': os.path.join( + settings.DEMO_FILE_ROOT, 'aws_credentials'), + 'AWS_PROFILE': settings.AWS_PROFILE, + 'AWS_ACCESS_KEY_ID': '', + 'AWS_SECRET_ACCESS_KEY': '', + 'AWS_SECURITY_TOKEN': '', + 'AWS_SESSION_TOKEN': '', + 'AWS_DEFAULT_REGION': '', + }) + self.mock_env.start() + self.mock_s3 = mock_s3() + self.mock_s3.start() + + def tearDown(self): + super().tearDown() + self.mock_s3.stop() + self.mock_env.stop() + + def test_s3_credentials(self): + """ + Check that dummy credentials are configured for S3. + """ + self.assertTrue(has_s3_credentials()) + + def test_create_log_bucket(self): + """ + Test creating an S3 bucket for server access logs. + """ + create_s3_server_access_log_bucket() + self.assert_bucket_is_not_public(settings.S3_SERVER_ACCESS_LOG_BUCKET) + + def test_upload_open_projects(self): + """ + Test uploading open-access projects to S3. + """ + create_s3_server_access_log_bucket() + + project1 = PublishedProject.objects.get(slug='demobsn', + version='1.0') + self.assertGreater(project1.compressed_storage_size, 0) + + project2 = PublishedProject.objects.get(slug='demowave', + version='1.0.0') + self.assertEqual(project2.compressed_storage_size, 0) + + self.assertFalse(check_s3_bucket_exists(project1)) + self.assertFalse(check_s3_bucket_exists(project2)) + + upload_project_to_S3(project1) + upload_project_to_S3(project2) + + self.assertTrue(check_s3_bucket_exists(project1)) + self.assertTrue(check_s3_bucket_exists(project2)) + + s3 = create_s3_client() + expected_files = {} + for project in (project1, project2): + bucket = get_bucket_name(project) + self.assert_bucket_is_public(bucket) + + prefix = project.slug + '/' + project.version + '/' + for subdir, _, files in os.walk(project.file_root()): + for name in files: + path = os.path.join(subdir, name) + relpath = os.path.relpath(path, project.file_root()) + expected_files[prefix + relpath] = os.path.getsize(path) + + if project.compressed_storage_size: + zip_path = project.zip_name(full=True) + zip_key = project.slug + '/' + project.zip_name(legacy=False) + expected_files[zip_key] = os.path.getsize(zip_path) + + objects = s3.list_objects_v2(Bucket=bucket) + bucket_files = {} + for object_info in objects['Contents']: + bucket_files[object_info['Key']] = object_info['Size'] + + self.assertEqual(bucket_files, expected_files) + + def assert_bucket_is_public(self, bucket_name): + """ + Check that a bucket exists and allows some form of public access. + """ + s3 = create_s3_client() + pab = s3.get_public_access_block(Bucket=bucket_name) + conf = pab['PublicAccessBlockConfiguration'] + self.assertFalse(conf['BlockPublicAcls']) + self.assertFalse(conf['IgnorePublicAcls']) + self.assertFalse(conf['BlockPublicPolicy']) + self.assertFalse(conf['RestrictPublicBuckets']) + + def assert_bucket_is_not_public(self, bucket_name): + """ + Check that a bucket exists and does not allow public access. + """ + s3 = create_s3_client() + pab = s3.get_public_access_block(Bucket=bucket_name) + conf = pab['PublicAccessBlockConfiguration'] + self.assertTrue(conf['BlockPublicAcls']) + self.assertTrue(conf['IgnorePublicAcls']) + self.assertTrue(conf['BlockPublicPolicy']) + self.assertTrue(conf['RestrictPublicBuckets']) diff --git a/physionet-django/project/test_views.py b/physionet-django/project/test_views.py index 97393f01ca..64b74d90a3 100644 --- a/physionet-django/project/test_views.py +++ b/physionet-django/project/test_views.py @@ -13,7 +13,6 @@ from project.models import ( AccessPolicy, ActiveProject, - ArchivedProject, Author, AuthorInvitation, DataAccessRequest, @@ -22,6 +21,7 @@ PublishedAuthor, PublishedProject, StorageRequest, + SubmissionStatus ) from user.models import User from user.test_views import TestMixin, prevent_request_warnings @@ -520,7 +520,10 @@ def test_content(self): 227, some ambiguous characters & < >,
    invalid tags
    , - invalid attributes + invalid attributes, + full URL to image, + partial URL to image, + cross-domain

    """ @@ -532,7 +535,10 @@ def test_content(self): 227, some ambiguous characters & < >, <form>invalid tags</form>, - invalid attributes + invalid attributes, + full URL to image, + partial URL to image, + cross-domain

    """ @@ -553,6 +559,45 @@ def test_content(self): self.assertFalse(project.is_submittable()) +class TestProjectTransfer(TestCase): + """ + Tests that submitting author status can be transferred to a co-author + """ + AUTHOR_EMAIL = 'rgmark@mit.edu' + COAUTHOR_EMAIL = 'aewj@mit.edu' + PASSWORD = 'Tester11!' + PROJECT_SLUG = 'T108xFtYkRAxiRiuOLEJ' + + def setUp(self): + self.client.login(username=self.AUTHOR_EMAIL, password=self.PASSWORD) + self.project = ActiveProject.objects.get(slug=self.PROJECT_SLUG) + self.submitting_author = self.project.authors.filter(is_submitting=True).first() + self.coauthor = self.project.authors.filter(is_submitting=False).first() + + def test_transfer_author(self): + """ + Test that an activate project can be transferred to a co-author. + """ + self.assertEqual(self.submitting_author.user.email, self.AUTHOR_EMAIL) + self.assertEqual(self.coauthor.user.email, self.COAUTHOR_EMAIL) + + response = self.client.post( + reverse('project_authors', args=(self.project.slug,)), + data={ + 'transfer_author': self.coauthor.user.id, + }) + + # Check if redirect happens, implying successful transfer + self.assertEqual(response.status_code, 302) + + # Fetch the updated project data + updated_project = ActiveProject.objects.get(slug=self.PROJECT_SLUG) + + # Verify that the author has been transferred + self.assertFalse(updated_project.authors.get(user=self.submitting_author.user).is_submitting) + self.assertTrue(updated_project.authors.get(user=self.coauthor.user).is_submitting) + + class TestAccessPublished(TestMixin): """ Test that certain views or content in their various states can only @@ -859,15 +904,20 @@ def test_archive(self): """ self.client.login(username='rgmark@mit.edu', password='Tester11!') project = ActiveProject.objects.get(title='MIT-BIH Arrhythmia Database') + self.assertTrue(ActiveProject.objects.filter(title='MIT-BIH Arrhythmia Database', + submission_status=SubmissionStatus.UNSUBMITTED)) author_id = project.authors.all().first().id abstract = project.abstract + # 'Delete' (archive) the project response = self.client.post(reverse('project_overview', args=(project.slug,)), data={'delete_project':''}) - # The ActiveProject model should be replaced, and all its - # related objects should point to the new ArchivedProject - self.assertFalse(ActiveProject.objects.filter(title='MIT-BIH Arrhythmia Database')) - project = ArchivedProject.objects.get(title='MIT-BIH Arrhythmia Database') + + # The ActiveProject model should be set to "Archived" status + self.assertFalse(ActiveProject.objects.filter(title='MIT-BIH Arrhythmia Database', + submission_status=SubmissionStatus.UNSUBMITTED)) + project = ActiveProject.objects.get(title='MIT-BIH Arrhythmia Database', + submission_status=SubmissionStatus.ARCHIVED) self.assertTrue(Author.objects.get(id=author_id).project == project) self.assertEqual(project.abstract, abstract) diff --git a/physionet-django/project/utility.py b/physionet-django/project/utility.py index 64f623941d..587ffedfdc 100644 --- a/physionet-django/project/utility.py +++ b/physionet-django/project/utility.py @@ -551,6 +551,8 @@ class LinkFilter: >>> f = LinkFilter(my_hostnames=['example.com']) >>> f.convert('') '' + >>> f.convert('') + '' >>> f.convert('') '' @@ -667,7 +669,7 @@ def convert_url(self, attr_name, url): if scheme in ('http', 'https') and self.my_netloc_re.fullmatch(netloc): url = urllib.parse.urlunparse(('', '', path, params, query, fragment)) - elif attr_name == 'src': + elif (scheme or netloc) and attr_name == 'src': # Discard cross-domain subresources return None diff --git a/physionet-django/project/validators.py b/physionet-django/project/validators.py index 6348c9d292..1aaf74bf7e 100644 --- a/physionet-django/project/validators.py +++ b/physionet-django/project/validators.py @@ -2,6 +2,7 @@ from django.core.exceptions import ValidationError from django.utils.translation import gettext as _ +from packaging.version import Version MAX_FILENAME_LENGTH = 100 MAX_PROJECT_SLUG_LENGTH = 30 @@ -95,6 +96,16 @@ def validate_version(value): 'For example, write "1.0.1" instead of "1.00.01".') +def is_version_greater(version1, version2): + """ + Return True if version1 is greater than version2, False otherwise. + """ + try: + return Version(version1) > Version(version2) + except ValueError: + return ValueError('Invalid version number.') + + def validate_slug(value): """ Validate a published slug. Not ending with dash number for google diff --git a/physionet-django/project/views.py b/physionet-django/project/views.py index a680ab43f5..f66e803e7a 100644 --- a/physionet-django/project/views.py +++ b/physionet-django/project/views.py @@ -33,7 +33,6 @@ ActiveProject, Affiliation, AnonymousAccess, - ArchivedProject, Author, AuthorInvitation, DataAccess, @@ -56,7 +55,10 @@ from project.validators import validate_filename, validate_gcs_bucket_object from user.forms import AssociatedEmailChoiceForm from user.models import CloudInformation, CredentialApplication, LegacyCredential, User, Training - +from project.cloud.s3 import ( + has_s3_credentials, + files_sent_to_S3, +) from django.db.models import F, DateTimeField, ExpressionWrapper LOGGER = logging.getLogger(__name__) @@ -117,6 +119,13 @@ def view_wrapper(request, *args, **kwargs): else: allow = False + # Authors cannot view archived projects + if ( + project.submission_status == SubmissionStatus.ARCHIVED + and not user.has_perm("project.change_activeproject") + ): + allow = False + # Post authentication if request.method == 'POST': if post_auth_mode == 1: @@ -218,19 +227,15 @@ def project_home(request): InvitationResponseFormSet = modelformset_factory(AuthorInvitation, form=forms.InvitationResponseForm, extra=0) - active_authors = Author.objects.filter(user=user, - content_type=ContentType.objects.get_for_model(ActiveProject)) - archived_authors = Author.objects.filter(user=user, - content_type=ContentType.objects.get_for_model(ArchivedProject)) - published_authors = PublishedAuthor.objects.filter(user=user, - project__is_latest_version=True) request_reviewers = DataAccessRequestReviewer.objects.filter(reviewer=user, is_revoked=False, project__is_latest_version=True) + published_projects = PublishedProject.objects.filter(authors__user=user, + is_latest_version=True).order_by("-publish_datetime") + # Get the various projects. - projects = [a.project for a in active_authors] - published_projects = [a.project for a in published_authors] + [ a.project for a in request_reviewers] + published_projects = [a for a in published_projects] + [a.project for a in request_reviewers] for p in published_projects: p.new_button = p.can_publish_new(user) p.requests_button = p.can_approve_requests(user) @@ -248,7 +253,11 @@ def project_home(request): pending_author_approvals = [] missing_affiliations = [] pending_revisions = [] - for p in projects: + + active_projects = ActiveProject.objects.filter(authors__user=user).exclude( + submission_status=SubmissionStatus.ARCHIVED).order_by("-creation_datetime") + + for p in active_projects: if (p.submission_status == SubmissionStatus.NEEDS_APPROVAL and not p.all_authors_approved()): if p.authors.get(user=user).is_submitting: @@ -260,7 +269,9 @@ def project_home(request): pending_revisions.append(p) if p.submission_status == SubmissionStatus.UNSUBMITTED and p.authors.get(user=user).affiliations.count() == 0: missing_affiliations.append([p, p.authors.get(user=user).creation_date]) - archived_projects = [a.project for a in archived_authors] + + archived_projects = ActiveProject.objects.filter( + authors__user=user, submission_status=SubmissionStatus.ARCHIVED).order_by("-creation_datetime") invitation_response_formset = InvitationResponseFormSet( queryset=AuthorInvitation.get_user_invitations(user)) @@ -276,7 +287,7 @@ def project_home(request): request, 'project/project_home.html', { - 'projects': projects, + 'projects': active_projects, 'published_projects': published_projects, 'archived_projects': archived_projects, 'missing_affiliations': missing_affiliations, @@ -295,11 +306,22 @@ def create_project(request): user = request.user - n_submitting = Author.objects.filter(user=user, is_submitting=True, - content_type=ContentType.objects.get_for_model(ActiveProject)).count() - if n_submitting >= ActiveProject.MAX_SUBMITTING_PROJECTS: - return render(request, 'project/project_limit_reached.html', - {'max_projects':ActiveProject.MAX_SUBMITTING_PROJECTS}) + # Filter ActiveProject instances (excluding ARCHIVED) + # Get Author objects related to these ActiveProjects + active_projects = ActiveProject.objects.exclude(submission_status=SubmissionStatus.ARCHIVED) + n_submitting = Author.objects.filter( + user=user, + is_submitting=True, + content_type=ContentType.objects.get_for_model(ActiveProject), + object_id__in=active_projects.values_list('id', flat=True) + ).count() + + if n_submitting >= settings.MAX_SUBMITTABLE_PROJECTS: + return render( + request, + "project/project_limit_reached.html", + {"max_projects": settings.MAX_SUBMITTABLE_PROJECTS}, + ) if request.method == 'POST': form = forms.CreateProjectForm(user=user, data=request.POST) @@ -324,11 +346,22 @@ def new_project_version(request, project_slug): user = request.user - n_submitting = Author.objects.filter(user=user, is_submitting=True, - content_type=ContentType.objects.get_for_model(ActiveProject)).count() - if n_submitting >= ActiveProject.MAX_SUBMITTING_PROJECTS: - return render(request, 'project/project_limit_reached.html', - {'max_projects':ActiveProject.MAX_SUBMITTING_PROJECTS}) + # Filter ActiveProject instances (excluding ARCHIVED) + # Get Author objects related to these ActiveProjects + active_projects = ActiveProject.objects.exclude(submission_status=SubmissionStatus.ARCHIVED) + n_submitting = Author.objects.filter( + user=user, + is_submitting=True, + content_type=ContentType.objects.get_for_model(ActiveProject), + object_id__in=active_projects.values_list('id', flat=True) + ).count() + + if n_submitting >= settings.MAX_SUBMITTABLE_PROJECTS: + return render( + request, + "project/project_limit_reached.html", + {"max_projects": settings.MAX_SUBMITTABLE_PROJECTS}, + ) previous_projects = PublishedProject.objects.filter( slug=project_slug).order_by('-version_order') @@ -367,7 +400,7 @@ def project_overview(request, project_slug, **kwargs): under_submission = project.under_submission() if request.method == 'POST' and 'delete_project' in request.POST and is_submitting and not under_submission: - project.fake_delete() + project.archive(archive_reason=1, clear_files=True) return redirect('delete_project_success') return render(request, 'project/project_overview.html', @@ -536,8 +569,10 @@ def project_authors(request, project_slug, **kwargs): inviter=user) corresponding_author_form = forms.CorrespondingAuthorForm( project=project) + transfer_author_form = forms.TransferAuthorForm( + project=project) else: - invite_author_form, corresponding_author_form = None, None + invite_author_form, corresponding_author_form, transfer_author_form = None, None, None if author.is_corresponding: corresponding_email_form = AssociatedEmailChoiceForm( @@ -591,18 +626,37 @@ def project_authors(request, project_slug, **kwargs): messages.success(request, 'Your corresponding email has been updated.') else: messages.error(request, 'Submission unsuccessful. See form for errors.') + elif 'transfer_author' in request.POST and is_submitting: + transfer_author_form = forms.TransferAuthorForm( + project=project, data=request.POST) + if transfer_author_form.is_valid(): + transfer_author_form.transfer() + notification.notify_submitting_author(request, project) + messages.success(request, 'The submitting author has been updated.') + return redirect('project_authors', project_slug=project.slug) + else: + messages.error(request, 'Submission unsuccessful. See form for errors.') authors = project.get_author_info() invitations = project.authorinvitations.filter(is_active=True) edit_affiliations_url = reverse('edit_affiliation', args=[project.slug]) - return render(request, 'project/project_authors.html', {'project':project, - 'authors':authors, 'invitations':invitations, - 'affiliation_formset':affiliation_formset, - 'invite_author_form':invite_author_form, - 'corresponding_author_form':corresponding_author_form, - 'corresponding_email_form':corresponding_email_form, - 'add_item_url':edit_affiliations_url, 'remove_item_url':edit_affiliations_url, - 'is_submitting':is_submitting}) + return render( + request, + "project/project_authors.html", + { + "project": project, + "authors": authors, + "invitations": invitations, + "affiliation_formset": affiliation_formset, + "invite_author_form": invite_author_form, + "corresponding_author_form": corresponding_author_form, + "corresponding_email_form": corresponding_email_form, + "transfer_author_form": transfer_author_form, + "add_item_url": edit_affiliations_url, + "remove_item_url": edit_affiliations_url, + "is_submitting": is_submitting, + }, + ) def edit_content_item(request, project_slug): @@ -1364,11 +1418,20 @@ def project_submission(request, project_slug, **kwargs): else: edit_logs, copyedit_logs = None, None - return render(request, 'project/project_submission.html', { - 'project':project, 'authors':authors, - 'is_submitting':is_submitting, 'author_comments_form':author_comments_form, - 'edit_logs':edit_logs, 'copyedit_logs':copyedit_logs, - 'awaiting_user_approval':awaiting_user_approval}) + return render( + request, + "project/project_submission.html", + { + "project": project, + "authors": authors, + "is_submitting": is_submitting, + "author_comments_form": author_comments_form, + "edit_logs": edit_logs, + "copyedit_logs": copyedit_logs, + "awaiting_user_approval": awaiting_user_approval, + "contact_email": project.editor_contact_email, + }, + ) @project_auth(auth_mode=0, post_auth_mode=2) @@ -1485,10 +1548,14 @@ def archived_submission_history(request, project_slug): user = request.user try: # Checks if the user is an author - project = ArchivedProject.objects.get(slug=project_slug, authors__user=user) - except ArchivedProject.DoesNotExist: - if user.has_perm('project.change_archivedproject'): - project = get_object_or_404(ArchivedProject, slug=project_slug) + project = ActiveProject.objects.get(slug=project_slug, + submission_status=SubmissionStatus.ARCHIVED, + authors__user=user) + except ActiveProject.DoesNotExist: + if user.has_perm('project.change_activeproject'): + project = get_object_or_404(ActiveProject, + slug=project_slug, + submission_status=SubmissionStatus.ARCHIVED) else: raise Http404() @@ -1796,7 +1863,7 @@ def published_project(request, project_slug, version, subdir=''): topics = project.topics.all() languages = project.programming_languages.all() contact = project.contact - news = project.news.all().order_by('-publish_datetime') + news = project.get_all_news().order_by('-publish_datetime') parent_projects = project.parent_projects.all() # derived_projects = project.derived_publishedprojects.all() data_access = DataAccess.objects.filter(project=project) @@ -1863,6 +1930,7 @@ def published_project(request, project_slug, version, subdir=''): 'platform_citations': platform_citations, 'is_lightwave_supported': project.files.is_lightwave_supported(), 'is_wget_supported': project.files.is_wget_supported(), + 'has_s3_credentials': has_s3_credentials(), 'show_platform_wide_citation': show_platform_wide_citation, 'main_platform_citation': main_platform_citation, } @@ -1919,6 +1987,7 @@ def sign_dua(request, project_slug, version): Page to sign the dua for a protected project. Both restricted and credentialed policies. """ + from console.views import update_aws_bucket_policy user = request.user project = PublishedProject.objects.filter(slug=project_slug, version=version) if project: @@ -1943,6 +2012,8 @@ def sign_dua(request, project_slug, version): if request.method == 'POST' and 'agree' in request.POST: DUASignature.objects.create(user=user, project=project) + if has_s3_credentials() and files_sent_to_S3(project) is not None: + update_aws_bucket_policy(project.id) return render(request, 'project/sign_dua_complete.html', { 'project':project}) diff --git a/physionet-django/search/views.py b/physionet-django/search/views.py index de990710b3..ab5f17126a 100644 --- a/physionet-django/search/views.py +++ b/physionet-django/search/views.py @@ -4,7 +4,6 @@ from functools import reduce from django.conf import settings -from django.contrib.postgres.search import SearchQuery, SearchVector, SearchRank from django.db.models import Case, Count, IntegerField, Q, Sum, Value, When from django.http import Http404 from django.shortcuts import redirect, render, reverse @@ -63,6 +62,11 @@ def get_content(resource_type, orderby, direction, search_term): def get_content_postgres_full_text_search(resource_type, orderby, direction, search_term): + from django.contrib.postgres.search import ( + SearchQuery, + SearchRank, + SearchVector, + ) # Split search term by whitespace or punctuation if search_term: diff --git a/physionet-django/static/images/about/citi-course-instructions-6.png b/physionet-django/static/images/about/citi-course-instructions-6.png new file mode 100644 index 0000000000..6a1361e646 Binary files /dev/null and b/physionet-django/static/images/about/citi-course-instructions-6.png differ diff --git a/physionet-django/static/images/about/citi-course-instructions-7.png b/physionet-django/static/images/about/citi-course-instructions-7.png new file mode 100644 index 0000000000..7c8d221bb4 Binary files /dev/null and b/physionet-django/static/images/about/citi-course-instructions-7.png differ diff --git a/physionet-django/static/sample/create-course-schema.json b/physionet-django/static/sample/create-course-schema.json new file mode 100644 index 0000000000..30f9b822fe --- /dev/null +++ b/physionet-django/static/sample/create-course-schema.json @@ -0,0 +1,28 @@ +{ + "title": "string", + "description": "string", + "valid_duration": "string", + "version": "string", + "modules": [ + { + "contents": [ + { + "body": "string", + "order": "integer" + } + ], + "quizzes": [ + { + "question": "string", + "order": "integer", + "choices": [ + { + "body": "string", + "is_correct": "boolean" + } + ] + } + ] + } + ] +} diff --git a/physionet-django/static/sample/example-course-create.json b/physionet-django/static/sample/example-course-create.json new file mode 100644 index 0000000000..1661b7a70c --- /dev/null +++ b/physionet-django/static/sample/example-course-create.json @@ -0,0 +1,43 @@ +{ + "title": "Course 1", + "description": "

    Test content description", + "valid_duration": "1095 00:00:00", + "version": "1.0.0", + "modules": [ + { + "name": "Module 1", + "description": "

    Module description", + "order": 0, + "contents": [ + { + "body": "

    Hello This is a test

    Test content1

    ", + "order": 0 + } + ], + "quizzes": [ + { + "question": "What is the correct answer(choice1)?", + "order": 1, + "choices": [ + { + "body": "I am a choice1", + "is_correct": true + }, + { + "body": "I am a choice2", + "is_correct": false + }, + { + "body": "I am a choice3", + "is_correct": false + }, + { + "body": "I am a choice4", + "is_correct": false + } + ] + } + ] + } + ] +} diff --git a/physionet-django/static/sample/example-course-update.json b/physionet-django/static/sample/example-course-update.json new file mode 100644 index 0000000000..bb8eef5b17 --- /dev/null +++ b/physionet-django/static/sample/example-course-update.json @@ -0,0 +1,43 @@ +{ + "title": "Course 1 Updated", + "description": "

    Test content description Updated", + "valid_duration": "1095 00:00:00", + "version": "1.5.1", + "modules": [ + { + "name": "Module 1 Updated", + "description": "

    Test content description Updated", + "order": 0, + "contents": [ + { + "body": "

    Hello This is a test

    Test content1 updated

    ", + "order": 0 + } + ], + "quizzes": [ + { + "question": "What is the correct answer(choice2)?", + "order": 1, + "choices": [ + { + "body": "I am a choice1", + "is_correct": false + }, + { + "body": "I am a choice2", + "is_correct": true + }, + { + "body": "I am a choice3", + "is_correct": false + }, + { + "body": "I am a choice4", + "is_correct": false + } + ] + } + ] + } + ] +} diff --git a/physionet-django/templates/about/citi_course.html b/physionet-django/templates/about/citi_course.html index c4c35c326c..c59e316638 100644 --- a/physionet-django/templates/about/citi_course.html +++ b/physionet-django/templates/about/citi_course.html @@ -20,6 +20,10 @@

    CITI Course Instructions

    Citi Course instructions 4
  • Once submitted, you should see the 'Data or Specimens Only Research' and 'Conflicts of Interest' modules. Please complete these modules.
  • Citi Course instructions 5 +
  • Please make sure when submitting your CITI training application, that you upload the training report rather than the certificate. You can find your training report under 'Records' at the top of the webpage. From there, you can download your training report by selecting 'View-Print-Share' under Completion Record.
  • + Citi Course instructions 6 +
  • Click 'view/print' under Completion Record to get the full training report to upload on PhysioNet.
  • + Citi Course instructions 7 {% endblock %} diff --git a/physionet-django/templates/home.html b/physionet-django/templates/home.html index bc8215ffda..98d4eab2af 100644 --- a/physionet-django/templates/home.html +++ b/physionet-django/templates/home.html @@ -23,7 +23,7 @@ @@ -70,7 +70,7 @@


    {% for news in news_pieces %}

    - {{ news.title }} + {{ news.title }}

    {% include "notification/news_content.html" %}
    diff --git a/physionet-django/training/__init__.py b/physionet-django/training/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/physionet-django/training/admin.py b/physionet-django/training/admin.py new file mode 100644 index 0000000000..0d2876f456 --- /dev/null +++ b/physionet-django/training/admin.py @@ -0,0 +1,42 @@ +from django.contrib import admin +from training import models + + +class ContentBlockInline(admin.StackedInline): + model = models.ContentBlock + extra = 1 + + +class QuizChoiceInline(admin.TabularInline): + model = models.QuizChoice + extra = 1 + + +class QuizInline(admin.StackedInline): + model = models.Quiz + inlines = [QuizChoiceInline, ] + extra = 1 + + +class ModuleInline(admin.StackedInline): + model = models.Module + inlines = [ContentBlockInline, QuizInline] + extra = 1 + + +@admin.register(models.Quiz) +class QuizAdmin(admin.ModelAdmin): + list_display = ('module', 'question', 'order') + inlines = [QuizChoiceInline, ] + + +@admin.register(models.Module) +class ModuleAdmin(admin.ModelAdmin): + list_display = ('name', 'course', 'order') + inlines = [ContentBlockInline, QuizInline] + + +@admin.register(models.Course) +class CourseAdmin(admin.ModelAdmin): + list_display = ('training_type', 'version') + inlines = [ModuleInline] diff --git a/physionet-django/training/apps.py b/physionet-django/training/apps.py new file mode 100644 index 0000000000..a94e5537a6 --- /dev/null +++ b/physionet-django/training/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TrainingConfig(AppConfig): + name = 'training' diff --git a/physionet-django/training/fixtures/demo-training.json b/physionet-django/training/fixtures/demo-training.json new file mode 100644 index 0000000000..85233b4e10 --- /dev/null +++ b/physionet-django/training/fixtures/demo-training.json @@ -0,0 +1,435 @@ +[ + { + "model": "training.course", + "pk": 1, + "fields": { + "training_type": 2, + "version": 1.0 + } + }, + { + "model": "training.module", + "pk": 1, + "fields": { + "name": "The Americas", + "description": "Learn about the Americas, including North and South America, Asia and Africa.", + "course": 1, + "order": 1 + } + }, + { + "model": "training.contentblock", + "pk": 1, + "fields": { + "module": 1, + "body": "

    The Americas are comprised of two continents: North America and South America. These continents are connected by a narrow strip of land called the Isthmus of Panama. Together, North and South America span from the Arctic Circle in the north to Cape Horn in the south.

    \n\n

    North America is home to three large countries: Canada, the United States, and Mexico. It also includes several smaller countries in Central America and the Caribbean. The continent is known for its diverse landscapes, ranging from frozen tundras in the north to tropical rainforests in the south.

    \n\n

    South America is made up of twelve countries, including Brazil, Argentina, and Colombia. It is known for its stunning natural beauty, including the Amazon rainforest and the Andes mountain range. The continent also has a rich cultural heritage, with vibrant cities and ancient ruins.

    ", + "order": 1 + } + }, + { + "model": "training.contentblock", + "pk": 2, + "fields": { + "module": 1, + "body": "

    (Copied content)The Americas are comprised of two continents: North America and South America. These continents are connected by a narrow strip of land called the Isthmus of Panama. Together, North and South America span from the Arctic Circle in the north to Cape Horn in the south.

    \n\n

    North America is home to three large countries: Canada, the United States, and Mexico. It also includes several smaller countries in Central America and the Caribbean. The continent is known for its diverse landscapes, ranging from frozen tundras in the north to tropical rainforests in the south.

    \n\n

    South America is made up of twelve countries, including Brazil, Argentina, and Colombia. It is known for its stunning natural beauty, including the Amazon rainforest and the Andes mountain range. The continent also has a rich cultural heritage, with vibrant cities and ancient ruins.

    ", + "order": 2 + } + }, + { + "model": "training.quiz", + "pk": 1, + "fields": { + "module": 1, + "question": "Which two continents make up the Americas?(Answer : North America and South America)", + "order": 3 + } + }, + { + "model": "training.quizchoice", + "pk": 1, + "fields": { + "quiz": 1, + "body": "Europe and Asia", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 2, + "fields": { + "quiz": 1, + "body": "Africa and Australia", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 3, + "fields": { + "quiz": 1, + "body": "North America and South America", + "is_correct": true + } + }, + { + "model": "training.quizchoice", + "pk": 4, + "fields": { + "quiz": 1, + "body": "Antarctica and South America", + "is_correct": false + } + }, + { + "model": "training.quiz", + "pk": 2, + "fields": { + "module": 1, + "question": "What connects North America and South America?(Answer : The Isthmus of Panama)", + "order": 4 + } + }, + { + "model": "training.quizchoice", + "pk": 5, + "fields": { + "quiz": 2, + "body": "The Isthmus of Panama", + "is_correct": true + } + }, + { + "model": "training.quizchoice", + "pk": 6, + "fields": { + "quiz": 2, + "body": "The Strait of Gibraltar", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 7, + "fields": { + "quiz": 2, + "body": "The Suez Canal", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 8, + "fields": { + "quiz": 2, + "body": "The Bering Strait", + "is_correct": false + } + }, + { + "model": "training.quiz", + "pk": 3, + "fields": { + "module": 1, + "question": "Which is NOT a large country in North America?(Answer : Cuba)", + "order": 5 + } + }, + { + "model": "training.quizchoice", + "pk": 9, + "fields": { + "quiz": 3, + "body": "Canada", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 10, + "fields": { + "quiz": 3, + "body": "United States", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 11, + "fields": { + "quiz": 3, + "body": "Mexico", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 12, + "fields": { + "quiz": 3, + "body": "Cuba", + "is_correct": true + } + }, + { + "model": "training.contentblock", + "pk": 3, + "fields": { + "module": 1, + "body": "

    The Americas, also known as America, are lands of the Western Hemisphere composed of numerous entities and regions variably defined by geography, politics, and culture1. The Americas are recognized in the English-speaking world to include two separate continents: North America and South America1.

    \n\n

    The Americas have more than 1.014 billion inhabitants and boast an area of over 16.43 million square miles2. The Americas comprise 35 countries, including some of the world’s largest countries as well as several dependent territories2.

    \n", + "order": 6 + } + }, + { + "model": "training.quiz", + "pk": 4, + "fields": { + "module": 1, + "question": "Which of the following is NOT a country in the Americas? (Answer : India)", + "order": 7 + } + }, + { + "model": "training.quizchoice", + "pk": 13, + "fields": { + "quiz": 4, + "body": "Canada", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 14, + "fields": { + "quiz": 4, + "body": "Mexico", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 15, + "fields": { + "quiz": 4, + "body": "Brazil", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 16, + "fields": { + "quiz": 4, + "body": "India", + "is_correct": true + } + }, + { + "model": "training.contentblock", + "pk": 4, + "fields": { + "module": 1, + "body": "

    The Americas are a group of countries in the Western Hemisphere. They are also known as America.

    \n\n

    There are two continents in the Americas: North America and South America. They are connected by the Isthmus of Panama.

    ", + "order": 8 + } + }, + { + "model": "training.contentblock", + "pk": 5, + "fields": { + "module": 1, + "body": "

    North America is home to three large countries: Canada, the United States, and Mexico. It also includes several smaller countries in Central America and the Caribbean. The continent is known for its diverse landscapes, ranging from frozen tundras in the north to tropical rainforests in the south.

    ", + "order": 9 + } + }, + { + "model": "training.contentblock", + "pk": 6, + "fields": { + "module": 1, + "body": "

    South America is made up of twelve countries, including Brazil, Argentina, and Colombia. It is known for its stunning natural beauty, including the Amazon rainforest and the Andes mountain range. The continent also has a rich cultural heritage, with vibrant cities and ancient ruins.

    ", + "order": 10 + } + }, + { + "model": "training.contentblock", + "pk": 7, + "fields": { + "module": 1, + "body": "

    As our course on World 101: Introduction to Continents and Countries comes to an end, I would like to take this opportunity to thank each and every one of you for your participation and engagement throughout the course.

    \n\n

    It has been a pleasure to share this journey with you and I hope that the knowledge and insights gained during our time together will serve you well in your future endeavors.

    \n\n

    Thank you for making this course a success. I wish you all the best in your future studies and pursuits.

    \n", + "order": 11 + } + }, + { + "model": "training.module", + "pk": 2, + "fields": { + "course": 1, + "name": "Wondering about Europe?", + "description": "Europe is a continent located in the Northern Hemisphere. It is bordered by the Arctic Ocean to the north, the Atlantic Ocean to the west, and the Mediterranean Sea to the south. It is also connected to Asia by the Ural Mountains and the Caspian Sea.", + "order": 2 + } + }, + { + "model": "training.contentblock", + "pk": 8, + "fields": { + "module": 2, + "body": "

    Europe is a continent located in the Northern Hemisphere. It is bordered by the Arctic Ocean to the north, the Atlantic Ocean to the west, and the Mediterranean Sea to the south. It is also connected to Asia by the Ural Mountains and the Caspian Sea.

    \n\n

    Europe is home to a diverse range of cultures, landscapes, and languages. It is also the second-smallest continent in the world, with a total area of 3.93 million square miles (10.2 million square kilometers).

    \n", + "order": 1 + } + }, + { + "model": "training.contentblock", + "pk": 9, + "fields": { + "module": 2, + "body": "

    Europe is made up of 50 countries. The largest by area is Russia, while the smallest is Monaco. The continent is also home to many important cities, including London, Paris, and Istanbul.

    \n", + "order": 2 + } + }, + { + "model": "training.contentblock", + "pk": 10, + "fields": { + "module": 2, + "body": "

    Europe is the second most populous continent in the world, with a population of 741 million. It is also home to many of the world’s most well-known historical sites, including Stonehenge, the Parthenon, and the Colosseum.

    \n", + "order": 3 + } + }, + { + "model": "training.contentblock", + "pk": 11, + "fields": { + "module": 2, + "body": "

    Europe is also known for its diverse landscapes. It is home to the highest mountain in the world, Mount Everest, as well as the lowest point on land, the Dead Sea. It is also home to the world’s largest ice sheet, the Greenland ice sheet.

    \n", + "order": 4 + } + }, + { + "model": "training.contentblock", + "pk": 12, + "fields": { + "module": 2, + "body": "

    The continent is also home to some of the world’s largest lakes, including Lake Superior and Lake Baikal. It is also home to the world’s largest river, the Amazon.

    \n", + "order": 5 + } + }, + { + "model": "training.contentblock", + "pk": 13, + "fields": { + "module": 2, + "body": "

    Europe is also home to many of the world’s most well-known cities. London, Paris, and Rome are some of the most famous cities in the world. They are also home to many of the world’s most famous landmarks, including the Eiffel Tower, the Colosseum, and the Parthenon.

    \n", + "order": 6 + } + }, + { + "model": "training.contentblock", + "pk": 14, + "fields": { + "module": 2, + "body": "

    Europe is also home to many of the world’s most famous museums. The Louvre in Paris is one of the most famous museums in the world. It is also home to many of the world’s most famous works of art, including the Mona Lisa and the Venus de Milo.

    \n", + "order": 7 + } + }, + { + "model": "training.contentblock", + "pk": 15, + "fields": { + "module": 2, + "body": "

    Europe is also home to many of the world’s most famous landmarks. The Eiffel Tower in Paris is one of the most famous landmarks in the world. It is also home to many of the world’s most famous works of art, including the Mona Lisa and the Venus de Milo.

    \n", + "order": 8 + } + }, + { + "model": "training.quiz", + "pk": 5, + "fields": { + "module": 2, + "question": "Which of the following countries is NOT located in Europe? (Answer: Nepal)", + "order": 9 + } + }, + { + "model": "training.quizchoice", + "pk": 17, + "fields": { + "quiz": 5, + "body": "France", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 18, + "fields": { + "quiz": 5, + "body": "Nepal", + "is_correct": true + } + }, + { + "model": "training.quizchoice", + "pk": 19, + "fields": { + "quiz": 5, + "body": "Spain", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 20, + "fields": { + "quiz": 5, + "body": "Germany", + "is_correct": false + } + }, + { + "model": "training.quiz", + "pk": 6, + "fields": { + "module": 2, + "question": "Which of the following countries is NOT located in Africa?(Answer: Nepal)", + "order": 10 + } + }, + { + "model": "training.quizchoice", + "pk": 21, + "fields": { + "quiz": 6, + "body": "Egypt", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 22, + "fields": { + "quiz": 6, + "body": "Nepal", + "is_correct": true + } + }, + { + "model": "training.quizchoice", + "pk": 23, + "fields": { + "quiz": 6, + "body": "Zimbabwe", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 24, + "fields": { + "quiz": 6, + "body": "Cameroon", + "is_correct": false + } + } +] diff --git a/physionet-django/training/migrations/0001_initial.py b/physionet-django/training/migrations/0001_initial.py new file mode 100644 index 0000000000..a5e502ead4 --- /dev/null +++ b/physionet-django/training/migrations/0001_initial.py @@ -0,0 +1,128 @@ +# Generated by Django 4.1.7 on 2023-04-11 20:40 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import project.modelcomponents.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('user', '0056_alter_trainingtype_required_field'), + ] + + operations = [ + migrations.CreateModel( + name='Course', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.FloatField(default=1.0)), + ('training_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='courses', to='user.trainingtype')), + ], + options={ + 'permissions': [('can_view_course_guidelines', 'Can view course guidelines')], + 'default_permissions': ('change',), + 'unique_together': {('training_type', 'version')}, + }, + ), + migrations.CreateModel( + name='CourseProgress', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('IP', 'In Progress'), ('C', 'Completed')], default='IP', + max_length=2)), + ('started_at', models.DateTimeField(auto_now_add=True)), + ('completed_at', models.DateTimeField(auto_now=True)), + ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='training.course')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('user', 'course')}, + }, + ), + migrations.CreateModel( + name='Module', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('order', models.PositiveIntegerField()), + ('description', project.modelcomponents.fields.SafeHTMLField()), + ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='modules', + to='training.course')), + ], + options={ + 'unique_together': {('course', 'order')}, + }, + ), + migrations.CreateModel( + name='Quiz', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('question', project.modelcomponents.fields.SafeHTMLField()), + ('order', models.PositiveIntegerField()), + ('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quizzes', + to='training.module')), + ], + ), + migrations.CreateModel( + name='QuizChoice', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('body', models.TextField()), + ('is_correct', models.BooleanField(default=False, verbose_name='Correct Choice?')), + ('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', + to='training.quiz')), + ], + ), + migrations.CreateModel( + name='ModuleProgress', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('IP', 'In Progress'), ('C', 'Completed')], default='IP', + max_length=2)), + ('last_completed_order', models.PositiveIntegerField(default=0, null=True)), + ('started_at', models.DateTimeField(blank=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('course_progress', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='module_progresses', to='training.courseprogress')), + ('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='training.module')), + ], + ), + migrations.CreateModel( + name='ContentBlock', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('body', project.modelcomponents.fields.SafeHTMLField()), + ('order', models.PositiveIntegerField()), + ('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contents', + to='training.module')), + ], + ), + migrations.CreateModel( + name='CompletedQuiz', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('completed_at', models.DateTimeField(blank=True, null=True)), + ('module_progress', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='completed_quizzes', to='training.moduleprogress')), + ('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='training.quiz')), + ], + ), + migrations.CreateModel( + name='CompletedContent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('completed_at', models.DateTimeField(blank=True, null=True)), + ('content', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + to='training.contentblock')), + ('module_progress', + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='completed_contents', to='training.moduleprogress')), + ], + ), + ] diff --git a/physionet-django/training/migrations/0002_alter_course_unique_together_and_more.py b/physionet-django/training/migrations/0002_alter_course_unique_together_and_more.py new file mode 100644 index 0000000000..dabf8e8076 --- /dev/null +++ b/physionet-django/training/migrations/0002_alter_course_unique_together_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 4.1.10 on 2023-07-31 19:27 + +from django.db import migrations, models +import project.validators + + +class Migration(migrations.Migration): + dependencies = [ + ("training", "0001_initial"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="course", + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name="courseprogress", + unique_together=set(), + ), + migrations.AlterField( + model_name="course", + name="version", + field=models.CharField( + blank=True, + default="", + max_length=15, + validators=[project.validators.validate_version], + ), + ), + migrations.AlterField( + model_name="courseprogress", + name="completed_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddConstraint( + model_name="course", + constraint=models.UniqueConstraint( + fields=("training_type", "version"), name="unique_course" + ), + ), + migrations.AddConstraint( + model_name="courseprogress", + constraint=models.UniqueConstraint( + fields=("user", "course"), name="unique_course_progress" + ), + ), + ] diff --git a/physionet-django/training/migrations/0003_course_is_active.py b/physionet-django/training/migrations/0003_course_is_active.py new file mode 100644 index 0000000000..2cd2d27f67 --- /dev/null +++ b/physionet-django/training/migrations/0003_course_is_active.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.13 on 2023-11-26 22:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('training', '0002_alter_course_unique_together_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='course', + name='is_active', + field=models.BooleanField(default=True), + ), + ] diff --git a/physionet-django/training/migrations/0004_course_description_course_title_and_more.py b/physionet-django/training/migrations/0004_course_description_course_title_and_more.py new file mode 100644 index 0000000000..a397b7c7f4 --- /dev/null +++ b/physionet-django/training/migrations/0004_course_description_course_title_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.1.13 on 2024-02-17 04:19 + +from django.db import migrations, models +import project.modelcomponents.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('training', '0003_course_is_active'), + ] + + operations = [ + migrations.AddField( + model_name='course', + name='description', + field=project.modelcomponents.fields.SafeHTMLField(blank=True, null=True), + ), + migrations.AddField( + model_name='course', + name='title', + field=models.CharField(blank=True, max_length=128, null=True), + ), + migrations.AddField( + model_name='course', + name='valid_duration', + field=models.DurationField(null=True), + ), + ] diff --git a/physionet-django/training/migrations/0005_course_trainings.py b/physionet-django/training/migrations/0005_course_trainings.py new file mode 100644 index 0000000000..3083193132 --- /dev/null +++ b/physionet-django/training/migrations/0005_course_trainings.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.13 on 2024-03-07 01:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0060_backfill_trainingtype_slugs'), + ('training', '0004_course_description_course_title_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='course', + name='trainings', + field=models.ManyToManyField(to='user.training'), + ), + ] diff --git a/physionet-django/training/migrations/__init__.py b/physionet-django/training/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/physionet-django/training/models.py b/physionet-django/training/models.py new file mode 100644 index 0000000000..cc75b26847 --- /dev/null +++ b/physionet-django/training/models.py @@ -0,0 +1,275 @@ +from token import NUMBER +from django.db import models +from django.utils import timezone + +from project.modelcomponents.fields import SafeHTMLField +from project.validators import validate_version + + +class Course(models.Model): + """ + A model representing a course for a specific training type. + + Attributes: + training_type (ForeignKey): The training type associated with the course. + version (CharField): The version of the course. + """ + + title = models.CharField(max_length=128, null=True, blank=True) + description = SafeHTMLField(null=True, blank=True) + valid_duration = models.DurationField(null=True) + training_type = models.ForeignKey( + "user.TrainingType", on_delete=models.CASCADE, related_name="courses" + ) + trainings = models.ManyToManyField("user.Training") + version = models.CharField( + max_length=15, default="", blank=True, validators=[validate_version] + ) + is_active = models.BooleanField(default=True) + + class Meta: + default_permissions = ("change",) + constraints = [ + models.UniqueConstraint( + fields=["training_type", "version"], name="unique_course" + ) + ] + permissions = [ + ("can_view_course_guidelines", "Can view course guidelines"), + ] + + def expire_course_version(self, instance, number_of_days): + """ + This method expires the course by setting the is_active field to False and expires all + the trainings associated with it. + """ + self.is_active = False + # reset the valid_duration to the number of days + self.valid_duration = timezone.timedelta(days=number_of_days) + self.save() + + def __str__(self): + return f"{self.training_type} v{self.version}" + + +class Module(models.Model): + """ + A module is a unit of teaching within a course, typically covering a single topic or area of knowledge. + + Attributes: + name (str): The name of the module. + course (Course): The course to which the module belongs. + order (int): The order in which the module appears within the course. + description (SafeHTML): The description of the module, in SafeHTML format. + """ + + name = models.CharField(max_length=100) + course = models.ForeignKey( + "training.Course", on_delete=models.CASCADE, related_name="modules" + ) + order = models.PositiveIntegerField() + description = SafeHTMLField() + + class Meta: + unique_together = ("course", "order") + + def __str__(self): + return self.name + + +class Quiz(models.Model): + """ + A model representing a quiz within a training module. + + Each quiz has a question, belongs to a specific module, and has a designated order within that module. + """ + + question = SafeHTMLField() + module = models.ForeignKey( + "training.Module", on_delete=models.CASCADE, related_name="quizzes" + ) + order = models.PositiveIntegerField() + + +class ContentBlock(models.Model): + """ + A model representing a block of content within a training module. + + Attributes: + module (ForeignKey): The module to which this content block belongs. + body (SafeHTMLField): The HTML content of the block. + order (PositiveIntegerField): The order in which this block should be displayed within the module. + """ + + module = models.ForeignKey( + "training.Module", on_delete=models.CASCADE, related_name="contents" + ) + body = SafeHTMLField() + order = models.PositiveIntegerField() + + +class QuizChoice(models.Model): + """ + A quiz choice is a collection of choices, which is a collection of several types of + content. A quiz choice is associated with a quiz, and an order number. + The order number is used to track the order of the quiz choices in a quiz. + """ + + quiz = models.ForeignKey( + "training.Quiz", on_delete=models.CASCADE, related_name="choices" + ) + body = models.TextField() + is_correct = models.BooleanField("Correct Choice?", default=False) + + +class CourseProgress(models.Model): + """ + Model representing the progress of a user in a course. + + Fields: + - user: ForeignKey to User model + - course: ForeignKey to Course model + - status: CharField with choices of "In Progress" and "Completed" + - started_at: DateTimeField that is automatically added on creation + - completed_at: DateTimeField that is nullable and blankable + + Methods: + - __str__: Returns a string representation of the CourseProgress object + - get_next_module: Returns the next module that the user should be working on + """ + + class Status(models.TextChoices): + IN_PROGRESS = "IP", "In Progress" + COMPLETED = "C", "Completed" + + user = models.ForeignKey("user.User", on_delete=models.CASCADE) + course = models.ForeignKey("training.Course", on_delete=models.CASCADE) + status = models.CharField( + max_length=2, choices=Status.choices, default=Status.IN_PROGRESS + ) + started_at = models.DateTimeField(auto_now_add=True) + completed_at = models.DateTimeField(null=True, blank=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["user", "course"], name="unique_course_progress" + ) + ] + + def __str__(self): + return f"{self.user.username} - {self.course}" + + def get_next_module(self): + if self.status == self.Status.COMPLETED: + return None + + next_module = self.module_progresses.filter( + status=self.module_progresses.model.Status.IN_PROGRESS + ).first() + if next_module: + return next_module.module + + last_module = ( + self.module_progresses.filter( + status=self.module_progresses.model.Status.COMPLETED + ) + .order_by("-last_completed_order") + .first() + ) + if last_module: + return ( + self.course.modules.filter(order__gt=last_module.module.order) + .order_by("order") + .first() + ) + + return self.course.modules.first() + + +class ModuleProgress(models.Model): + """ + Model representing the progress of a user in a module. + + Fields: + - course_progress: ForeignKey to CourseProgress model + - module: ForeignKey to Module model + - status: CharField with choices of "In Progress" and "Completed" + - last_completed_order: PositiveIntegerField with default value of 0 + - started_at: DateTimeField that is nullable and blankable + - updated_at: DateTimeField that is automatically updated on save + + Methods: + - __str__: Returns a string representation of the ModuleProgress object + + """ + + class Status(models.TextChoices): + IN_PROGRESS = "IP", "In Progress" + COMPLETED = "C", "Completed" + + course_progress = models.ForeignKey( + "training.CourseProgress", + on_delete=models.CASCADE, + related_name="module_progresses", + ) + module = models.ForeignKey("training.Module", on_delete=models.CASCADE) + status = models.CharField( + max_length=2, choices=Status.choices, default=Status.IN_PROGRESS + ) + last_completed_order = models.PositiveIntegerField(null=True, default=0) + started_at = models.DateTimeField(null=True, blank=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"{self.course_progress.user.username} - {self.module}" + + +class CompletedContent(models.Model): + """ + Model representing a completed content block. + + Fields: + - module_progress: ForeignKey to ModuleProgress model + - content: ForeignKey to ContentBlock model + - completed_at: DateTimeField that is nullable and blankable + + Methods: + - __str__: Returns a string representation of the CompletedContent object + """ + + module_progress = models.ForeignKey( + "training.ModuleProgress", + on_delete=models.CASCADE, + related_name="completed_contents", + ) + content = models.ForeignKey("training.ContentBlock", on_delete=models.CASCADE) + completed_at = models.DateTimeField(null=True, blank=True) + + def __str__(self): + return f"{self.module_progress.course_progress.user.username} - {self.content}" + + +class CompletedQuiz(models.Model): + """ + Model representing a completed quiz. + + Fields: + - module_progress: ForeignKey to ModuleProgress model + - quiz: ForeignKey to Quiz model + - completed_at: DateTimeField that is nullable and blankable + + Methods: + - __str__: Returns a string representation of the CompletedQuiz object + """ + + module_progress = models.ForeignKey( + "training.ModuleProgress", + on_delete=models.CASCADE, + related_name="completed_quizzes", + ) + quiz = models.ForeignKey("training.Quiz", on_delete=models.CASCADE) + completed_at = models.DateTimeField(null=True, blank=True) + + def __str__(self): + return f"{self.module_progress.course_progress.user.username} - {self.quiz}" diff --git a/physionet-django/training/serializers.py b/physionet-django/training/serializers.py new file mode 100644 index 0000000000..044fdb7b6a --- /dev/null +++ b/physionet-django/training/serializers.py @@ -0,0 +1,121 @@ +import datetime +from rest_framework import serializers +from django.db import transaction + +from training.models import Course, Module, Quiz, QuizChoice, ContentBlock +from user.models import Training, TrainingType +from user.enums import RequiredField + + +class QuizChoiceSerializer(serializers.ModelSerializer): + + class Meta: + model = QuizChoice + fields = ['body', 'is_correct'] + read_only_fields = ['id', 'quiz'] + + +class QuizSerializer(serializers.ModelSerializer): + choices = QuizChoiceSerializer(many=True) + + class Meta: + model = Quiz + fields = ['question', 'order', 'choices'] + read_only_fields = ['id', 'module'] + + +class ContentBlockSerializer(serializers.ModelSerializer): + + class Meta: + model = ContentBlock + fields = ['body', 'order'] + read_only_fields = ['id', 'module'] + + +class ModuleSerializer(serializers.ModelSerializer): + quizzes = QuizSerializer(many=True) + contents = ContentBlockSerializer(many=True) + + class Meta: + model = Module + fields = ['name', 'description', 'order', 'contents', 'quizzes'] + read_only_fields = ['id', 'course'] + + +def create_quizzes(module_instance, quizzes_data): + choice_bulk = [] + for quiz in quizzes_data: + choices = quiz.pop('choices') + + quiz['module'] = module_instance + q = Quiz(**quiz) + q.save() + + for choice in choices: + choice['quiz'] = q + choice_bulk.append(QuizChoice(**choice)) + + QuizChoice.objects.bulk_create(choice_bulk) + + +def create_contentblocks(module_instance, content_data): + content_bulk = [] + for content in content_data: + content['module'] = module_instance + content_bulk.append(ContentBlock(**content)) + ContentBlock.objects.bulk_create(content_bulk) + + +def create_modules(course_instance, modules_data): + for module_data in modules_data: + quizzes = module_data.pop('quizzes') + contents = module_data.pop('contents') + + module_data['course'] = course_instance + module_instance = Module.objects.create(**module_data) + + create_quizzes(module_instance, quizzes) + create_contentblocks(module_instance, contents) + + +class CourseSerializer(serializers.ModelSerializer): + modules = ModuleSerializer(many=True) + + class Meta: + model = Course + fields = ['title', 'description', 'valid_duration', 'version', 'modules'] + read_only_fields = ['id', 'training_type'] + + def update(self, instance, validated_data): + with transaction.atomic(): + course = validated_data + modules = course.pop('modules') + course['training_type'] = instance.training_type + + course_instance = Course.objects.create(**course) + create_modules(course_instance, modules) + + return course_instance + + def create(self, validated_data): + with transaction.atomic(): + course = validated_data + modules = course.pop('modules') + training_type_name = validated_data['title'] + training_type_description = validated_data['description'] + training_type_valid_duration = validated_data['valid_duration'] + training_type_required_field = RequiredField.PLATFORM + + training_type_instance = TrainingType.objects.create( + name=training_type_name, + description=training_type_description, + valid_duration=training_type_valid_duration, + required_field=training_type_required_field + ) + + course['training_type'] = training_type_instance + course_instance = Course.objects.create(**course) + + create_modules(course_instance, modules) + + return course_instance diff --git a/physionet-django/training/templates/training/email/training_expiry_notification.html b/physionet-django/training/templates/training/email/training_expiry_notification.html new file mode 100644 index 0000000000..3155d5d553 --- /dev/null +++ b/physionet-django/training/templates/training/email/training_expiry_notification.html @@ -0,0 +1,11 @@ +{% load i18n %}{% autoescape off %}{% filter wordwrap:70 %} +Dear {{ name }}, + +Your {{ training }} training on {{ domain }} will be expiring in {{ expiry }} days. After the expiry date, you may lose +access to certain resources. To retain access, please submit a new training certificate via the training page in your +user profile. + + +Regards +The {{ SITE_NAME }} Team +{% endfilter %}{% endautoescape %} diff --git a/physionet-django/training/urls.py b/physionet-django/training/urls.py new file mode 100644 index 0000000000..1e3749899d --- /dev/null +++ b/physionet-django/training/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from training import views + + +urlpatterns = [ + +] diff --git a/physionet-django/training/views.py b/physionet-django/training/views.py new file mode 100644 index 0000000000..d63ba9ccee --- /dev/null +++ b/physionet-django/training/views.py @@ -0,0 +1,177 @@ +from calendar import c +from hmac import new +import json +import operator +from itertools import chain +import re + +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.db import transaction +from django.db.models import Prefetch +from django.http import JsonResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.utils import timezone +from django.utils.crypto import get_random_string +from project.validators import validate_version, is_version_greater + +from rest_framework.parsers import JSONParser + +from user.models import Training, TrainingType, TrainingQuestion, RequiredField +from user.enums import TrainingStatus + +from training.models import Course, Quiz, QuizChoice, ContentBlock +from training.models import CourseProgress, ModuleProgress, CompletedContent, CompletedQuiz +from training.serializers import CourseSerializer +from console.views import console_permission_required + + +@permission_required('training.change_course', raise_exception=True) +@console_permission_required('training.change_course') +def courses(request): + """ + View function for managing courses. + Allows creation and updating of courses for a given training type. + """ + if request.POST: + + json_file = request.FILES.get("json_file", "") + + if not json_file.name.endswith('.json'): + messages.warning(request, 'File is not of JSON type') + return redirect("courses") + + # Checking if the content of the JSON file is properly formatted and according to the schema + try: + file_data = JSONParser().parse(json_file.file) + except json.decoder.JSONDecodeError: + messages.error(request, 'JSON file is not properly formatted.') + return redirect("courses") + + serializer = CourseSerializer(data=file_data, partial=True) + + if serializer.is_valid(raise_exception=False): + serializer.save() + messages.success(request, 'Course created successfully.') + else: + messages.error(request, serializer.errors) + + return redirect("courses") + + training_types = TrainingType.objects.filter(required_field=RequiredField.PLATFORM) + return render( + request, + 'console/training_type/index.html', + { + 'training_types': training_types, + 'training_type_nav': True, + }) + + +@permission_required('training.change_course', raise_exception=True) +@console_permission_required('training.change_course') +def course_details(request, training_slug): + """ + View function for managing courses. + Allows managing the version of the courses for a given training type. + Allows expiring the specific version of the course. + """ + if request.POST: + training_type = get_object_or_404(TrainingType, slug=training_slug) + json_file = request.FILES.get("json_file", "") + + if not json_file.name.endswith('.json'): + messages.warning(request, 'File is not of JSON type') + return redirect("courses") + + # Checking if the content of the JSON file is properly formatted and according to the schema + try: + file_data = JSONParser().parse(json_file.file) + except json.decoder.JSONDecodeError: + messages.error(request, 'JSON file is not properly formatted.') + return redirect("courses") + + # Checking if the Training type with the same version already exists + existing_course = Course.objects.filter(training_type=training_type) + if existing_course.exists(): + latest_course = existing_course.order_by('-version').first() + latest_course_version = existing_course.order_by('-version').first().version + new_course_version = file_data['version'] + # checking if the new course file has a valid version + if validate_version(new_course_version) is not None: + messages.error(request, 'Version number is not valid.') + # checking if the version number is greater than the existing version + elif not is_version_greater(new_course_version, latest_course_version): + messages.error(request, 'Version number should be greater than the existing version.') + else: + serializer = CourseSerializer(latest_course, data=file_data, partial=True) + if serializer.is_valid(raise_exception=False): + serializer.save() + messages.success(request, 'Course updated successfully.') + + return redirect("course_details", training_slug) + + training_type = get_object_or_404(TrainingType, slug=training_slug) + active_course_versions = Course.objects.filter(training_type=training_type, is_active=True).order_by('-version') + inactive_course_versions = Course.objects.filter(training_type=training_type, is_active=False).order_by('-version') + return render( + request, + 'console/training_type/course_details.html', + { + 'training_type': training_type, + 'active_course_versions': active_course_versions, + 'inactive_course_versions': inactive_course_versions, + 'training_type_nav': True, + }) + + +@permission_required('training.change_course', raise_exception=True) +@console_permission_required('training.change_course') +def expire_course(request, training_slug, version): + """ + This view takes a primary key and a version number as input parameters, + and expires the course with the specified primary key and version number. + """ + course = Course.objects.filter(training_type__slug=training_slug, version=version).first() + expiry_date = request.POST.get('expiry_date') + if not course: + messages.error(request, 'Course not found') + return redirect('courses') + if not expiry_date: + messages.error(request, 'Expiry Date is required') + return redirect('course_details', training_slug) + # Checking if the expiry date is greater than the current date + expiry_date_tz = timezone.make_aware(timezone.datetime.strptime(expiry_date, '%Y-%m-%d')) + if expiry_date_tz < timezone.now(): + messages.error(request, 'Expiry Date should be greater than the current date') + return redirect('course_details', training_slug) + # Calculating the number of days between the current date and the expiry date + number_of_days = (expiry_date_tz - timezone.now()).days + course.expire_course_version(course.training_type, int(number_of_days)) + messages.success(request, 'Course expired successfully.') + return redirect('course_details', training_slug) + + +@permission_required('training.change_course', raise_exception=True) +@console_permission_required('training.change_course') +def download_course(request, training_slug, version): + """ + This view takes a primary key and a version number as input parameters, + and returns a JSON response containing information about the + training course with the specified primary key and version number. + """ + training_type = get_object_or_404(TrainingType, slug=training_slug) + course = training_type.courses.filter(version=version).first() + if not course: + messages.error(request, 'Course not found') + return redirect('courses') + training_type = course.training_type + if training_type.required_field != RequiredField.PLATFORM: + messages.error(request, 'Only onplatform course can be downloaded') + return redirect('courses') + + serializer = CourseSerializer(course) + response_data = serializer.data + response = JsonResponse(response_data, safe=False, json_dumps_params={'indent': 2}) + response['Content-Disposition'] = f'attachment; filename={training_type.name}--version-{version}.json' + return response diff --git a/physionet-django/user/enums.py b/physionet-django/user/enums.py index b3d32278f2..004ebbc96e 100644 --- a/physionet-django/user/enums.py +++ b/physionet-django/user/enums.py @@ -17,6 +17,7 @@ def choices(cls): class RequiredField(IntEnum): DOCUMENT = 0 URL = 1 + PLATFORM = 2 @classmethod def choices(cls): diff --git a/physionet-django/user/fixtures/demo-training-type.json b/physionet-django/user/fixtures/demo-training-type.json index 35b34ca87d..b8ac562adc 100644 --- a/physionet-django/user/fixtures/demo-training-type.json +++ b/physionet-django/user/fixtures/demo-training-type.json @@ -7,6 +7,7 @@ "description": "

    The CITI Data or Specimens only course covers important aspects of research with human participant data. Modules covered include:

    \n\n
      \n\t
    • Defining Research with Human Subjects
    • \n\t
    • Privacy and Confidentiality
    • \n\t
    • Assessing Risk
    • \n\t
    • Research with Children
    • \n\t
    • International Research
    • \n\t
    • History and Ethical Principles
    • \n\t
    • Regulations and Process
    • \n\t
    • SBR Methodologies in Biomedical Research
    • \n\t
    • Genetics Research
    • \n\t
    • Records-Based Research
    • \n\t
    • Populations in Research Requiring Additional Considerations and/or Protections
    • \n\t
    • HIPAA and Human Subjects Research
    • \n\t
    • Conflicts of Interest in Research Involving Human Subjects
    • \n
    ", "home_page": "/about/citi-course/", "valid_duration": "1095 00:00:00", + "slug": "citi-data-or-specimens-only-research", "required_field": 0, "questions": [ 1, @@ -17,6 +18,25 @@ ] } }, + { + "model": "user.trainingtype", + "pk": 2, + "fields": { + "name": "World 101: Introduction to Continents and Countries", + "slug": "world-101-introduction-to-continents-and-countries", + "description": "

    Join us for an exciting journey around the globe as we explore the continents and countries that make up our world. In this training, you’ll learn about the geography, culture, and history of different regions and gain a deeper understanding of our interconnected world.

    \n\n

    What You Will Learn:

    \n\n
      \n\t
    • The names and locations of the seven continents
    • \n\t
    • Key countries and their capitals on each continent
    • \n\t
    • Basic geographical features and landmarks
    • \n\t
    • Cultural and historical highlights of different regions
    • \n
    \n\n

    Prerequisites:

    \n\n
      \n\t
    • No prior knowledge is required
    • \n\t
    • An interest in geography and world cultures is recommended
    • \n
    \n\n

    Don’t miss this opportunity to expand your horizons and discover the fascinating world we live in. Our experienced instructors will guide you through this engaging training, providing insights and knowledge along the way. Sign up now to reserve your spot!

    \n\n

    Contact: For more information or to register for this training, please contact us at training@discoveringtheworld.com or call us at 555-1234.

    \n", + "home_page": "/", + "valid_duration": "1095 00:00:00", + "required_field": 2, + "questions": [ + 1, + 2, + 3, + 4, + 5 + ] + } + }, { "model": "user.question", "pk": 1, diff --git a/physionet-django/user/fixtures/demo-user.json b/physionet-django/user/fixtures/demo-user.json index 23fc251e0d..fa1eb2cb39 100644 --- a/physionet-django/user/fixtures/demo-user.json +++ b/physionet-django/user/fixtures/demo-user.json @@ -14389,11 +14389,6 @@ "project", "activeproject" ], - [ - "change_archivedproject", - "project", - "archivedproject" - ], [ "add_dua", "project", @@ -14463,6 +14458,16 @@ "view_redirect", "redirects", "redirect" + ], + [ + "change_course", + "training", + "course" + ], + [ + "can_view_course_guidelines", + "training", + "course" ] ] } @@ -14482,11 +14487,6 @@ "project", "activeproject" ], - [ - "change_archivedproject", - "project", - "archivedproject" - ], [ "can_view_access_logs", "project", @@ -14580,6 +14580,11 @@ "can_view_admin_console", "user", "user" + ], + [ + "can_view_course_guidelines", + "training", + "course" ] ] } diff --git a/physionet-django/user/forms.py b/physionet-django/user/forms.py index 5ca6e18c13..dc9e7d08a6 100644 --- a/physionet-django/user/forms.py +++ b/physionet-django/user/forms.py @@ -542,7 +542,7 @@ def clean_reference_name(self): def clean_reference_email(self): reference_email = self.cleaned_data.get('reference_email') if reference_email: - if reference_email in self.user.get_emails(): + if reference_email.lower() in [email.lower() for email in self.user.get_emails()]: raise forms.ValidationError("""You can not put yourself as a reference.""") else: diff --git a/physionet-django/user/management/commands/resetdb.py b/physionet-django/user/management/commands/resetdb.py index 96a8ebdfa5..29238248cb 100644 --- a/physionet-django/user/management/commands/resetdb.py +++ b/physionet-django/user/management/commands/resetdb.py @@ -19,7 +19,7 @@ from django.core.management.base import BaseCommand from lightwave.views import DBCAL_FILE -from project.models import ActiveProject, PublishedProject, ArchivedProject +from project.models import ActiveProject, PublishedProject from user.models import User, CredentialApplication diff --git a/physionet-django/user/migrations/0056_alter_trainingtype_required_field.py b/physionet-django/user/migrations/0056_alter_trainingtype_required_field.py new file mode 100644 index 0000000000..1328047fe3 --- /dev/null +++ b/physionet-django/user/migrations/0056_alter_trainingtype_required_field.py @@ -0,0 +1,21 @@ +# Generated by Django 4.1.5 on 2023-02-18 02:12 + +from django.db import migrations, models +import user.enums + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0055_auto_20230330_1723'), + ] + + operations = [ + migrations.AlterField( + model_name='trainingtype', + name='required_field', + field=models.PositiveSmallIntegerField( + choices=[(0, 'DOCUMENT'), (1, 'URL'), (2, 'PLATFORM')], + default=user.enums.RequiredField['DOCUMENT']), + ), + ] diff --git a/physionet-django/user/migrations/0057_merge_20230828_1158.py b/physionet-django/user/migrations/0057_merge_20230828_1158.py new file mode 100644 index 0000000000..c1d6e5ce87 --- /dev/null +++ b/physionet-django/user/migrations/0057_merge_20230828_1158.py @@ -0,0 +1,14 @@ +# Generated by Django 4.1.10 on 2023-08-28 15:58 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0056_alter_trainingtype_required_field'), + ('user', '0056_remove_credentialreview_appears_correct_and_more'), + ] + + operations = [ + ] diff --git a/physionet-django/user/migrations/0058_merge_20231127_1642.py b/physionet-django/user/migrations/0058_merge_20231127_1642.py new file mode 100644 index 0000000000..7353589c68 --- /dev/null +++ b/physionet-django/user/migrations/0058_merge_20231127_1642.py @@ -0,0 +1,14 @@ +# Generated by Django 4.1.13 on 2023-11-27 21:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0057_alter_cloudinformation_aws_id'), + ('user', '0057_merge_20230828_1158'), + ] + + operations = [ + ] diff --git a/physionet-django/user/migrations/0059_trainingtype_slug.py b/physionet-django/user/migrations/0059_trainingtype_slug.py new file mode 100644 index 0000000000..dc2ec6ab58 --- /dev/null +++ b/physionet-django/user/migrations/0059_trainingtype_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.13 on 2024-03-04 18:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("user", "0058_merge_20231127_1642"), + ] + + operations = [ + migrations.AddField( + model_name="trainingtype", + name="slug", + field=models.SlugField(max_length=128, null=True, unique=True), + ), + ] diff --git a/physionet-django/user/migrations/0060_backfill_trainingtype_slugs.py b/physionet-django/user/migrations/0060_backfill_trainingtype_slugs.py new file mode 100644 index 0000000000..e8d2187b68 --- /dev/null +++ b/physionet-django/user/migrations/0060_backfill_trainingtype_slugs.py @@ -0,0 +1,33 @@ +# Generated by Django 4.1.13 on 2024-03-04 18:15 + +from django.db import migrations +from django.utils.text import slugify + + +def generate_slugs(apps, schema_editor): + TrainingType = apps.get_model("user", "TrainingType") + for training_type in TrainingType.objects.all(): + if not training_type.slug: + slug = slugify(training_type.name) + unique_slug = slug + num = 1 + while TrainingType.objects.filter(slug=unique_slug).exists(): + unique_slug = '{}-{}'.format(slug, num) + num += 1 + training_type.slug = unique_slug + training_type.save() + + +def migrate_backward(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ("user", "0059_trainingtype_slug"), + ] + + operations = [ + migrations.RunPython(generate_slugs, migrate_backward), + ] diff --git a/physionet-django/user/models.py b/physionet-django/user/models.py index 2ad2e396f2..10de5ac0be 100644 --- a/physionet-django/user/models.py +++ b/physionet-django/user/models.py @@ -18,16 +18,18 @@ from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone +from django.utils.text import slugify from django.utils.crypto import constant_time_compare from django.utils.translation import gettext as _ -from project.validators import validate_version from project.modelcomponents.access import AccessPolicy from project.modelcomponents.fields import SafeHTMLField +from project.validators import validate_version from user import validators from user.userfiles import UserFiles from user.enums import TrainingStatus, RequiredField from user.managers import TrainingQuerySet +from training.models import Course logger = logging.getLogger(__name__) @@ -1101,10 +1103,35 @@ def __str__(self): class TrainingType(models.Model): + """Represents a type of training. For example, CITI training - + which can be on-platform or off-platform. + The training type if off-platform, will have a set of questions that + the admin uses to make sure that the training was properly validated. + If the training type is on-platform, there will be attached + courses. Each Course is a version of the training type, and has modules, + which the user can complete to get the accredition (Training) for + the particular training type. + + Attributes: + name (str): The name of the training type. + description (SafeHTMLField): The description of the training type. + valid_duration (DurationField, optional): The valid duration of the training type. + questions (ManyToManyField): The questions associated with the training type. + required_field (PositiveSmallIntegerField): The required field for the training type. + home_page (URLField, optional): The home page URL for the training type. + + Meta: + default_permissions (tuple): The default permissions for the training type. + permissions (list): The additional permissions for the training type. + + Methods: + __str__(): Returns a string representation of the training type. + """ name = models.CharField(max_length=128) description = SafeHTMLField() valid_duration = models.DurationField(null=True) questions = models.ManyToManyField(Question, related_name='training_types') + slug = models.SlugField(max_length=128, null=True, unique=True) required_field = models.PositiveSmallIntegerField(choices=RequiredField.choices(), default=RequiredField.DOCUMENT) home_page = models.URLField(blank=True) @@ -1117,6 +1144,17 @@ class Meta: def __str__(self): return self.name + def save(self, *args, **kwargs): + if not self.slug: + slug = slugify(self.name) + unique_slug = slug + num = 1 + while TrainingType.objects.filter(slug=unique_slug).exists(): + unique_slug = f'{slug}-{num}' + num += 1 + self.slug = unique_slug + return super().save(*args, **kwargs) + class TrainingRegex(models.Model): name = models.CharField(max_length=48) @@ -1179,20 +1217,28 @@ def is_withdrawn(self): def is_valid(self): if self.status == TrainingStatus.ACCEPTED: - if not self.training_type.valid_duration: - return True + if self.training_type.required_field == RequiredField.PLATFORM: + associated_course = Course.objects.filter(training=self).first() + return self.process_datetime + associated_course.valid_duration >= timezone.now() else: - return self.process_datetime + self.training_type.valid_duration >= timezone.now() + if not self.training_type.valid_duration: + return False + else: + return self.process_datetime + self.training_type.valid_duration >= timezone.now() def is_expired(self): """checks if it has exceeded the valid period (process_time + duration) if no valid duration, its not expired. """ if self.status == TrainingStatus.ACCEPTED: - if not self.training_type.valid_duration: - return False + if self.training_type.required_field == RequiredField.PLATFORM: + associated_course = Course.objects.filter(training=self).first() + return self.process_datetime + associated_course.valid_duration < timezone.now() else: - return self.process_datetime + self.training_type.valid_duration < timezone.now() + if not self.training_type.valid_duration: + return False + else: + return self.process_datetime + self.training_type.valid_duration < timezone.now() def is_rejected(self): return self.status == TrainingStatus.REJECTED diff --git a/physionet-django/user/templates/user/email/notify_primary_email.html b/physionet-django/user/templates/user/email/notify_primary_email.html new file mode 100644 index 0000000000..d34907d84e --- /dev/null +++ b/physionet-django/user/templates/user/email/notify_primary_email.html @@ -0,0 +1,8 @@ +{% load i18n %}{% autoescape off %}{% filter wordwrap:70 %} +You are receiving this email because you requested a password reset for your account at {{ SITE_NAME }}. + +Only your primary email address can be used for resetting your password. Your primary email address is {{ primary_email }}. + +Regards +The {{ SITE_NAME }} Team +{% endfilter %}{% endautoescape %} diff --git a/physionet-django/user/views.py b/physionet-django/user/views.py index 05628b8699..4607ce8c1c 100644 --- a/physionet-django/user/views.py +++ b/physionet-django/user/views.py @@ -1,7 +1,7 @@ import logging import os import pdb -from datetime import datetime +from datetime import datetime, date, timedelta import django.contrib.auth.views as auth_views import pytz @@ -9,6 +9,7 @@ from django.contrib import messages from django.contrib.auth import login from django.contrib.auth.decorators import login_required +from django.contrib.auth.forms import PasswordResetForm from django.contrib.auth.tokens import default_token_generator from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError @@ -29,6 +30,7 @@ credential_application_request, get_url_prefix, notify_account_registration, + notify_primary_email, process_credential_complete, training_application_request, ) @@ -58,7 +60,7 @@ ) from user.userfiles import UserFiles from physionet.models import StaticPage - +from django.db.models import F logger = logging.getLogger(__name__) @@ -95,13 +97,27 @@ class LogoutView(auth_views.LogoutView): pass +class CustomPasswordResetForm(PasswordResetForm): + def clean_email(self): + """ + Override the clean_email method to allow for secondary email checks. + """ + email = self.cleaned_data['email'] + secondary_email = AssociatedEmail.objects.filter(is_verified=True, is_primary_email=False, email=email).first() + + if secondary_email: + notify_primary_email(secondary_email) + + return email + + # Request password reset class PasswordResetView(auth_views.PasswordResetView): template_name = 'user/reset_password_request.html' success_url = reverse_lazy('reset_password_sent') email_template_name = 'user/email/reset_password_email.html' extra_email_context = {'SITE_NAME': settings.SITE_NAME} - + form_class = CustomPasswordResetForm # Page shown after reset email has been sent class PasswordResetDoneView(auth_views.PasswordResetDoneView): @@ -754,6 +770,19 @@ def edit_training(request): else: training_form = forms.TrainingForm(user=request.user) + expiring_trainings = Training.objects.filter( + user=request.user, + process_datetime__lte=date.today() - F('training_type__valid_duration') + timedelta(days=30), + process_datetime__gt=date.today() - F('training_type__valid_duration') + ) + if expiring_trainings: + for training in expiring_trainings: + days_until_expiry = ( + training.process_datetime.date() + training.training_type.valid_duration - date.today() + ).days + message = f"Your {training.training_type.name} training will expire in {days_until_expiry} days." + messages.warning(request, message) + return render( request, "user/edit_training.html", diff --git a/poetry.lock b/poetry.lock index 9c88d618de..645fc40b23 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,16 +1,20 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry and should not be changed by hand. [[package]] name = "asgiref" -version = "3.5.2" +version = "3.7.2" description = "ASGI specs, helper code, and adapters" +category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, - {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, + {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, + {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, ] +[package.dependencies] +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} + [package.extras] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] @@ -18,6 +22,7 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] name = "bleach" version = "3.3.0" description = "An easy safelist-based HTML-sanitizing tool." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -34,6 +39,7 @@ webencodings = "*" name = "boto3" version = "1.28.53" description = "The AWS SDK for Python" +category = "main" optional = false python-versions = ">= 3.7" files = [ @@ -53,6 +59,7 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] name = "botocore" version = "1.31.53" description = "Low-level, data-driven core of boto 3." +category = "main" optional = false python-versions = ">= 3.7" files = [ @@ -72,6 +79,7 @@ crt = ["awscrt (==0.16.26)"] name = "cachetools" version = "4.2.2" description = "Extensible memoizing collections and decorators" +category = "main" optional = false python-versions = "~=3.5" files = [ @@ -83,6 +91,7 @@ files = [ name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -94,6 +103,7 @@ files = [ name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." +category = "main" optional = false python-versions = "*" files = [ @@ -170,6 +180,7 @@ pycparser = "*" name = "chardet" version = "3.0.4" description = "Universal encoding detector for Python 2 and 3" +category = "main" optional = false python-versions = "*" files = [ @@ -181,6 +192,7 @@ files = [ 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" files = [ @@ -195,6 +207,7 @@ unicode-backport = ["unicodedata2"] name = "coverage" version = "7.2.3" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -256,34 +269,35 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "41.0.4" +version = "41.0.6" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839"}, - {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f"}, - {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714"}, - {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb"}, - {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13"}, - {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143"}, - {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397"}, - {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860"}, - {file = "cryptography-41.0.4-cp37-abi3-win32.whl", hash = "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd"}, - {file = "cryptography-41.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d"}, - {file = "cryptography-41.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67"}, - {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e"}, - {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829"}, - {file = "cryptography-41.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca"}, - {file = "cryptography-41.0.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d"}, - {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac"}, - {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9"}, - {file = "cryptography-41.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f"}, - {file = "cryptography-41.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91"}, - {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8"}, - {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6"}, - {file = "cryptography-41.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311"}, - {file = "cryptography-41.0.4.tar.gz", hash = "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a"}, + {file = "cryptography-41.0.6-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:0f27acb55a4e77b9be8d550d762b0513ef3fc658cd3eb15110ebbcbd626db12c"}, + {file = "cryptography-41.0.6-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ae236bb8760c1e55b7a39b6d4d32d2279bc6c7c8500b7d5a13b6fb9fc97be35b"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afda76d84b053923c27ede5edc1ed7d53e3c9f475ebaf63c68e69f1403c405a8"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da46e2b5df770070412c46f87bac0849b8d685c5f2679771de277a422c7d0b86"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff369dd19e8fe0528b02e8df9f2aeb2479f89b1270d90f96a63500afe9af5cae"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b648fe2a45e426aaee684ddca2632f62ec4613ef362f4d681a9a6283d10e079d"}, + {file = "cryptography-41.0.6-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5daeb18e7886a358064a68dbcaf441c036cbdb7da52ae744e7b9207b04d3908c"}, + {file = "cryptography-41.0.6-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:068bc551698c234742c40049e46840843f3d98ad7ce265fd2bd4ec0d11306596"}, + {file = "cryptography-41.0.6-cp37-abi3-win32.whl", hash = "sha256:2132d5865eea673fe6712c2ed5fb4fa49dba10768bb4cc798345748380ee3660"}, + {file = "cryptography-41.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:48783b7e2bef51224020efb61b42704207dde583d7e371ef8fc2a5fb6c0aabc7"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8efb2af8d4ba9dbc9c9dd8f04d19a7abb5b49eab1f3694e7b5a16a5fc2856f5c"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5a550dc7a3b50b116323e3d376241829fd326ac47bc195e04eb33a8170902a9"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:85abd057699b98fce40b41737afb234fef05c67e116f6f3650782c10862c43da"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f39812f70fc5c71a15aa3c97b2bbe213c3f2a460b79bd21c40d033bb34a9bf36"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:742ae5e9a2310e9dade7932f9576606836ed174da3c7d26bc3d3ab4bd49b9f65"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:35f3f288e83c3f6f10752467c48919a7a94b7d88cc00b0668372a0d2ad4f8ead"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4d03186af98b1c01a4eda396b137f29e4e3fb0173e30f885e27acec8823c1b09"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b27a7fd4229abef715e064269d98a7e2909ebf92eb6912a9603c7e14c181928c"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:398ae1fc711b5eb78e977daa3cbf47cec20f2c08c5da129b7a296055fbb22aed"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7e00fb556bda398b99b0da289ce7053639d33b572847181d6483ad89835115f6"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:60e746b11b937911dc70d164060d28d273e31853bb359e2b2033c9e93e6f3c43"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3288acccef021e3c3c10d58933f44e8602cf04dba96d9796d70d537bb2f4bbc4"}, + {file = "cryptography-41.0.6.tar.gz", hash = "sha256:422e3e31d63743855e43e5a6fcc8b4acab860f560f9321b0ee6269cc7ed70cc3"}, ] [package.dependencies] @@ -299,37 +313,21 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] -[[package]] -name = "deprecated" -version = "1.2.13" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, - {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, -] - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] - [[package]] name = "django" -version = "4.1.10" +version = "4.2.11" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "Django-4.1.10-py3-none-any.whl", hash = "sha256:26d0260c2fb8121009e62ffc548b2398dea2522b6454208a852fb0ef264c206c"}, - {file = "Django-4.1.10.tar.gz", hash = "sha256:56343019a9fd839e2e5bf203daf45f25af79d5bffa4c71d56eae4f4404d82ade"}, + {file = "Django-4.2.11-py3-none-any.whl", hash = "sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3"}, + {file = "Django-4.2.11.tar.gz", hash = "sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4"}, ] [package.dependencies] -asgiref = ">=3.5.2,<4" -sqlparse = ">=0.2.2" +asgiref = ">=3.6.0,<4" +sqlparse = ">=0.3.1" tzdata = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] @@ -340,6 +338,7 @@ bcrypt = ["bcrypt"] name = "django-autocomplete-light" version = "3.9.4" description = "Fresh autocompletes for Django" +category = "main" optional = false python-versions = "*" files = [ @@ -359,6 +358,7 @@ tags = ["django-taggit"] name = "django-background-tasks-updated" version = "1.2.7" description = "Database backed asynchronous task queue" +category = "main" optional = false python-versions = "*" files = [ @@ -374,6 +374,7 @@ six = "*" name = "django-ckeditor" version = "6.5.1" description = "Django admin CKEditor integration." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -389,6 +390,7 @@ django-js-asset = ">=2.0" name = "django-cors-headers" version = "3.14.0" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -399,10 +401,26 @@ files = [ [package.dependencies] Django = ">=3.2" +[[package]] +name = "django-coverage-plugin" +version = "3.1.0" +description = "Django template coverage.py plugin" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "django_coverage_plugin-3.1.0-py3-none-any.whl", hash = "sha256:eb0ea8ffdb0db11a02994fc99be6500550efb496c350d709f418ff3d8e553a67"}, + {file = "django_coverage_plugin-3.1.0.tar.gz", hash = "sha256:223d34bf92bebadcb8b7b89932480e41c7bd98b44a8156934488fbe7f4a71f99"}, +] + +[package.dependencies] +coverage = "*" + [[package]] name = "django-debug-toolbar" version = "3.2.4" description = "A configurable set of panels that display various debug information about the current request/response." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -418,6 +436,7 @@ sqlparse = ">=0.2.0" name = "django-js-asset" version = "2.0.0" description = "script tag with additional attributes for django.forms.Media" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -435,6 +454,7 @@ tests = ["coverage"] name = "django-oauth-toolkit" version = "2.2.0" description = "OAuth2 Provider for Django" +category = "main" optional = false python-versions = "*" files = [ @@ -448,10 +468,51 @@ jwcrypto = ">=0.8.0" oauthlib = ">=3.1.0" requests = ">=2.13.0" +[[package]] +name = "django-picklefield" +version = "3.1" +description = "Pickled object field for Django" +category = "main" +optional = false +python-versions = ">=3" +files = [ + {file = "django-picklefield-3.1.tar.gz", hash = "sha256:c786cbeda78d6def2b43bff4840d19787809c8909f7ad683961703060398d356"}, + {file = "django_picklefield-3.1-py3-none-any.whl", hash = "sha256:d77c504df7311e8ec14e8b779f10ca6fec74de6c7f8e2c136e1ef60cf955125d"}, +] + +[package.dependencies] +Django = ">=3.2" + +[package.extras] +tests = ["tox"] + +[[package]] +name = "django-q2" +version = "1.6.2" +description = "A multiprocessing distributed task queue for Django" +category = "main" +optional = false +python-versions = ">=3.8,<4" +files = [ + {file = "django_q2-1.6.2-py3-none-any.whl", hash = "sha256:c2d75552c80b83ca0d8c0b0db7db4f17e9f43ee131a46d0ddd514c5f5fc603cb"}, + {file = "django_q2-1.6.2.tar.gz", hash = "sha256:cd83c16b5791cd99f83a8d106d2447305d73c6c8ed8ec22c7cb954fe0e814284"}, +] + +[package.dependencies] +django = ">=3.2,<6" +django-picklefield = ">=3.1,<4.0" +importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} + +[package.extras] +rollbar = ["django-q-rollbar (>=0.1)"] +sentry = ["django-q-sentry (>=0.1)"] +testing = ["blessed (>=1.19.1,<2.0.0)", "boto3 (>=1.24.92,<2.0.0)", "croniter (>=2.0.1,<3.0.0)", "django-redis (>=5.2.0,<6.0.0)", "hiredis (>=2.0.0,<3.0.0)", "iron-mq (>=0.9,<0.10)", "psutil (>=5.9.2,<6.0.0)", "pymongo (>=4.2.0,<5.0.0)", "redis (>=4.3.4,<5.0.0)", "setproctitle (>=1.3.2,<2.0.0)"] + [[package]] name = "django-sass" version = "1.1.0" description = "The absolute simplest way to use Sass with Django. Pure Python, minimal dependencies, and no special configuration required!" +category = "main" optional = false python-versions = "*" files = [ @@ -467,6 +528,7 @@ libsass = "*" name = "django-storages" version = "1.12.3" description = "Support for many storage backends in Django" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -490,6 +552,7 @@ sftp = ["paramiko"] name = "djangorestframework" version = "3.14.0" description = "Web APIs for Django, made easy." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -505,6 +568,7 @@ pytz = "*" name = "google-api-core" version = "1.34.0" description = "Google API client core library" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -529,6 +593,7 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"] name = "google-api-python-client" version = "1.12.8" description = "Google API Client Library for Python" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" files = [ @@ -548,6 +613,7 @@ uritemplate = ">=3.0.0,<4dev" name = "google-auth" version = "1.32.0" description = "Google Authentication Library" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" files = [ @@ -571,6 +637,7 @@ reauth = ["pyu2f (>=0.1.5)"] name = "google-auth-httplib2" version = "0.1.0" description = "Google Authentication Library: httplib2 transport" +category = "main" optional = false python-versions = "*" files = [ @@ -587,6 +654,7 @@ six = "*" name = "google-cloud-core" version = "1.7.0" description = "Google Cloud API client core library" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" files = [ @@ -606,6 +674,7 @@ grpc = ["grpcio (>=1.8.2,<2.0dev)"] name = "google-cloud-storage" version = "1.42.3" description = "Google Cloud Storage API client library" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" files = [ @@ -626,6 +695,7 @@ six = "*" name = "google-cloud-workflows" version = "1.9.1" description = "Google Cloud Workflows API client library" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -634,10 +704,10 @@ files = [ ] [package.dependencies] -google-api-core = {version = ">=1.34.0,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} +google-api-core = {version = ">=1.34.0,<2.0.0 || >=2.11.0,<3.0.0dev", extras = ["grpc"]} proto-plus = [ - {version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\""}, {version = ">=1.22.0,<2.0.0dev", markers = "python_version < \"3.11\""}, + {version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\""}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev" @@ -645,6 +715,7 @@ protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4 name = "google-crc32c" version = "1.1.2" description = "A python wrapper of the C library 'Google CRC32C'" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -689,6 +760,7 @@ testing = ["pytest"] name = "google-resumable-media" version = "1.3.1" description = "Utilities for Google Media Downloads and Resumable Uploads" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" files = [ @@ -708,6 +780,7 @@ requests = ["requests (>=2.18.0,<3.0.0dev)"] name = "googleapis-common-protos" version = "1.58.0" description = "Common protobufs used in Google APIs" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -723,65 +796,67 @@ grpc = ["grpcio (>=1.44.0,<2.0.0dev)"] [[package]] name = "grpcio" -version = "1.53.0" +version = "1.53.2" description = "HTTP/2-based RPC framework" +category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "grpcio-1.53.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:752d2949b40e12e6ad3ed8cc552a65b54d226504f6b1fb67cab2ccee502cc06f"}, - {file = "grpcio-1.53.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:8a48fd3a7222be226bb86b7b413ad248f17f3101a524018cdc4562eeae1eb2a3"}, - {file = "grpcio-1.53.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:f3e837d29f0e1b9d6e7b29d569e2e9b0da61889e41879832ea15569c251c303a"}, - {file = "grpcio-1.53.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef7d30242409c3aa5839b501e877e453a2c8d3759ca8230dd5a21cda029f046"}, - {file = "grpcio-1.53.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6f90698b5d1c5dd7b3236cd1fa959d7b80e17923f918d5be020b65f1c78b173"}, - {file = "grpcio-1.53.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a96c3c7f564b263c5d7c0e49a337166c8611e89c4c919f66dba7b9a84abad137"}, - {file = "grpcio-1.53.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ee81349411648d1abc94095c68cd25e3c2812e4e0367f9a9355be1e804a5135c"}, - {file = "grpcio-1.53.0-cp310-cp310-win32.whl", hash = "sha256:fdc6191587de410a184550d4143e2b24a14df495c86ca15e59508710681690ac"}, - {file = "grpcio-1.53.0-cp310-cp310-win_amd64.whl", hash = "sha256:658ffe1e39171be00490db5bd3b966f79634ac4215a1eb9a85c6cd6783bf7f6e"}, - {file = "grpcio-1.53.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:1b172e6d497191940c4b8d75b53de82dc252e15b61de2951d577ec5b43316b29"}, - {file = "grpcio-1.53.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:82434ba3a5935e47908bc861ce1ebc43c2edfc1001d235d6e31e5d3ed55815f7"}, - {file = "grpcio-1.53.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:1c734a2d4843e4e14ececf5600c3c4750990ec319e1299db7e4f0d02c25c1467"}, - {file = "grpcio-1.53.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6a2ead3de3b2d53119d473aa2f224030257ef33af1e4ddabd4afee1dea5f04c"}, - {file = "grpcio-1.53.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a34d6e905f071f9b945cabbcc776e2055de1fdb59cd13683d9aa0a8f265b5bf9"}, - {file = "grpcio-1.53.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eaf8e3b97caaf9415227a3c6ca5aa8d800fecadd526538d2bf8f11af783f1550"}, - {file = "grpcio-1.53.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:da95778d37be8e4e9afca771a83424f892296f5dfb2a100eda2571a1d8bbc0dc"}, - {file = "grpcio-1.53.0-cp311-cp311-win32.whl", hash = "sha256:e4f513d63df6336fd84b74b701f17d1bb3b64e9d78a6ed5b5e8a198bbbe8bbfa"}, - {file = "grpcio-1.53.0-cp311-cp311-win_amd64.whl", hash = "sha256:ddb2511fbbb440ed9e5c9a4b9b870f2ed649b7715859fd6f2ebc585ee85c0364"}, - {file = "grpcio-1.53.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:2a912397eb8d23c177d6d64e3c8bc46b8a1c7680b090d9f13a640b104aaec77c"}, - {file = "grpcio-1.53.0-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:55930c56b8f5b347d6c8c609cc341949a97e176c90f5cbb01d148d778f3bbd23"}, - {file = "grpcio-1.53.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:6601d812105583948ab9c6e403a7e2dba6e387cc678c010e74f2d6d589d1d1b3"}, - {file = "grpcio-1.53.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c705e0c21acb0e8478a00e7e773ad0ecdb34bd0e4adc282d3d2f51ba3961aac7"}, - {file = "grpcio-1.53.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba074af9ca268ad7b05d3fc2b920b5fb3c083da94ab63637aaf67f4f71ecb755"}, - {file = "grpcio-1.53.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:14817de09317dd7d3fbc8272864288320739973ef0f4b56bf2c0032349da8cdf"}, - {file = "grpcio-1.53.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c7ad9fbedb93f331c2e9054e202e95cf825b885811f1bcbbdfdc301e451442db"}, - {file = "grpcio-1.53.0-cp37-cp37m-win_amd64.whl", hash = "sha256:dad5b302a4c21c604d88a5d441973f320134e6ff6a84ecef9c1139e5ffd466f6"}, - {file = "grpcio-1.53.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:fa8eaac75d3107e3f5465f2c9e3bbd13db21790c6e45b7de1756eba16b050aca"}, - {file = "grpcio-1.53.0-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:104a2210edd3776c38448b4f76c2f16e527adafbde171fc72a8a32976c20abc7"}, - {file = "grpcio-1.53.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:dbc1ba968639c1d23476f75c356e549e7bbf2d8d6688717dcab5290e88e8482b"}, - {file = "grpcio-1.53.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95952d3fe795b06af29bb8ec7bbf3342cdd867fc17b77cc25e6733d23fa6c519"}, - {file = "grpcio-1.53.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f144a790f14c51b8a8e591eb5af40507ffee45ea6b818c2482f0457fec2e1a2e"}, - {file = "grpcio-1.53.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0698c094688a2dd4c7c2f2c0e3e142cac439a64d1cef6904c97f6cde38ba422f"}, - {file = "grpcio-1.53.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6b6d60b0958be711bab047e9f4df5dbbc40367955f8651232bfdcdd21450b9ab"}, - {file = "grpcio-1.53.0-cp38-cp38-win32.whl", hash = "sha256:1948539ce78805d4e6256ab0e048ec793956d54787dc9d6777df71c1d19c7f81"}, - {file = "grpcio-1.53.0-cp38-cp38-win_amd64.whl", hash = "sha256:df9ba1183b3f649210788cf80c239041dddcb375d6142d8bccafcfdf549522cd"}, - {file = "grpcio-1.53.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:19caa5b7282a89b799e63776ff602bb39604f7ca98db6df27e2de06756ae86c3"}, - {file = "grpcio-1.53.0-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:b5bd026ac928c96cc23149e6ef79183125542062eb6d1ccec34c0a37e02255e7"}, - {file = "grpcio-1.53.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:7dc8584ca6c015ad82e186e82f4c0fe977394588f66b8ecfc4ec873285314619"}, - {file = "grpcio-1.53.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2eddaae8af625e45b5c8500dcca1043264d751a6872cde2eda5022df8a336959"}, - {file = "grpcio-1.53.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5fb6f3d7824696c1c9f2ad36ddb080ba5a86f2d929ef712d511b4d9972d3d27"}, - {file = "grpcio-1.53.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8270d1dc2c98ab57e6dbf36fa187db8df4c036f04a398e5d5e25b4e01a766d70"}, - {file = "grpcio-1.53.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:976a7f24eb213e8429cab78d5e120500dfcdeb01041f1f5a77b17b9101902615"}, - {file = "grpcio-1.53.0-cp39-cp39-win32.whl", hash = "sha256:9c84a481451e7174f3a764a44150f93b041ab51045aa33d7b5b68b6979114e48"}, - {file = "grpcio-1.53.0-cp39-cp39-win_amd64.whl", hash = "sha256:6beb84f83360ff29a3654f43f251ec11b809dcb5524b698d711550243debd289"}, - {file = "grpcio-1.53.0.tar.gz", hash = "sha256:a4952899b4931a6ba12951f9a141ef3e74ff8a6ec9aa2dc602afa40f63595e33"}, + {file = "grpcio-1.53.2-cp310-cp310-linux_armv7l.whl", hash = "sha256:18afdda2bbe0c615da4daff754cab0df9bbd859c415d85e7e741a2975b3208b4"}, + {file = "grpcio-1.53.2-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:33f7678287ac330c94e25f96cdb951e0861e206115ba4d8ea66cf6546b1a09d0"}, + {file = "grpcio-1.53.2-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:1af074f28a56425e4f4d99761708981543a27ae963f5b4b0a36ff71f3483479d"}, + {file = "grpcio-1.53.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df07843c8c0dc71a56d3af3dfe19165fb0d3af7d3354a72185f6fa1b4ac05cab"}, + {file = "grpcio-1.53.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b403c4ad22f3ba37c7720547d8888a1e4b74ad980a94332bbbc50330b623abc"}, + {file = "grpcio-1.53.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6275a54b41d6b1ec539b019bc3affaf6d05b0a0ba36af1a65b8a2810ef69e07d"}, + {file = "grpcio-1.53.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0e92dc6a85cd1de42527812ef1276095e62169d002d86c888b6e889fcda1dd29"}, + {file = "grpcio-1.53.2-cp310-cp310-win32.whl", hash = "sha256:6be86e8d5cf47415968588e5dfbfb92ee8757fb41139584192b67050d1a72c58"}, + {file = "grpcio-1.53.2-cp310-cp310-win_amd64.whl", hash = "sha256:5b49f372df33f5f84865aef5d46cacd23180b586c80e8cbe0ce149b96dfa8c4c"}, + {file = "grpcio-1.53.2-cp311-cp311-linux_armv7l.whl", hash = "sha256:8166ac6671472d172cc0db50323b7a7504bd534de54aa31354465a00ca44409d"}, + {file = "grpcio-1.53.2-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:f7e66d8b31ef2bada7029275debbe12c97397ec7ac70a659837a7b8a6a9dc916"}, + {file = "grpcio-1.53.2-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:712113946b303db9ae4245a13de213710367850a6c3c53530b70e87989feb8e0"}, + {file = "grpcio-1.53.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3761f9a6817e32898eaa5aecd0b0ad69d0c68ab45ea7bf206e8dc4548f025f0"}, + {file = "grpcio-1.53.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24c63592103fded38b258f1e520ba8b0a7a0bbc397cddd6520a1f74dc4b5dec0"}, + {file = "grpcio-1.53.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:53d34cbf212f03634d74ba366d595b4a06a3b60fcc731eddbd6fd7ebe4acf981"}, + {file = "grpcio-1.53.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3b789472e9ef75d179295d0c6a1f7f0aefd08189cd1c822b068b0523365a1dbe"}, + {file = "grpcio-1.53.2-cp311-cp311-win32.whl", hash = "sha256:7b44ed75b9d67d17e5a098a0f99a8fd3e5861fd3c4eb54212277a0acdf298434"}, + {file = "grpcio-1.53.2-cp311-cp311-win_amd64.whl", hash = "sha256:1d1a320230e0d020880178b8eb453300bd57700b44c3744268370502e7376a9b"}, + {file = "grpcio-1.53.2-cp37-cp37m-linux_armv7l.whl", hash = "sha256:69e99fe6bdc2cdacd04cef6b6585b00630d958c98e36d825de3eea406e15fb31"}, + {file = "grpcio-1.53.2-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:590c7206f764cfe37a65003a75977358e20919ed488f970935f54efa2741b497"}, + {file = "grpcio-1.53.2-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:1fcced1abb13cdb6a5d8b105765d30212a6cb29ab0dfb01eedecf2ff6c84371b"}, + {file = "grpcio-1.53.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07b83c06e7d113044cf3da15ca52f578c5f3dca299af711e9a589c1b71eb8be5"}, + {file = "grpcio-1.53.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3bee217bda6b2c81d9e2866f523217135a03a007a89043eee074e93d76706b0"}, + {file = "grpcio-1.53.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9efbedc737ba342d8a2459afc9bd5c5df31adcdf774b772a4e663739f2cf0d06"}, + {file = "grpcio-1.53.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0f76287d98ec79a38cba8292d0bdcd6ab9b9daf568dce1d53b9eb0135fc14d26"}, + {file = "grpcio-1.53.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d406cf2f6ccf39883a24b048c448a37bac16939408c1b6fbb4d021f3cd961448"}, + {file = "grpcio-1.53.2-cp38-cp38-linux_armv7l.whl", hash = "sha256:f9f7c0dd17f24e1774cc3a8df738246772994e853c28b28ed6ba7711ccf0abb4"}, + {file = "grpcio-1.53.2-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:f14a82d12d53eb93298c35edf88d8c3ef37243b95f94dd3c75fddcba575d34ab"}, + {file = "grpcio-1.53.2-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:1deeb84bb344351434f999cea4704ac6f1e07b3d861e34c44b50d8afa06caaa1"}, + {file = "grpcio-1.53.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cbf1e3aaec3edf734ef90182363a395d234cd4790544be914cedbe1b9fec99a"}, + {file = "grpcio-1.53.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ea235cecb9df14b49a75cbd27a634683a96bb76576363407ec820ae454ce2b2"}, + {file = "grpcio-1.53.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1df931fbb4c36363d2cb985c2c26fda8f060b541a89c6c1191fdb59151a8c934"}, + {file = "grpcio-1.53.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7734d1b91f1f3b1f186debf8ec4d168ee088a54e8186c14d89a95f7e51d3198d"}, + {file = "grpcio-1.53.2-cp38-cp38-win32.whl", hash = "sha256:b16258a31269b97e26a08d71b5deb56499e86077d26e453fad8f6ec4c06fe666"}, + {file = "grpcio-1.53.2-cp38-cp38-win_amd64.whl", hash = "sha256:b676c4365a5753bc8c49f922a5f88bdb5df6746c670a9d859d2ba2f5f97d9269"}, + {file = "grpcio-1.53.2-cp39-cp39-linux_armv7l.whl", hash = "sha256:8fc7667564c8c15748354dea1bb4035c5118df4e9dc5154ccdb6e62a3e5a2bac"}, + {file = "grpcio-1.53.2-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:80a8867746cff41c2db436dd9eea18ebbfcd0449d65b64b3ed3c995207898971"}, + {file = "grpcio-1.53.2-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:431f864f2642a97d0aa8c6b606c307f03d22f919b1a226af90488426aed35809"}, + {file = "grpcio-1.53.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bea6a20c5a732a27b64623d43614b3022e6fcfc081a75236b7f9aa069d2eaa4d"}, + {file = "grpcio-1.53.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504af9e86ab01c9c33d8a452fe846aa931d024945f2e897537ccb8f7d76778ee"}, + {file = "grpcio-1.53.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2b4f5671f9e88b7f51f54adda37a23277b7fdebd1557c47543b3e8a8044dd510"}, + {file = "grpcio-1.53.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d9c51ca201326b49cfee38336c6e7dd1cb8a6b6d0dcf84aeaecbae310a736dbc"}, + {file = "grpcio-1.53.2-cp39-cp39-win32.whl", hash = "sha256:7e6885a8431939f1ee547e965fa3cb801a518b83d3d3509e90dbef78f0b5fd29"}, + {file = "grpcio-1.53.2-cp39-cp39-win_amd64.whl", hash = "sha256:ea84becb5cbd6a94a810c5214eb263ae57e915a9ed1bdcd5b4a6baf13d8c5177"}, + {file = "grpcio-1.53.2.tar.gz", hash = "sha256:0c9e42f2499c8603af1d88771dc97e2c6b0310c278337058fd7fd1ddb35ab853"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.53.0)"] +protobuf = ["grpcio-tools (>=1.53.2)"] [[package]] name = "grpcio-status" version = "1.48.2" description = "Status proto mapping for gRPC" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -798,6 +873,7 @@ protobuf = ">=3.12.0" name = "hdn-research-environment" version = "2.3.8" description = "A Django app for supporting cloud-native research environments" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -813,6 +889,7 @@ google-cloud-workflows = ">=1.6.1" name = "html2text" version = "2018.1.9" description = "Turn HTML into equivalent Markdown-structured text." +category = "main" optional = false python-versions = "*" files = [ @@ -824,6 +901,7 @@ files = [ name = "httplib2" version = "0.19.1" description = "A comprehensive HTTP client library." +category = "main" optional = false python-versions = "*" files = [ @@ -838,6 +916,7 @@ pyparsing = ">=2.4.2,<3" name = "idna" version = "2.10" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -845,10 +924,49 @@ files = [ {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] +[[package]] +name = "importlib-metadata" +version = "7.0.2" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-7.0.2-py3-none-any.whl", hash = "sha256:f4bc4c0c070c490abf4ce96d715f68e95923320370efb66143df00199bb6c100"}, + {file = "importlib_metadata-7.0.2.tar.gz", hash = "sha256:198f568f3230878cb1b44fbd7975f87906c22336dba2e4a7f05278c281fbd792"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jinja2" +version = "3.1.3" +description = "A very fast and expressive template engine." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "jmespath" version = "1.0.1" description = "JSON Matching Expressions" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -858,22 +976,25 @@ files = [ [[package]] name = "jwcrypto" -version = "1.4.2" +version = "1.5.6" description = "Implementation of JOSE Web standards" +category = "main" optional = false -python-versions = ">= 3.6" +python-versions = ">= 3.8" files = [ - {file = "jwcrypto-1.4.2.tar.gz", hash = "sha256:80a35e9ed1b3b2c43ce03d92c5d48e6d0b6647e2aa2618e4963448923d78a37b"}, + {file = "jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789"}, + {file = "jwcrypto-1.5.6.tar.gz", hash = "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039"}, ] [package.dependencies] -cryptography = ">=2.3" -deprecated = "*" +cryptography = ">=3.4" +typing-extensions = ">=4.5.0" [[package]] name = "libsass" version = "0.21.0" description = "Sass for Python: A straightforward binding of libsass for Python." +category = "main" optional = false python-versions = "*" files = [ @@ -892,10 +1013,131 @@ files = [ [package.dependencies] six = "*" +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "moto" +version = "4.2.10" +description = "" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "moto-4.2.10-py2.py3-none-any.whl", hash = "sha256:5cf0736d1f43cb887498d00b00ae522774bfddb7db1f4994fedea65b290b9f0e"}, + {file = "moto-4.2.10.tar.gz", hash = "sha256:92595fe287474a31ac3ef847941ebb097e8ffb0c3d6c106e47cf573db06933b2"}, +] + +[package.dependencies] +boto3 = ">=1.9.201" +botocore = ">=1.12.201" +cryptography = ">=3.3.1" +Jinja2 = ">=2.10.1" +python-dateutil = ">=2.1,<3.0.0" +requests = ">=2.5" +responses = ">=0.13.0" +werkzeug = ">=0.5,<2.2.0 || >2.2.0,<2.2.1 || >2.2.1" +xmltodict = "*" + +[package.extras] +all = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.4.2)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] +apigateway = ["PyYAML (>=5.1)", "ecdsa (!=0.15)", "openapi-spec-validator (>=0.5.0)", "python-jose[cryptography] (>=3.1.0,<4.0.0)"] +apigatewayv2 = ["PyYAML (>=5.1)"] +appsync = ["graphql-core"] +awslambda = ["docker (>=3.0.0)"] +batch = ["docker (>=3.0.0)"] +cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.4.2)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] +cognitoidp = ["ecdsa (!=0.15)", "python-jose[cryptography] (>=3.1.0,<4.0.0)"] +ds = ["sshpubkeys (>=3.1.0)"] +dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.4.2)"] +dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.4.2)"] +ebs = ["sshpubkeys (>=3.1.0)"] +ec2 = ["sshpubkeys (>=3.1.0)"] +efs = ["sshpubkeys (>=3.1.0)"] +eks = ["sshpubkeys (>=3.1.0)"] +glue = ["pyparsing (>=3.0.7)"] +iotdata = ["jsondiff (>=1.1.2)"] +proxy = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=2.5.1)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.4.2)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] +resourcegroupstaggingapi = ["PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.4.2)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "sshpubkeys (>=3.1.0)"] +route53resolver = ["sshpubkeys (>=3.1.0)"] +s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.4.2)"] +s3crc32c = ["PyYAML (>=5.1)", "crc32c", "py-partiql-parser (==0.4.2)"] +server = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.4.2)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] +ssm = ["PyYAML (>=5.1)"] +xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] + [[package]] name = "oauthlib" version = "3.2.2" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -912,6 +1154,7 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] name = "packaging" version = "20.9" description = "Core utilities for Python packages" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -926,6 +1169,7 @@ pyparsing = ">=2.0.2" name = "pdfminer.six" version = "20211012" description = "PDF parser and analyzer" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -943,75 +1187,95 @@ docs = ["sphinx", "sphinx-argparse"] [[package]] name = "pillow" -version = "10.0.1" +version = "10.2.0" description = "Python Imaging Library (Fork)" +category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "Pillow-10.0.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a"}, - {file = "Pillow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d"}, - {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d"}, - {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19"}, - {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f"}, - {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff"}, - {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf"}, - {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd"}, - {file = "Pillow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0"}, - {file = "Pillow-10.0.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"}, - {file = "Pillow-10.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1"}, - {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21"}, - {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54"}, - {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205"}, - {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2"}, - {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b"}, - {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1"}, - {file = "Pillow-10.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088"}, - {file = "Pillow-10.0.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b"}, - {file = "Pillow-10.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed"}, - {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635"}, - {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad"}, - {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a"}, - {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91"}, - {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4"}, - {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08"}, - {file = "Pillow-10.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08"}, - {file = "Pillow-10.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a"}, - {file = "Pillow-10.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68"}, - {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500"}, - {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21"}, - {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d"}, - {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7"}, - {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a"}, - {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7"}, - {file = "Pillow-10.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3"}, - {file = "Pillow-10.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849"}, - {file = "Pillow-10.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1"}, - {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37"}, - {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876"}, - {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f"}, - {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145"}, - {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2"}, - {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf"}, - {file = "Pillow-10.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971"}, - {file = "Pillow-10.0.1-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db"}, - {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e"}, - {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4"}, - {file = "Pillow-10.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f"}, - {file = "Pillow-10.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf"}, - {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317"}, - {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d"}, - {file = "Pillow-10.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d"}, - {file = "Pillow-10.0.1.tar.gz", hash = "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, + {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, + {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, + {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, + {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, + {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, + {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, + {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, + {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, + {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, + {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, + {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, + {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, + {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, + {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, + {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, + {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, ] [package.extras] docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] [[package]] name = "proto-plus" version = "1.22.2" description = "Beautiful, Pythonic protocol buffers." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1029,9 +1293,11 @@ testing = ["google-api-core[grpc] (>=1.31.5)"] name = "protobuf" version = "3.20.3" description = "Protocol Buffers" +category = "main" optional = false python-versions = ">=3.7" files = [ + {file = "protobuf-3.20.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e67f9af1b607eb3a89aafc9bc68a9d1172aae788b2445cb9fd781bd97531f1f1"}, {file = "protobuf-3.20.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99"}, {file = "protobuf-3.20.3-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e"}, {file = "protobuf-3.20.3-cp310-cp310-win32.whl", hash = "sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c"}, @@ -1060,6 +1326,7 @@ files = [ name = "psycopg2" version = "2.9.5" description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1082,10 +1349,22 @@ files = [ name = "pyasn1" version = "0.4.8" description = "ASN.1 types and codecs" +category = "main" optional = false python-versions = "*" files = [ + {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, + {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, + {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, + {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, + {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, + {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, + {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, + {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, + {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, + {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, ] @@ -1093,11 +1372,23 @@ files = [ name = "pyasn1-modules" version = "0.2.8" description = "A collection of ASN.1-based protocols modules." +category = "main" optional = false python-versions = "*" files = [ {file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"}, + {file = "pyasn1_modules-0.2.8-py2.4.egg", hash = "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199"}, + {file = "pyasn1_modules-0.2.8-py2.5.egg", hash = "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"}, + {file = "pyasn1_modules-0.2.8-py2.6.egg", hash = "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb"}, + {file = "pyasn1_modules-0.2.8-py2.7.egg", hash = "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"}, {file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"}, + {file = "pyasn1_modules-0.2.8-py3.1.egg", hash = "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d"}, + {file = "pyasn1_modules-0.2.8-py3.2.egg", hash = "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45"}, + {file = "pyasn1_modules-0.2.8-py3.3.egg", hash = "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"}, + {file = "pyasn1_modules-0.2.8-py3.4.egg", hash = "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811"}, + {file = "pyasn1_modules-0.2.8-py3.5.egg", hash = "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed"}, + {file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"}, + {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"}, ] [package.dependencies] @@ -1107,6 +1398,7 @@ pyasn1 = ">=0.4.6,<0.5.0" name = "pycparser" version = "2.20" description = "C parser in Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1118,6 +1410,7 @@ files = [ name = "pyopenssl" version = "23.2.0" description = "Python wrapper module around the OpenSSL library" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1136,6 +1429,7 @@ test = ["flaky", "pretend", "pytest (>=3.0.1)"] name = "pyparsing" version = "2.4.7" description = "Python parsing module" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1147,6 +1441,7 @@ files = [ 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" files = [ @@ -1161,6 +1456,7 @@ six = ">=1.5" name = "python-decouple" version = "3.4" description = "Strict separation of settings from code." +category = "main" optional = false python-versions = "*" files = [ @@ -1172,6 +1468,7 @@ files = [ name = "python-json-logger" version = "2.0.2" description = "A python library adding a json log formatter" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1183,6 +1480,7 @@ files = [ name = "pytz" version = "2022.1" description = "World timezone definitions, modern and historical" +category = "main" optional = false python-versions = "*" files = [ @@ -1190,10 +1488,72 @@ files = [ {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, ] +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + [[package]] name = "requests" version = "2.31.0" description = "Python HTTP for Humans." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1215,6 +1575,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "requests-mock" version = "1.9.3" description = "Mock out responses from the requests package" +category = "dev" optional = false python-versions = "*" files = [ @@ -1234,11 +1595,13 @@ test = ["fixtures", "mock", "purl", "pytest", "sphinx", "testrepository (>=0.0.1 name = "requests-oauthlib" version = "1.3.0" description = "OAuthlib authentication support for Requests." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"}, + {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, ] [package.dependencies] @@ -1248,10 +1611,31 @@ requests = ">=2.0.0" [package.extras] rsa = ["oauthlib[signedtoken] (>=3.0.0)"] +[[package]] +name = "responses" +version = "0.24.1" +description = "A utility library for mocking out the `requests` Python library." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "responses-0.24.1-py3-none-any.whl", hash = "sha256:a2b43f4c08bfb9c9bd242568328c65a34b318741d3fab884ac843c5ceeb543f9"}, + {file = "responses-0.24.1.tar.gz", hash = "sha256:b127c6ca3f8df0eb9cc82fd93109a3007a86acb24871834c47b77765152ecf8c"}, +] + +[package.dependencies] +pyyaml = "*" +requests = ">=2.30.0,<3.0" +urllib3 = ">=1.25.10,<3.0" + +[package.extras] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"] + [[package]] name = "rsa" version = "4.7.2" description = "Pure-Python RSA implementation" +category = "main" optional = false python-versions = ">=3.5, <4" files = [ @@ -1266,6 +1650,7 @@ pyasn1 = ">=0.1.3" name = "s3transfer" version = "0.6.2" description = "An Amazon S3 Transfer Manager" +category = "main" optional = false python-versions = ">= 3.7" files = [ @@ -1283,6 +1668,7 @@ crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] name = "selenium" version = "3.141.0" description = "Python bindings for Selenium" +category = "dev" optional = false python-versions = "*" files = [ @@ -1297,6 +1683,7 @@ urllib3 = "*" name = "sentry-sdk" version = "1.14.0" description = "Python client for Sentry (https://sentry.io)" +category = "main" optional = false python-versions = "*" files = [ @@ -1335,6 +1722,7 @@ tornado = ["tornado (>=5)"] name = "setuptools" version = "65.5.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1351,6 +1739,7 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( 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.*" files = [ @@ -1362,6 +1751,7 @@ files = [ name = "sqlparse" version = "0.4.4" description = "A non-validating SQL parser." +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1374,10 +1764,23 @@ dev = ["build", "flake8"] doc = ["sphinx"] test = ["pytest", "pytest-cov"] +[[package]] +name = "typing-extensions" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, +] + [[package]] name = "tzdata" version = "2022.7" description = "Provider of IANA time zone data" +category = "main" optional = false python-versions = ">=2" files = [ @@ -1389,6 +1792,7 @@ files = [ name = "uritemplate" version = "3.0.1" description = "URI templates" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1400,6 +1804,7 @@ files = [ name = "urllib3" version = "1.26.18" 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.*, !=3.5.*" files = [ @@ -1416,6 +1821,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "uwsgi" version = "2.0.22" description = "The uWSGI server" +category = "main" optional = false python-versions = "*" files = [ @@ -1426,6 +1832,7 @@ files = [ name = "webencodings" version = "0.5.1" description = "Character encoding aliases for legacy web content" +category = "main" optional = false python-versions = "*" files = [ @@ -1434,93 +1841,56 @@ files = [ ] [[package]] -name = "wrapt" -version = "1.15.0" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, - {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, - {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, - {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, - {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, - {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, - {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, - {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, - {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, - {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, - {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, - {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, - {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, - {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, - {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, - {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, - {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, - {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, - {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, +name = "werkzeug" +version = "3.0.1" +description = "The comprehensive WSGI web application library." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"}, + {file = "werkzeug-3.0.1.tar.gz", hash = "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc"}, ] +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "xmltodict" +version = "0.13.0" +description = "Makes working with XML feel like you are working with JSON" +category = "dev" +optional = false +python-versions = ">=3.4" +files = [ + {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, + {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, +] + +[[package]] +name = "zipp" +version = "3.18.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.18.0-py3-none-any.whl", hash = "sha256:c1bb803ed69d2cce2373152797064f7e79bc43f0a3748eb494096a867e0ebf79"}, + {file = "zipp-3.18.0.tar.gz", hash = "sha256:df8d042b02765029a09b157efd8e820451045890acc30f8e37dd2f94a060221f"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + [[package]] name = "zxcvbn" version = "4.4.28" description = "" +category = "main" optional = false python-versions = "*" files = [ @@ -1530,4 +1900,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "e61f939f4000f94213fc7bfb6be961299d04d920f94172670df1900868e0d09d" +content-hash = "c53c15112493cc7e6249996abd6a5927484e2b6ff31567cb3a28c7c5a747476e" diff --git a/pyproject.toml b/pyproject.toml index 8caf5ce5e6..f2e7c48866 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,15 +10,16 @@ bleach = "^3.3.0" boto3 = "^1.28.53" botocore = "^1.31.53" chardet = "^3.0.4" -cryptography = "^41.0.4" -Django = "4.1.10" +cryptography = "^41.0.6" +Django = "4.2.11" django-autocomplete-light = "^3.9.4" django-background-tasks-updated = "=1.2.7" django-ckeditor = "6.5.1" djangorestframework = "3.14.0" +django-q2 = "1.6.2" google-cloud-storage = "^1.41.1" html2text = "^2018.1.9" -Pillow = "^10.0.1" +Pillow = "^10.2.0" python-decouple = "^3.1" uWSGI = "2.0.22" pyOpenSSL = "^23.2.0" @@ -46,7 +47,9 @@ urllib3 = "^1.26.18" [tool.poetry.dev-dependencies] coverage = "^7.2.3" +django-coverage-plugin = "^3.1.0" django-debug-toolbar = "^3.2.4" +moto = "^4.2.0" requests = "^2.21.0" requests-mock = "^1.7.0" selenium = "^3.141.0" diff --git a/requirements.txt b/requirements.txt index 83dbd6ea5b..0cf4491ece 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -asgiref==3.5.2 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4 \ - --hash=sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424 +asgiref==3.7.2 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e \ + --hash=sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed bleach==3.3.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125 \ --hash=sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433 @@ -139,33 +139,30 @@ coverage==7.2.3 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910 \ --hash=sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859 \ --hash=sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312 -cryptography==41.0.4 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67 \ - --hash=sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311 \ - --hash=sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8 \ - --hash=sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13 \ - --hash=sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143 \ - --hash=sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f \ - --hash=sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829 \ - --hash=sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd \ - --hash=sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397 \ - --hash=sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac \ - --hash=sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d \ - --hash=sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a \ - --hash=sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839 \ - --hash=sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e \ - --hash=sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6 \ - --hash=sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9 \ - --hash=sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860 \ - --hash=sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca \ - --hash=sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91 \ - --hash=sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d \ - --hash=sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714 \ - --hash=sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb \ - --hash=sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f -deprecated==1.2.13 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d \ - --hash=sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d +cryptography==41.0.6 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:068bc551698c234742c40049e46840843f3d98ad7ce265fd2bd4ec0d11306596 \ + --hash=sha256:0f27acb55a4e77b9be8d550d762b0513ef3fc658cd3eb15110ebbcbd626db12c \ + --hash=sha256:2132d5865eea673fe6712c2ed5fb4fa49dba10768bb4cc798345748380ee3660 \ + --hash=sha256:3288acccef021e3c3c10d58933f44e8602cf04dba96d9796d70d537bb2f4bbc4 \ + --hash=sha256:35f3f288e83c3f6f10752467c48919a7a94b7d88cc00b0668372a0d2ad4f8ead \ + --hash=sha256:398ae1fc711b5eb78e977daa3cbf47cec20f2c08c5da129b7a296055fbb22aed \ + --hash=sha256:422e3e31d63743855e43e5a6fcc8b4acab860f560f9321b0ee6269cc7ed70cc3 \ + --hash=sha256:48783b7e2bef51224020efb61b42704207dde583d7e371ef8fc2a5fb6c0aabc7 \ + --hash=sha256:4d03186af98b1c01a4eda396b137f29e4e3fb0173e30f885e27acec8823c1b09 \ + --hash=sha256:5daeb18e7886a358064a68dbcaf441c036cbdb7da52ae744e7b9207b04d3908c \ + --hash=sha256:60e746b11b937911dc70d164060d28d273e31853bb359e2b2033c9e93e6f3c43 \ + --hash=sha256:742ae5e9a2310e9dade7932f9576606836ed174da3c7d26bc3d3ab4bd49b9f65 \ + --hash=sha256:7e00fb556bda398b99b0da289ce7053639d33b572847181d6483ad89835115f6 \ + --hash=sha256:85abd057699b98fce40b41737afb234fef05c67e116f6f3650782c10862c43da \ + --hash=sha256:8efb2af8d4ba9dbc9c9dd8f04d19a7abb5b49eab1f3694e7b5a16a5fc2856f5c \ + --hash=sha256:ae236bb8760c1e55b7a39b6d4d32d2279bc6c7c8500b7d5a13b6fb9fc97be35b \ + --hash=sha256:afda76d84b053923c27ede5edc1ed7d53e3c9f475ebaf63c68e69f1403c405a8 \ + --hash=sha256:b27a7fd4229abef715e064269d98a7e2909ebf92eb6912a9603c7e14c181928c \ + --hash=sha256:b648fe2a45e426aaee684ddca2632f62ec4613ef362f4d681a9a6283d10e079d \ + --hash=sha256:c5a550dc7a3b50b116323e3d376241829fd326ac47bc195e04eb33a8170902a9 \ + --hash=sha256:da46e2b5df770070412c46f87bac0849b8d685c5f2679771de277a422c7d0b86 \ + --hash=sha256:f39812f70fc5c71a15aa3c97b2bbe213c3f2a460b79bd21c40d033bb34a9bf36 \ + --hash=sha256:ff369dd19e8fe0528b02e8df9f2aeb2479f89b1270d90f96a63500afe9af5cae django-autocomplete-light==3.9.4 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:0f6da75c1c7186698b867a467a8cdb359f0513fdd8e09288a0c2fb018ae3d94e django-background-tasks-updated==1.2.7 ; python_version >= "3.9" and python_version < "4.0" \ @@ -177,6 +174,9 @@ django-ckeditor==6.5.1 ; python_version >= "3.9" and python_version < "4.0" \ django-cors-headers==3.14.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:5fbd58a6fb4119d975754b2bc090f35ec160a8373f276612c675b00e8a138739 \ --hash=sha256:684180013cc7277bdd8702b80a3c5a4b3fcae4abb2bf134dceb9f5dfe300228e +django-coverage-plugin==3.1.0 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:223d34bf92bebadcb8b7b89932480e41c7bd98b44a8156934488fbe7f4a71f99 \ + --hash=sha256:eb0ea8ffdb0db11a02994fc99be6500550efb496c350d709f418ff3d8e553a67 django-debug-toolbar==3.2.4 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:644bbd5c428d3283aa9115722471769cac1bec189edf3a0c855fd8ff870375a9 \ --hash=sha256:6b633b6cfee24f232d73569870f19aa86c819d750e7f3e833f2344a9eb4b4409 @@ -186,15 +186,21 @@ django-js-asset==2.0.0 ; python_version >= "3.9" and python_version < "4.0" \ django-oauth-toolkit==2.2.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:46890decb24a34e2a5382debeaf7752e50d90b7a11716cf2a9fd067097ec0963 \ --hash=sha256:abd85c74af525a62365ec2049113e73a2ff8b46ef906e7104a7ba968ef02a11d +django-picklefield==3.1 ; python_version >= "3.9" and python_version < "4" \ + --hash=sha256:c786cbeda78d6def2b43bff4840d19787809c8909f7ad683961703060398d356 \ + --hash=sha256:d77c504df7311e8ec14e8b779f10ca6fec74de6c7f8e2c136e1ef60cf955125d +django-q2==1.6.2 ; python_version >= "3.9" and python_version < "4" \ + --hash=sha256:c2d75552c80b83ca0d8c0b0db7db4f17e9f43ee131a46d0ddd514c5f5fc603cb \ + --hash=sha256:cd83c16b5791cd99f83a8d106d2447305d73c6c8ed8ec22c7cb954fe0e814284 django-sass==1.1.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:992f39d97a7b2d7d891255e5f99512ffd616b2e0aae1ca9b76983c5ecd603342 \ --hash=sha256:da163cd7ad7b9c2f1f10f2ec8b76939640d5b32d21ee74a240ca50a059c5fe7a django-storages[google]==1.12.3 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:204a99f218b747c46edbfeeb1310d357f83f90fa6a6024d8d0a3f422570cee84 \ --hash=sha256:a475edb2f0f04c4f7e548919a751ecd50117270833956ed5bd585c0575d2a5e7 -django==4.1.10 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:26d0260c2fb8121009e62ffc548b2398dea2522b6454208a852fb0ef264c206c \ - --hash=sha256:56343019a9fd839e2e5bf203daf45f25af79d5bffa4c71d56eae4f4404d82ade +django==4.2.11 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4 \ + --hash=sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3 djangorestframework==3.14.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8 \ --hash=sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08 @@ -261,52 +267,52 @@ googleapis-common-protos==1.58.0 ; python_version >= "3.9" and python_version < grpcio-status==1.48.2 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:2c33bbdbe20188b2953f46f31af669263b6ee2a9b2d38fa0d36ee091532e21bf \ --hash=sha256:53695f45da07437b7c344ee4ef60d370fd2850179f5a28bb26d8e2aa1102ec11 -grpcio==1.53.0 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:0698c094688a2dd4c7c2f2c0e3e142cac439a64d1cef6904c97f6cde38ba422f \ - --hash=sha256:104a2210edd3776c38448b4f76c2f16e527adafbde171fc72a8a32976c20abc7 \ - --hash=sha256:14817de09317dd7d3fbc8272864288320739973ef0f4b56bf2c0032349da8cdf \ - --hash=sha256:1948539ce78805d4e6256ab0e048ec793956d54787dc9d6777df71c1d19c7f81 \ - --hash=sha256:19caa5b7282a89b799e63776ff602bb39604f7ca98db6df27e2de06756ae86c3 \ - --hash=sha256:1b172e6d497191940c4b8d75b53de82dc252e15b61de2951d577ec5b43316b29 \ - --hash=sha256:1c734a2d4843e4e14ececf5600c3c4750990ec319e1299db7e4f0d02c25c1467 \ - --hash=sha256:2a912397eb8d23c177d6d64e3c8bc46b8a1c7680b090d9f13a640b104aaec77c \ - --hash=sha256:2eddaae8af625e45b5c8500dcca1043264d751a6872cde2eda5022df8a336959 \ - --hash=sha256:55930c56b8f5b347d6c8c609cc341949a97e176c90f5cbb01d148d778f3bbd23 \ - --hash=sha256:658ffe1e39171be00490db5bd3b966f79634ac4215a1eb9a85c6cd6783bf7f6e \ - --hash=sha256:6601d812105583948ab9c6e403a7e2dba6e387cc678c010e74f2d6d589d1d1b3 \ - --hash=sha256:6b6d60b0958be711bab047e9f4df5dbbc40367955f8651232bfdcdd21450b9ab \ - --hash=sha256:6beb84f83360ff29a3654f43f251ec11b809dcb5524b698d711550243debd289 \ - --hash=sha256:752d2949b40e12e6ad3ed8cc552a65b54d226504f6b1fb67cab2ccee502cc06f \ - --hash=sha256:7dc8584ca6c015ad82e186e82f4c0fe977394588f66b8ecfc4ec873285314619 \ - --hash=sha256:82434ba3a5935e47908bc861ce1ebc43c2edfc1001d235d6e31e5d3ed55815f7 \ - --hash=sha256:8270d1dc2c98ab57e6dbf36fa187db8df4c036f04a398e5d5e25b4e01a766d70 \ - --hash=sha256:8a48fd3a7222be226bb86b7b413ad248f17f3101a524018cdc4562eeae1eb2a3 \ - --hash=sha256:95952d3fe795b06af29bb8ec7bbf3342cdd867fc17b77cc25e6733d23fa6c519 \ - --hash=sha256:976a7f24eb213e8429cab78d5e120500dfcdeb01041f1f5a77b17b9101902615 \ - --hash=sha256:9c84a481451e7174f3a764a44150f93b041ab51045aa33d7b5b68b6979114e48 \ - --hash=sha256:a34d6e905f071f9b945cabbcc776e2055de1fdb59cd13683d9aa0a8f265b5bf9 \ - --hash=sha256:a4952899b4931a6ba12951f9a141ef3e74ff8a6ec9aa2dc602afa40f63595e33 \ - --hash=sha256:a96c3c7f564b263c5d7c0e49a337166c8611e89c4c919f66dba7b9a84abad137 \ - --hash=sha256:aef7d30242409c3aa5839b501e877e453a2c8d3759ca8230dd5a21cda029f046 \ - --hash=sha256:b5bd026ac928c96cc23149e6ef79183125542062eb6d1ccec34c0a37e02255e7 \ - --hash=sha256:b6a2ead3de3b2d53119d473aa2f224030257ef33af1e4ddabd4afee1dea5f04c \ - --hash=sha256:ba074af9ca268ad7b05d3fc2b920b5fb3c083da94ab63637aaf67f4f71ecb755 \ - --hash=sha256:c5fb6f3d7824696c1c9f2ad36ddb080ba5a86f2d929ef712d511b4d9972d3d27 \ - --hash=sha256:c705e0c21acb0e8478a00e7e773ad0ecdb34bd0e4adc282d3d2f51ba3961aac7 \ - --hash=sha256:c7ad9fbedb93f331c2e9054e202e95cf825b885811f1bcbbdfdc301e451442db \ - --hash=sha256:da95778d37be8e4e9afca771a83424f892296f5dfb2a100eda2571a1d8bbc0dc \ - --hash=sha256:dad5b302a4c21c604d88a5d441973f320134e6ff6a84ecef9c1139e5ffd466f6 \ - --hash=sha256:dbc1ba968639c1d23476f75c356e549e7bbf2d8d6688717dcab5290e88e8482b \ - --hash=sha256:ddb2511fbbb440ed9e5c9a4b9b870f2ed649b7715859fd6f2ebc585ee85c0364 \ - --hash=sha256:df9ba1183b3f649210788cf80c239041dddcb375d6142d8bccafcfdf549522cd \ - --hash=sha256:e4f513d63df6336fd84b74b701f17d1bb3b64e9d78a6ed5b5e8a198bbbe8bbfa \ - --hash=sha256:e6f90698b5d1c5dd7b3236cd1fa959d7b80e17923f918d5be020b65f1c78b173 \ - --hash=sha256:eaf8e3b97caaf9415227a3c6ca5aa8d800fecadd526538d2bf8f11af783f1550 \ - --hash=sha256:ee81349411648d1abc94095c68cd25e3c2812e4e0367f9a9355be1e804a5135c \ - --hash=sha256:f144a790f14c51b8a8e591eb5af40507ffee45ea6b818c2482f0457fec2e1a2e \ - --hash=sha256:f3e837d29f0e1b9d6e7b29d569e2e9b0da61889e41879832ea15569c251c303a \ - --hash=sha256:fa8eaac75d3107e3f5465f2c9e3bbd13db21790c6e45b7de1756eba16b050aca \ - --hash=sha256:fdc6191587de410a184550d4143e2b24a14df495c86ca15e59508710681690ac +grpcio==1.53.2 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:07b83c06e7d113044cf3da15ca52f578c5f3dca299af711e9a589c1b71eb8be5 \ + --hash=sha256:0c9e42f2499c8603af1d88771dc97e2c6b0310c278337058fd7fd1ddb35ab853 \ + --hash=sha256:0e92dc6a85cd1de42527812ef1276095e62169d002d86c888b6e889fcda1dd29 \ + --hash=sha256:0f76287d98ec79a38cba8292d0bdcd6ab9b9daf568dce1d53b9eb0135fc14d26 \ + --hash=sha256:18afdda2bbe0c615da4daff754cab0df9bbd859c415d85e7e741a2975b3208b4 \ + --hash=sha256:1af074f28a56425e4f4d99761708981543a27ae963f5b4b0a36ff71f3483479d \ + --hash=sha256:1d1a320230e0d020880178b8eb453300bd57700b44c3744268370502e7376a9b \ + --hash=sha256:1deeb84bb344351434f999cea4704ac6f1e07b3d861e34c44b50d8afa06caaa1 \ + --hash=sha256:1df931fbb4c36363d2cb985c2c26fda8f060b541a89c6c1191fdb59151a8c934 \ + --hash=sha256:1fcced1abb13cdb6a5d8b105765d30212a6cb29ab0dfb01eedecf2ff6c84371b \ + --hash=sha256:24c63592103fded38b258f1e520ba8b0a7a0bbc397cddd6520a1f74dc4b5dec0 \ + --hash=sha256:2b4f5671f9e88b7f51f54adda37a23277b7fdebd1557c47543b3e8a8044dd510 \ + --hash=sha256:33f7678287ac330c94e25f96cdb951e0861e206115ba4d8ea66cf6546b1a09d0 \ + --hash=sha256:3b789472e9ef75d179295d0c6a1f7f0aefd08189cd1c822b068b0523365a1dbe \ + --hash=sha256:431f864f2642a97d0aa8c6b606c307f03d22f919b1a226af90488426aed35809 \ + --hash=sha256:504af9e86ab01c9c33d8a452fe846aa931d024945f2e897537ccb8f7d76778ee \ + --hash=sha256:53d34cbf212f03634d74ba366d595b4a06a3b60fcc731eddbd6fd7ebe4acf981 \ + --hash=sha256:590c7206f764cfe37a65003a75977358e20919ed488f970935f54efa2741b497 \ + --hash=sha256:5b403c4ad22f3ba37c7720547d8888a1e4b74ad980a94332bbbc50330b623abc \ + --hash=sha256:5b49f372df33f5f84865aef5d46cacd23180b586c80e8cbe0ce149b96dfa8c4c \ + --hash=sha256:6275a54b41d6b1ec539b019bc3affaf6d05b0a0ba36af1a65b8a2810ef69e07d \ + --hash=sha256:69e99fe6bdc2cdacd04cef6b6585b00630d958c98e36d825de3eea406e15fb31 \ + --hash=sha256:6be86e8d5cf47415968588e5dfbfb92ee8757fb41139584192b67050d1a72c58 \ + --hash=sha256:712113946b303db9ae4245a13de213710367850a6c3c53530b70e87989feb8e0 \ + --hash=sha256:7734d1b91f1f3b1f186debf8ec4d168ee088a54e8186c14d89a95f7e51d3198d \ + --hash=sha256:7b44ed75b9d67d17e5a098a0f99a8fd3e5861fd3c4eb54212277a0acdf298434 \ + --hash=sha256:7cbf1e3aaec3edf734ef90182363a395d234cd4790544be914cedbe1b9fec99a \ + --hash=sha256:7e6885a8431939f1ee547e965fa3cb801a518b83d3d3509e90dbef78f0b5fd29 \ + --hash=sha256:7ea235cecb9df14b49a75cbd27a634683a96bb76576363407ec820ae454ce2b2 \ + --hash=sha256:80a8867746cff41c2db436dd9eea18ebbfcd0449d65b64b3ed3c995207898971 \ + --hash=sha256:8166ac6671472d172cc0db50323b7a7504bd534de54aa31354465a00ca44409d \ + --hash=sha256:8fc7667564c8c15748354dea1bb4035c5118df4e9dc5154ccdb6e62a3e5a2bac \ + --hash=sha256:9efbedc737ba342d8a2459afc9bd5c5df31adcdf774b772a4e663739f2cf0d06 \ + --hash=sha256:a3bee217bda6b2c81d9e2866f523217135a03a007a89043eee074e93d76706b0 \ + --hash=sha256:b16258a31269b97e26a08d71b5deb56499e86077d26e453fad8f6ec4c06fe666 \ + --hash=sha256:b676c4365a5753bc8c49f922a5f88bdb5df6746c670a9d859d2ba2f5f97d9269 \ + --hash=sha256:bea6a20c5a732a27b64623d43614b3022e6fcfc081a75236b7f9aa069d2eaa4d \ + --hash=sha256:d406cf2f6ccf39883a24b048c448a37bac16939408c1b6fbb4d021f3cd961448 \ + --hash=sha256:d9c51ca201326b49cfee38336c6e7dd1cb8a6b6d0dcf84aeaecbae310a736dbc \ + --hash=sha256:df07843c8c0dc71a56d3af3dfe19165fb0d3af7d3354a72185f6fa1b4ac05cab \ + --hash=sha256:ea84becb5cbd6a94a810c5214eb263ae57e915a9ed1bdcd5b4a6baf13d8c5177 \ + --hash=sha256:f14a82d12d53eb93298c35edf88d8c3ef37243b95f94dd3c75fddcba575d34ab \ + --hash=sha256:f3761f9a6817e32898eaa5aecd0b0ad69d0c68ab45ea7bf206e8dc4548f025f0 \ + --hash=sha256:f7e66d8b31ef2bada7029275debbe12c97397ec7ac70a659837a7b8a6a9dc916 \ + --hash=sha256:f9f7c0dd17f24e1774cc3a8df738246772994e853c28b28ed6ba7711ccf0abb4 hdn-research-environment==2.3.8 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:371b33950e3c1598b650edefd42fa19a3532d2ca815dd42f495f0460c57df97f html2text==2018.1.9 ; python_version >= "3.9" and python_version < "4.0" \ @@ -318,11 +324,18 @@ httplib2==0.19.1 ; python_version >= "3.9" and python_version < "4.0" \ idna==2.10 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \ --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 +importlib-metadata==7.0.2 ; python_version >= "3.9" and python_version < "3.10" \ + --hash=sha256:198f568f3230878cb1b44fbd7975f87906c22336dba2e4a7f05278c281fbd792 \ + --hash=sha256:f4bc4c0c070c490abf4ce96d715f68e95923320370efb66143df00199bb6c100 +jinja2==3.1.3 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa \ + --hash=sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90 jmespath==1.0.1 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe -jwcrypto==1.4.2 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:80a35e9ed1b3b2c43ce03d92c5d48e6d0b6647e2aa2618e4963448923d78a37b +jwcrypto==1.5.6 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789 \ + --hash=sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039 libsass==0.21.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:06c8776417fe930714bdc930a3d7e795ae3d72be6ac883ff72a1b8f7c49e5ffb \ --hash=sha256:12f39712de38689a8b785b7db41d3ba2ea1d46f9379d81ea4595802d91fa6529 \ @@ -334,6 +347,70 @@ libsass==0.21.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:c9ec490609752c1d81ff6290da33485aa7cb6d7365ac665b74464c1b7d97f7da \ --hash=sha256:d5ba529d9ce668be9380563279f3ffe988f27bc5b299c5a28453df2e0b0fbaf2 \ --hash=sha256:e2b1a7d093f2e76dc694c17c0c285e846d0b0deb0e8b21dc852ba1a3a4e2f1d6 +markupsafe==2.1.3 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ + --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ + --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \ + --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \ + --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \ + --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \ + --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \ + --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \ + --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \ + --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \ + --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \ + --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \ + --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \ + --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \ + --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \ + --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \ + --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \ + --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \ + --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \ + --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \ + --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \ + --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \ + --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \ + --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \ + --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \ + --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \ + --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \ + --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \ + --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \ + --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \ + --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \ + --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \ + --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \ + --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \ + --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \ + --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \ + --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \ + --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \ + --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \ + --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \ + --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \ + --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \ + --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \ + --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \ + --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \ + --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \ + --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \ + --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \ + --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \ + --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \ + --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \ + --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \ + --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \ + --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \ + --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \ + --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \ + --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \ + --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \ + --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \ + --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11 +moto==4.2.10 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:5cf0736d1f43cb887498d00b00ae522774bfddb7db1f4994fedea65b290b9f0e \ + --hash=sha256:92595fe287474a31ac3ef847941ebb097e8ffb0c3d6c106e47cf573db06933b2 oauthlib==3.2.2 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca \ --hash=sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918 @@ -343,65 +420,79 @@ packaging==20.9 ; python_version >= "3.9" and python_version < "4.0" \ pdfminer-six==20211012 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:0351f17d362ee2d48b158be52bcde6576d96460efd038a3e89a043fba6d634d7 \ --hash=sha256:d3efb75c0249b51c1bf795e3a8bddf1726b276c77bf75fb136adea471ee2825b -pillow==10.0.1 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff \ - --hash=sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f \ - --hash=sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21 \ - --hash=sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635 \ - --hash=sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a \ - --hash=sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f \ - --hash=sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1 \ - --hash=sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d \ - --hash=sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db \ - --hash=sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849 \ - --hash=sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7 \ - --hash=sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876 \ - --hash=sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3 \ - --hash=sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317 \ - --hash=sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91 \ - --hash=sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d \ - --hash=sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b \ - --hash=sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd \ - --hash=sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed \ - --hash=sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500 \ - --hash=sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7 \ - --hash=sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a \ - --hash=sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a \ - --hash=sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0 \ - --hash=sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf \ - --hash=sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f \ - --hash=sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1 \ - --hash=sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088 \ - --hash=sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971 \ - --hash=sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a \ - --hash=sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205 \ - --hash=sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54 \ - --hash=sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08 \ - --hash=sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21 \ - --hash=sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d \ - --hash=sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08 \ - --hash=sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e \ - --hash=sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf \ - --hash=sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b \ - --hash=sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145 \ - --hash=sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2 \ - --hash=sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d \ - --hash=sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d \ - --hash=sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf \ - --hash=sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad \ - --hash=sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d \ - --hash=sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1 \ - --hash=sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4 \ - --hash=sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2 \ - --hash=sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19 \ - --hash=sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37 \ - --hash=sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4 \ - --hash=sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68 \ - --hash=sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1 +pillow==10.2.0 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8 \ + --hash=sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39 \ + --hash=sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac \ + --hash=sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869 \ + --hash=sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e \ + --hash=sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04 \ + --hash=sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9 \ + --hash=sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e \ + --hash=sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe \ + --hash=sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef \ + --hash=sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56 \ + --hash=sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa \ + --hash=sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f \ + --hash=sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f \ + --hash=sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e \ + --hash=sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a \ + --hash=sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2 \ + --hash=sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2 \ + --hash=sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5 \ + --hash=sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a \ + --hash=sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2 \ + --hash=sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213 \ + --hash=sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563 \ + --hash=sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591 \ + --hash=sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c \ + --hash=sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2 \ + --hash=sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb \ + --hash=sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757 \ + --hash=sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0 \ + --hash=sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452 \ + --hash=sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad \ + --hash=sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01 \ + --hash=sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f \ + --hash=sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5 \ + --hash=sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61 \ + --hash=sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e \ + --hash=sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b \ + --hash=sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068 \ + --hash=sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9 \ + --hash=sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588 \ + --hash=sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483 \ + --hash=sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f \ + --hash=sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67 \ + --hash=sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7 \ + --hash=sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311 \ + --hash=sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6 \ + --hash=sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72 \ + --hash=sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6 \ + --hash=sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129 \ + --hash=sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13 \ + --hash=sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67 \ + --hash=sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c \ + --hash=sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516 \ + --hash=sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e \ + --hash=sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e \ + --hash=sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364 \ + --hash=sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023 \ + --hash=sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1 \ + --hash=sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04 \ + --hash=sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d \ + --hash=sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a \ + --hash=sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7 \ + --hash=sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb \ + --hash=sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4 \ + --hash=sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e \ + --hash=sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1 \ + --hash=sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48 \ + --hash=sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868 proto-plus==1.22.2 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:0e8cda3d5a634d9895b75c573c9352c16486cb75deb0e078b5fda34db4243165 \ --hash=sha256:de34e52d6c9c6fcd704192f09767cb561bb4ee64e70eede20b0834d841f0be4d -protobuf==3.20.3 ; python_version >= "3.9" and python_version < "4.0" \ +protobuf==3.20.3 ; python_version < "4.0" and python_version >= "3.9" \ --hash=sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7 \ --hash=sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c \ --hash=sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2 \ @@ -422,6 +513,7 @@ protobuf==3.20.3 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402 \ --hash=sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7 \ --hash=sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4 \ + --hash=sha256:e67f9af1b607eb3a89aafc9bc68a9d1172aae788b2445cb9fd781bd97531f1f1 \ --hash=sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99 \ --hash=sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee psycopg2==2.9.5 ; python_version >= "3.9" and python_version < "4.0" \ @@ -439,11 +531,33 @@ psycopg2==2.9.5 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:f5b6320dbc3cf6cfb9f25308286f9f7ab464e65cfb105b64cc9c52831748ced2 \ --hash=sha256:fc04dd5189b90d825509caa510f20d1d504761e78b8dfb95a0ede180f71d50e5 pyasn1-modules==0.2.8 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8 \ + --hash=sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199 \ + --hash=sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811 \ + --hash=sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed \ + --hash=sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4 \ --hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \ - --hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74 + --hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74 \ + --hash=sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb \ + --hash=sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45 \ + --hash=sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd \ + --hash=sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0 \ + --hash=sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d \ + --hash=sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405 pyasn1==0.4.8 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359 \ + --hash=sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576 \ + --hash=sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf \ + --hash=sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7 \ --hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \ - --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba + --hash=sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00 \ + --hash=sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8 \ + --hash=sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86 \ + --hash=sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12 \ + --hash=sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776 \ + --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba \ + --hash=sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2 \ + --hash=sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3 pycparser==2.20 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0 \ --hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 @@ -465,15 +579,71 @@ python-json-logger==2.0.2 ; python_version >= "3.9" and python_version < "4.0" \ pytz==2022.1 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \ --hash=sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c +pyyaml==6.0.1 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ + --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ + --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \ + --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ + --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ + --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ + --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ + --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ + --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ + --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ + --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \ + --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \ + --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ + --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \ + --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ + --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ + --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ + --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ + --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ + --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ + --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ + --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \ + --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ + --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ + --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ + --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \ + --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ + --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ + --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ + --hash=sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef \ + --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ + --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ + --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ + --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ + --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ + --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ + --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ + --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ + --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ + --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ + --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ + --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ + --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \ + --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ + --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \ + --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ + --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ + --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ + --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ + --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ + --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f requests-mock==1.9.3 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:0a2d38a117c08bb78939ec163522976ad59a6b7fdd82b709e23bb98004a44970 \ --hash=sha256:8d72abe54546c1fc9696fa1516672f1031d72a55a1d66c85184f972a24ba0eba requests-oauthlib==1.3.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d \ - --hash=sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a + --hash=sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a \ + --hash=sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc requests==2.31.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 +responses==0.24.1 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:a2b43f4c08bfb9c9bd242568328c65a34b318741d3fab884ac843c5ceeb543f9 \ + --hash=sha256:b127c6ca3f8df0eb9cc82fd93109a3007a86acb24871834c47b77765152ecf8c rsa==4.7.2 ; python_version >= "3.9" and python_version < "4" \ --hash=sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2 \ --hash=sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9 @@ -495,6 +665,9 @@ six==1.16.0 ; python_version >= "3.9" and python_version < "4.0" \ sqlparse==0.4.4 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3 \ --hash=sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c +typing-extensions==4.10.0 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb tzdata==2022.7 ; python_version >= "3.9" and python_version < "4.0" and sys_platform == "win32" \ --hash=sha256:2b88858b0e3120792a3c0635c23daf36a7d7eeeca657c323da299d2094402a0d \ --hash=sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa @@ -509,81 +682,14 @@ uwsgi==2.0.22 ; python_version >= "3.9" and python_version < "4.0" \ webencodings==0.5.1 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 -wrapt==1.15.0 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0 \ - --hash=sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420 \ - --hash=sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a \ - --hash=sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c \ - --hash=sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079 \ - --hash=sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923 \ - --hash=sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f \ - --hash=sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1 \ - --hash=sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8 \ - --hash=sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86 \ - --hash=sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0 \ - --hash=sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364 \ - --hash=sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e \ - --hash=sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c \ - --hash=sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e \ - --hash=sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c \ - --hash=sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727 \ - --hash=sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff \ - --hash=sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e \ - --hash=sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29 \ - --hash=sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7 \ - --hash=sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72 \ - --hash=sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475 \ - --hash=sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a \ - --hash=sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317 \ - --hash=sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2 \ - --hash=sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd \ - --hash=sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640 \ - --hash=sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98 \ - --hash=sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248 \ - --hash=sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e \ - --hash=sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d \ - --hash=sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec \ - --hash=sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1 \ - --hash=sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e \ - --hash=sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9 \ - --hash=sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92 \ - --hash=sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb \ - --hash=sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094 \ - --hash=sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46 \ - --hash=sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29 \ - --hash=sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd \ - --hash=sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705 \ - --hash=sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8 \ - --hash=sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975 \ - --hash=sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb \ - --hash=sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e \ - --hash=sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b \ - --hash=sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418 \ - --hash=sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019 \ - --hash=sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1 \ - --hash=sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba \ - --hash=sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6 \ - --hash=sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2 \ - --hash=sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3 \ - --hash=sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7 \ - --hash=sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752 \ - --hash=sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416 \ - --hash=sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f \ - --hash=sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1 \ - --hash=sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc \ - --hash=sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145 \ - --hash=sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee \ - --hash=sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a \ - --hash=sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7 \ - --hash=sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b \ - --hash=sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653 \ - --hash=sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0 \ - --hash=sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90 \ - --hash=sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29 \ - --hash=sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6 \ - --hash=sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034 \ - --hash=sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09 \ - --hash=sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559 \ - --hash=sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639 +werkzeug==3.0.1 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc \ + --hash=sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10 +xmltodict==0.13.0 ; python_version >= "3.9" and python_version < "4.0" \ + --hash=sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56 \ + --hash=sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852 +zipp==3.18.0 ; python_version >= "3.9" and python_version < "3.10" \ + --hash=sha256:c1bb803ed69d2cce2373152797064f7e79bc43f0a3748eb494096a867e0ebf79 \ + --hash=sha256:df8d042b02765029a09b157efd8e820451045890acc30f8e37dd2f94a060221f zxcvbn==4.4.28 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:151bd816817e645e9064c354b13544f85137ea3320ca3be1fb6873ea75ef7dc1