diff --git a/docker-compose.yml b/docker-compose.yml index be2458d882c4..bf16c0a4b1b0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,7 +47,7 @@ services: command: label-studio-uwsgi db: - image: postgres:11.5 + image: pgautoupgrade/pgautoupgrade:13-alpine hostname: db restart: unless-stopped # Optional: Enable TLS on PostgreSQL @@ -62,4 +62,3 @@ services: volumes: - ${POSTGRES_DATA_DIR:-./postgres-data}:/var/lib/postgresql/data - ./deploy/pgsql/certs:/var/lib/postgresql/certs:ro - diff --git a/docs/source/guide/email_setup.md b/docs/source/guide/email_setup.md index ada03702c917..98dbc881f8bc 100644 --- a/docs/source/guide/email_setup.md +++ b/docs/source/guide/email_setup.md @@ -5,7 +5,7 @@ tier: enterprise type: guide order: 0 order_enterprise: 90 -meta_title: Email backends in Label Studio +meta_title: Email backends in Label Studio section: "Install & Setup" --- @@ -13,10 +13,10 @@ section: "Install & Setup" In Label Studio Enterprise, you can configure email backends to enable password reset via email and receive notifications. There are three available options for setting up email backends: * Dummy console email backend - all emails will be printed in the app console. * SMTP backend -* [Sendgrid backend](https://sendgrid.com/) +* [Sendgrid backend](https://sendgrid.com/) -### Dummy Console Backend +### Dummy Console Backend The Dummy Console Email Backend is a simple option for testing purposes. When this backend is configured, all emails generated by the application will be printed in the application console, rather than being sent to recipients. @@ -31,10 +31,11 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' The SMTP (Simple Mail Transfer Protocol) Backend allows you to use a custom SMTP server to send emails. This provides flexibility in configuring email delivery options and can integrate with various email service providers. -More details can be found here: https://docs.djangoproject.com/en/3.2/topics/email/#smtp-backend +More details can be found here: https://docs.djangoproject.com/en/4.2/topics/email/#smtp-backend. If your SMTP configuration is not compatible with certificate or hostname validation, we provide the +`label_studio.core.utils.mail.NoVerificationEmailBackend` which can be used instead of Django's default `smtp.EmailBackend`, but note that this option is less secure. See https://docs.djangoproject.com/en/5.0/releases/4.2/#miscellaneous for more information. ```bash -# SMTP server, +# SMTP server, FROM_EMAIL=Label Studio EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend EMAIL_HOST=smtp.gmail.com @@ -52,8 +53,8 @@ EMAIL_TIMEOUT=60 The Sendgrid Backend utilizes the Sendgrid API to send emails. This option requires an active Sendgrid account and API key for authentication. The Sendgrid backend offers an easy-to-use and reliable email delivery service. -```bash +```bash # option 2: Sendgrid EMAIL_BACKEND=sendgrid_backend.SendgridBackend SENDGRID_API_KEY= -``` \ No newline at end of file +``` diff --git a/label_studio/core/middleware.py b/label_studio/core/middleware.py index 12b2cb33aa9a..dca9182488eb 100644 --- a/label_studio/core/middleware.py +++ b/label_studio/core/middleware.py @@ -97,6 +97,14 @@ def process_response(self, request, response): return response + def should_redirect_with_slash(self, request): + """ + Override the original method to keep global APPEND_SLASH setting false + """ + if not request.path_info.endswith('/'): + return True + return False + class SetSessionUIDMiddleware(CommonMiddleware): def process_request(self, request): diff --git a/label_studio/core/settings/base.py b/label_studio/core/settings/base.py index 30fe83845e40..6a44c95df00a 100644 --- a/label_studio/core/settings/base.py +++ b/label_studio/core/settings/base.py @@ -236,7 +236,6 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'core.middleware.CommonMiddlewareAppendSlashWithoutRedirect', # instead of 'CommonMiddleware' - 'core.middleware.CommonMiddleware', 'django_user_agents.middleware.UserAgentMiddleware', 'core.middleware.SetSessionUIDMiddleware', 'core.middleware.ContextLogMiddleware', @@ -762,3 +761,4 @@ def collect_versions_dummy(**kwargs): } OPENAI_API_VERSION = get_env('OPENAI_API_VERSION', '2024-06-01') +APPEND_SLASH = False diff --git a/label_studio/core/utils/mail.py b/label_studio/core/utils/mail.py new file mode 100644 index 000000000000..de5d37058dca --- /dev/null +++ b/label_studio/core/utils/mail.py @@ -0,0 +1,24 @@ +import ssl + +from django.core.mail.backends.smtp import EmailBackend +from django.utils.functional import cached_property + + +class NoVerificationEmailBackend(EmailBackend): + """SMTP email backend that does not verify SSL certificates or hostname + if no certfile or keyfile is provided. This is equivalent to the behavior + of Django's smtp.EmailBackend prior to Django 4. If EmailBackend + works for you, prefer that as it's more secure than this. + """ + + @cached_property + def ssl_context(self): + if self.ssl_certfile or self.ssl_keyfile: + ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT) + ssl_context.load_cert_chain(self.ssl_certfile, self.ssl_keyfile) + return ssl_context + else: + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + return ssl_context diff --git a/label_studio/core/utils/static_serve.py b/label_studio/core/utils/static_serve.py index 7469a7170f7c..012e1cae95a8 100644 --- a/label_studio/core/utils/static_serve.py +++ b/label_studio/core/utils/static_serve.py @@ -41,7 +41,7 @@ def serve(request, path, document_root=None, show_indexes=False): raise Http404(_('ā€œ%(path)sā€ does not exist') % {'path': fullpath}) # Respect the If-Modified-Since header. statobj = fullpath.stat() - if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), statobj.st_mtime, statobj.st_size): + if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), statobj.st_mtime): return HttpResponseNotModified() content_type, encoding = mimetypes.guess_type(str(fullpath)) content_type = content_type or 'application/octet-stream' diff --git a/label_studio/data_manager/managers.py b/label_studio/data_manager/managers.py index 424f48dc0cb6..c86cf11e358d 100644 --- a/label_studio/data_manager/managers.py +++ b/label_studio/data_manager/managers.py @@ -11,7 +11,6 @@ from data_manager.prepare_params import ConjunctionEnum from django.conf import settings from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.postgres.fields.jsonb import KeyTextTransform from django.db import models from django.db.models import ( Aggregate, @@ -28,6 +27,7 @@ Value, When, ) +from django.db.models.fields.json import KeyTextTransform from django.db.models.functions import Cast, Coalesce, Concat from pydantic import BaseModel diff --git a/label_studio/data_manager/migrations/0012_alter_view_user.py b/label_studio/data_manager/migrations/0012_alter_view_user.py new file mode 100644 index 000000000000..78f0aa6f4d22 --- /dev/null +++ b/label_studio/data_manager/migrations/0012_alter_view_user.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.13 on 2024-08-13 19:51 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("data_manager", "0011_auto_20240718_1355"), + ] + + operations = [ + migrations.AlterField( + model_name="view", + name="user", + field=models.ForeignKey( + help_text="User who made this view", + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)ss", + to=settings.AUTH_USER_MODEL, + ), + ) + ] diff --git a/label_studio/io_storages/migrations/0018_alter_azureblobexportstorage_project_and_more.py b/label_studio/io_storages/migrations/0018_alter_azureblobexportstorage_project_and_more.py new file mode 100644 index 000000000000..6355a02b1e81 --- /dev/null +++ b/label_studio/io_storages/migrations/0018_alter_azureblobexportstorage_project_and_more.py @@ -0,0 +1,206 @@ +# Generated by Django 4.2.13 on 2024-08-13 19:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("tasks", "0047_merge_20240318_2210"), + ("projects", "0026_auto_20231103_0020"), + ("io_storages", "0017_auto_20240731_1638"), + ] + + operations = [ + migrations.AlterField( + model_name="azureblobexportstorage", + name="project", + field=models.ForeignKey( + help_text="A unique integer value identifying this project.", + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)ss", + to="projects.project", + ), + ), + migrations.AlterField( + model_name="azureblobexportstoragelink", + name="annotation", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s", + to="tasks.annotation", + ), + ), + migrations.AlterField( + model_name="azureblobimportstorage", + name="project", + field=models.ForeignKey( + help_text="A unique integer value identifying this project.", + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)ss", + to="projects.project", + ), + ), + migrations.AlterField( + model_name="azureblobimportstoragelink", + name="task", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s", + to="tasks.task", + ), + ), + migrations.AlterField( + model_name="gcsexportstorage", + name="project", + field=models.ForeignKey( + help_text="A unique integer value identifying this project.", + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)ss", + to="projects.project", + ), + ), + migrations.AlterField( + model_name="gcsexportstoragelink", + name="annotation", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s", + to="tasks.annotation", + ), + ), + migrations.AlterField( + model_name="gcsimportstorage", + name="project", + field=models.ForeignKey( + help_text="A unique integer value identifying this project.", + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)ss", + to="projects.project", + ), + ), + migrations.AlterField( + model_name="gcsimportstoragelink", + name="task", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s", + to="tasks.task", + ), + ), + migrations.AlterField( + model_name="localfilesexportstorage", + name="project", + field=models.ForeignKey( + help_text="A unique integer value identifying this project.", + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)ss", + to="projects.project", + ), + ), + migrations.AlterField( + model_name="localfilesexportstoragelink", + name="annotation", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s", + to="tasks.annotation", + ), + ), + migrations.AlterField( + model_name="localfilesimportstorage", + name="project", + field=models.ForeignKey( + help_text="A unique integer value identifying this project.", + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)ss", + to="projects.project", + ), + ), + migrations.AlterField( + model_name="localfilesimportstoragelink", + name="task", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s", + to="tasks.task", + ), + ), + migrations.AlterField( + model_name="redisexportstorage", + name="project", + field=models.ForeignKey( + help_text="A unique integer value identifying this project.", + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)ss", + to="projects.project", + ), + ), + migrations.AlterField( + model_name="redisexportstoragelink", + name="annotation", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s", + to="tasks.annotation", + ), + ), + migrations.AlterField( + model_name="redisimportstorage", + name="project", + field=models.ForeignKey( + help_text="A unique integer value identifying this project.", + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)ss", + to="projects.project", + ), + ), + migrations.AlterField( + model_name="redisimportstoragelink", + name="task", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s", + to="tasks.task", + ), + ), + migrations.AlterField( + model_name="s3exportstorage", + name="project", + field=models.ForeignKey( + help_text="A unique integer value identifying this project.", + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)ss", + to="projects.project", + ), + ), + migrations.AlterField( + model_name="s3exportstoragelink", + name="annotation", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s", + to="tasks.annotation", + ), + ), + migrations.AlterField( + model_name="s3importstorage", + name="project", + field=models.ForeignKey( + help_text="A unique integer value identifying this project.", + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)ss", + to="projects.project", + ), + ), + migrations.AlterField( + model_name="s3importstoragelink", + name="task", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s", + to="tasks.task", + ), + ), + ] diff --git a/label_studio/projects/models.py b/label_studio/projects/models.py index c21048dc87cb..eae7fde8fa7c 100644 --- a/label_studio/projects/models.py +++ b/label_studio/projects/models.py @@ -725,7 +725,7 @@ def get_overall(name): } return control_weights - def save(self, *args, recalc=True, **kwargs): + def save(self, *args, update_fields=None, recalc=True, **kwargs): exists = True if self.pk else False project_with_config_just_created = not exists and self.label_config @@ -733,14 +733,18 @@ def save(self, *args, recalc=True, **kwargs): self.data_types = extract_data_types(self.label_config) self.parsed_label_config = parse_config(self.label_config) self.label_config_hash = hash(str(self.parsed_label_config)) + if update_fields is not None: + update_fields = {'data_types', 'parsed_label_config', 'label_config_hash'}.union(update_fields) if self.label_config and (self._label_config_has_changed() or not exists or not self.control_weights): self.control_weights = self.get_updated_weights() + if update_fields is not None: + update_fields = {'control_weights'}.union(update_fields) if self._label_config_has_changed(): self.__original_label_config = self.label_config - super(Project, self).save(*args, **kwargs) + super(Project, self).save(*args, update_fields=update_fields, **kwargs) if not exists: steps = ProjectOnboardingSteps.objects.all() diff --git a/label_studio/pytest.ini b/label_studio/pytest.ini index f68d902c5ddd..fc9ecd49a5bc 100644 --- a/label_studio/pytest.ini +++ b/label_studio/pytest.ini @@ -3,8 +3,6 @@ DJANGO_SETTINGS_MODULE = core.settings.label_studio python_files = tests.py test_*.py *_tests.py tavern-global-cfg= tests/shared_stages.yml -filterwarnings = - ignore::django.utils.deprecation.RemovedInDjango40Warning env = D:SENTRY_RATE=0 D:SENTRY_DSN= diff --git a/label_studio/tasks/api.py b/label_studio/tasks/api.py index f50a4460a51a..fe2dc9f781f7 100644 --- a/label_studio/tasks/api.py +++ b/label_studio/tasks/api.py @@ -804,7 +804,6 @@ class AnnotationConvertAPI(generics.RetrieveAPIView): def process_intermediate_state(self, annotation, draft): pass - @swagger_auto_schema(auto_schema=None) def post(self, request, *args, **kwargs): annotation = self.get_object() organization = annotation.project.organization diff --git a/label_studio/tasks/models.py b/label_studio/tasks/models.py index 782de42ab4de..f2867d75a840 100644 --- a/label_studio/tasks/models.py +++ b/label_studio/tasks/models.py @@ -499,7 +499,7 @@ def decrease_project_summary_counters(self): def ensure_unique_groundtruth(self, annotation_id): self.annotations.exclude(id=annotation_id).update(ground_truth=False) - def save(self, *args, **kwargs): + def save(self, *args, update_fields=None, **kwargs): if flag_set('ff_back_2070_inner_id_12052022_short', AnonymousUser): if self.inner_id == 0: task = Task.objects.filter(project=self.project).order_by('-inner_id').first() @@ -509,7 +509,10 @@ def save(self, *args, **kwargs): # max_inner_id might be None in the old projects self.inner_id = None if max_inner_id is None else (max_inner_id + 1) - super().save(*args, **kwargs) + if update_fields is not None: + update_fields = {'inner_id'}.union(update_fields) + + super().save(*args, update_fields=update_fields, **kwargs) @staticmethod def delete_tasks_without_signals(queryset): @@ -536,8 +539,8 @@ def delete(self, *args, **kwargs): return result -pre_bulk_create = Signal(providing_args=['objs', 'batch_size']) -post_bulk_create = Signal(providing_args=['objs', 'batch_size']) +pre_bulk_create = Signal() # providing args 'objs' and 'batch_size' +post_bulk_create = Signal() # providing args 'objs' and 'batch_size' class AnnotationManager(models.Manager): @@ -724,11 +727,13 @@ def update_task(self): self.task.save(update_fields=update_fields) - def save(self, *args, **kwargs): + def save(self, *args, update_fields=None, **kwargs): request = get_current_request() if request: self.updated_by = request.user - result = super().save(*args, **kwargs) + if update_fields is not None: + update_fields = {'updated_by'}.union(update_fields) + result = super().save(*args, update_fields=update_fields, **kwargs) self.update_task() return result @@ -965,10 +970,12 @@ def update_task(self): self.task.save(update_fields=update_fields) - def save(self, *args, **kwargs): + def save(self, *args, update_fields=None, **kwargs): if self.project_id is None and self.task_id: logger.warning('project_id is not set for prediction, project_id being set in save method') self.project_id = Task.objects.only('project_id').get(pk=self.task_id).project_id + if update_fields is not None: + update_fields = {'project_id'}.union(update_fields) # "result" data can come in different forms - normalize them to JSON if flag_set( @@ -978,9 +985,11 @@ def save(self, *args, **kwargs): self.result = self.prepare_prediction_result(self.result, self.project) else: self.result = self.prepare_prediction_result(self.result, self.task.project) + if update_fields is not None: + update_fields = {'result'}.union(update_fields) # set updated_at field of task to now() self.update_task() - return super(Prediction, self).save(*args, **kwargs) + return super(Prediction, self).save(*args, update_fields=update_fields, **kwargs) def delete(self, *args, **kwargs): result = super().delete(*args, **kwargs) diff --git a/label_studio/users/urls.py b/label_studio/users/urls.py index 9c1763019974..e5110aa626ee 100644 --- a/label_studio/users/urls.py +++ b/label_studio/users/urls.py @@ -3,7 +3,7 @@ from os.path import join from django.conf import settings -from django.conf.urls import include, url +from django.conf.urls import include from django.urls import path, re_path from django.views.static import serve from rest_framework import routers @@ -13,12 +13,12 @@ router.register(r'users', api.UserAPI, basename='user') urlpatterns = [ - url(r'^api/', include(router.urls)), + re_path(r'^api/', include(router.urls)), # Authentication path('user/login/', views.user_login, name='user-login'), path('user/signup/', views.user_signup, name='user-signup'), path('user/account/', views.user_account, name='user-account'), - url(r'^logout/?$', views.logout, name='logout'), + re_path(r'^logout/?$', views.logout, name='logout'), # Token path('api/current-user/reset-token/', api.UserResetTokenAPI.as_view(), name='current-user-reset-token'), path('api/current-user/token', api.UserGetTokenAPI.as_view(), name='current-user-token'), diff --git a/label_studio/users/views.py b/label_studio/users/views.py index 9706deded4e1..80c52b0ba108 100644 --- a/label_studio/users/views.py +++ b/label_studio/users/views.py @@ -10,7 +10,7 @@ from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from django.shortcuts import redirect, render, reverse -from django.utils.http import is_safe_url +from django.utils.http import url_has_allowed_host_and_scheme from organizations.forms import OrganizationSignupForm from organizations.models import Organization from rest_framework.authtoken.models import Token @@ -39,7 +39,7 @@ def user_signup(request): token = request.GET.get('token') # checks if the URL is a safe redirection. - if not next_page or not is_safe_url(url=next_page, allowed_hosts=request.get_host()): + if not next_page or not url_has_allowed_host_and_scheme(url=next_page, allowed_hosts=request.get_host()): next_page = reverse('projects:project-index') user_form = forms.UserSignupForm() @@ -97,7 +97,7 @@ def user_login(request): next_page = request.GET.get('next') # checks if the URL is a safe redirection. - if not next_page or not is_safe_url(url=next_page, allowed_hosts=request.get_host()): + if not next_page or not url_has_allowed_host_and_scheme(url=next_page, allowed_hosts=request.get_host()): next_page = reverse('projects:project-index') login_form = load_func(settings.USER_LOGIN_FORM) diff --git a/poetry.lock b/poetry.lock index 6e66e14633ba..14bad8919b87 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -132,6 +132,34 @@ typing-extensions = ">=4.3.0" [package.extras] aio = ["azure-core[aio] (>=1.28.0,<2.0.0)"] +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +description = "Backport of the standard library zoneinfo module" +optional = false +python-versions = ">=3.6" +files = [ + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] + +[package.extras] +tzdata = ["tzdata"] + [[package]] name = "bleach" version = "5.0.1" @@ -586,19 +614,20 @@ files = [ [[package]] name = "django" -version = "3.2.25" -description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." +version = "4.2.13" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "Django-3.2.25-py3-none-any.whl", hash = "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38"}, - {file = "Django-3.2.25.tar.gz", hash = "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777"}, + {file = "Django-4.2.13-py3-none-any.whl", hash = "sha256:a17fcba2aad3fc7d46fdb23215095dbbd64e6174bf4589171e732b18b07e426a"}, + {file = "Django-4.2.13.tar.gz", hash = "sha256:837e3cf1f6c31347a1396a3f6b65688f2b4bb4a11c580dcb628b5afe527b68a5"}, ] [package.dependencies] -asgiref = ">=3.3.2,<4" -pytz = "*" -sqlparse = ">=0.2.2" +asgiref = ">=3.6.0,<4" +"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] argon2 = ["argon2-cffi (>=19.1.0)"] @@ -793,17 +822,17 @@ user-agents = "*" [[package]] name = "djangorestframework" -version = "3.13.1" +version = "3.14.0" description = "Web APIs for Django, made easy." optional = false python-versions = ">=3.6" files = [ - {file = "djangorestframework-3.13.1-py3-none-any.whl", hash = "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa"}, - {file = "djangorestframework-3.13.1.tar.gz", hash = "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee"}, + {file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"}, + {file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"}, ] [package.dependencies] -django = ">=2.2" +django = ">=3.0" pytz = "*" [[package]] @@ -3099,7 +3128,6 @@ files = [ {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"}, @@ -3594,12 +3622,13 @@ files = [ [[package]] name = "rules" -version = "2.2" +version = "3.4" description = "Awesome Django authorization, without the database" optional = false python-versions = "*" files = [ - {file = "rules-2.2.tar.gz", hash = "sha256:9bae429f9d4f91a375402990da1541f9e093b0ac077221d57124d06eeeca4405"}, + {file = "rules-3.4-py2.py3-none-any.whl", hash = "sha256:e906114d9b3cce73871213c6311b94cb26a5bc2c3b00aa2f13d3633a65447817"}, + {file = "rules-3.4.tar.gz", hash = "sha256:c4702c1d60ca43e97d4dfced31e98274c652dea3c461105d8df6186d663e3212"}, ] [[package]] @@ -4107,4 +4136,4 @@ mysql = ["mysqlclient"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<4" -content-hash = "6706529a96d6763abfa07f7ec9f3c0bafabb8b206ff035463601404344097012" +content-hash = "4bfdec44f23f0ded73b5cdf6c934459b098a86a8d1ba99fc12a5bf040d3f4cf2" diff --git a/pyproject.toml b/pyproject.toml index 7b421d5b44f3..b3e958205e97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,7 +154,7 @@ boto = "^2.49.0" boto3 = "^1.28.58" botocore = "^1.31.58" bleach = "~=5.0.0" -Django = "~=3.2.24" +Django = "~=4.2.13" django-storages = "1.12.3" django-annoying = "0.10.6" django-debug-toolbar = "3.2.1" @@ -167,7 +167,7 @@ django-extensions = "3.1.0" django-user-agents = "0.4.0" django-ranged-fileresponse = ">=0.1.2" drf-dynamic-fields = "0.3.0" -djangorestframework = "3.13.1" +djangorestframework = "3.14.0" drf-flex-fields = "0.9.5" humansignal-drf-yasg = ">=1.21.9" drf-generators = "0.3.0" @@ -185,7 +185,7 @@ pytz = "~=2022.1" requests = "~=2.32.3" urllib3 = "^1.26.18" rq = "1.10.1" -rules = "2.2" +rules = "3.4" ujson = ">=3.0.0" xmljson = "0.2.1" colorama = ">=0.4.4"