diff --git a/config/settings/base.py b/config/settings/base.py index 9ee6e5310..4b1e59215 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -3,6 +3,8 @@ from dotenv import load_dotenv from machina import MACHINA_MAIN_STATIC_DIR, MACHINA_MAIN_TEMPLATE_DIR +from lacommunaute.utils.loggers import CustomJsonFormatter + load_dotenv() @@ -311,19 +313,25 @@ "version": 1, "disable_existing_loggers": False, "formatters": { - "simple": { - "format": "{levelname} {asctime} {pathname} : {message}", - "style": "{", - }, + "json": {"()": CustomJsonFormatter}, }, "handlers": { - "console": {"class": "logging.StreamHandler", "formatter": "simple"}, + "console": {"class": "logging.StreamHandler", "formatter": "json"}, + "null": {"class": "logging.NullHandler"}, }, "loggers": { "django": { "handlers": ["console"], "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"), }, + "django.security.DisallowedHost": { + "handlers": ["null"], + "propagate": False, + }, + "lacommunaute": { + "handlers": ["console"], + "level": os.getenv("LACOMMUNAUTE_LOG_LEVEL", "INFO"), + }, "commands": { "handlers": ["console"], "level": os.getenv("COMMANDS_LOG_LEVEL", "INFO"), diff --git a/lacommunaute/event/views.py b/lacommunaute/event/views.py index 91143afc5..ecb6e5848 100644 --- a/lacommunaute/event/views.py +++ b/lacommunaute/event/views.py @@ -1,5 +1,5 @@ -import logging from datetime import datetime +from logging import getLogger from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import PermissionDenied @@ -14,7 +14,7 @@ from lacommunaute.event.models import Event -logger = logging.getLogger(__name__) +logger = getLogger("lacommunaute") class SuccessUrlMixin: diff --git a/lacommunaute/forum/views.py b/lacommunaute/forum/views.py index 322336b60..5dd730e8c 100644 --- a/lacommunaute/forum/views.py +++ b/lacommunaute/forum/views.py @@ -1,4 +1,4 @@ -import logging +from logging import getLogger from django.conf import settings from django.contrib.auth.mixins import UserPassesTestMixin @@ -24,7 +24,7 @@ ) -logger = logging.getLogger(__name__) +logger = getLogger("lacommunaute") PermissionRequiredMixin = get_class("forum_permission.viewmixins", "PermissionRequiredMixin") diff --git a/lacommunaute/forum_conversation/forum_polls/views.py b/lacommunaute/forum_conversation/forum_polls/views.py index 11b4583f9..e857d1536 100644 --- a/lacommunaute/forum_conversation/forum_polls/views.py +++ b/lacommunaute/forum_conversation/forum_polls/views.py @@ -1,4 +1,4 @@ -import logging +from logging import getLogger from django.template.response import TemplateResponse from machina.apps.forum_conversation.forum_polls.views import TopicPollVoteView as BaseTopicPollVoteView @@ -6,7 +6,7 @@ from machina.core.loading import get_class -logger = logging.getLogger(__name__) +logger = getLogger("lacommunaute") Topic = get_model("forum_conversation", "Topic") TopicPollVote = get_model("forum_polls", "TopicPollVote") diff --git a/lacommunaute/forum_conversation/views.py b/lacommunaute/forum_conversation/views.py index def06aa9b..101d9b4ee 100644 --- a/lacommunaute/forum_conversation/views.py +++ b/lacommunaute/forum_conversation/views.py @@ -1,4 +1,4 @@ -import logging +from logging import getLogger from django.conf import settings from django.contrib import messages @@ -15,7 +15,7 @@ from lacommunaute.notification.models import Notification -logger = logging.getLogger(__name__) +logger = getLogger("lacommunaute") TrackingHandler = get_class("forum_tracking.handler", "TrackingHandler") track_handler = TrackingHandler() @@ -26,6 +26,7 @@ def form_valid(self, *args, **kwargs): valid = super().form_valid(*args, **kwargs) track_handler.mark_topic_read(self.forum_post.topic, self.request.user) + logger.info("form is valid", extra={"context": self}) return valid diff --git a/lacommunaute/forum_conversation/views_htmx.py b/lacommunaute/forum_conversation/views_htmx.py index ff228a532..45d47e714 100644 --- a/lacommunaute/forum_conversation/views_htmx.py +++ b/lacommunaute/forum_conversation/views_htmx.py @@ -1,4 +1,4 @@ -import logging +from logging import getLogger from django.shortcuts import get_object_or_404, render from django.views import View @@ -10,7 +10,7 @@ from lacommunaute.notification.models import Notification -logger = logging.getLogger(__name__) +logger = getLogger("lacommunaute") PermissionRequiredMixin = get_class("forum_permission.viewmixins", "PermissionRequiredMixin") TrackingHandler = get_class("forum_tracking.handler", "TrackingHandler") diff --git a/lacommunaute/forum_member/views.py b/lacommunaute/forum_member/views.py index 3469110ec..4deeb7220 100644 --- a/lacommunaute/forum_member/views.py +++ b/lacommunaute/forum_member/views.py @@ -1,4 +1,4 @@ -import logging +from logging import getLogger from django.urls import reverse from django.views.generic import ListView @@ -12,7 +12,7 @@ from lacommunaute.forum_member.models import ForumProfile -logger = logging.getLogger(__name__) +logger = getLogger("lacommunaute") PermissionRequiredMixin = get_class("forum_permission.viewmixins", "PermissionRequiredMixin") diff --git a/lacommunaute/forum_moderation/views.py b/lacommunaute/forum_moderation/views.py index f7c4e8122..682fdaaae 100644 --- a/lacommunaute/forum_moderation/views.py +++ b/lacommunaute/forum_moderation/views.py @@ -1,3 +1,5 @@ +from logging import getLogger + from django.contrib import messages from django.urls import reverse from machina.apps.forum_moderation.views import ( @@ -9,6 +11,9 @@ from lacommunaute.forum_moderation.models import BlockedEmail, BlockedPost +logger = getLogger("lacommunaute") + + class TopicDeleteView(BaseTopicDeleteView): def get_success_url(self): messages.success(self.request, self.success_message) diff --git a/lacommunaute/forum_upvote/views.py b/lacommunaute/forum_upvote/views.py index 622498684..3d1e4f869 100644 --- a/lacommunaute/forum_upvote/views.py +++ b/lacommunaute/forum_upvote/views.py @@ -1,4 +1,4 @@ -import logging +from logging import getLogger from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin @@ -13,7 +13,7 @@ from lacommunaute.forum_upvote.models import UpVote -logger = logging.getLogger(__name__) +logger = getLogger("lacommunaute") PermissionRequiredMixin = get_class("forum_permission.viewmixins", "PermissionRequiredMixin") TrackingHandler = get_class("forum_tracking.handler", "TrackingHandler") diff --git a/lacommunaute/notification/emails.py b/lacommunaute/notification/emails.py index 67ffcc186..00a67d0cd 100644 --- a/lacommunaute/notification/emails.py +++ b/lacommunaute/notification/emails.py @@ -1,4 +1,4 @@ -import logging +from logging import getLogger from urllib.parse import urljoin import httpx @@ -9,7 +9,7 @@ from lacommunaute.utils.enums import Environment -logger = logging.getLogger(__name__) +logger = getLogger("lacommunaute") SIB_SMTP_URL = urljoin(settings.SIB_URL, settings.SIB_SMTP_ROUTE) SIB_CONTACTS_URL = urljoin(settings.SIB_URL, settings.SIB_CONTACTS_ROUTE) diff --git a/lacommunaute/notification/management/commands/add_user_to_list_when_register.py b/lacommunaute/notification/management/commands/add_user_to_list_when_register.py index 6b950bcad..068e5c342 100644 --- a/lacommunaute/notification/management/commands/add_user_to_list_when_register.py +++ b/lacommunaute/notification/management/commands/add_user_to_list_when_register.py @@ -1,11 +1,16 @@ +from logging import getLogger + from django.core.management.base import BaseCommand from lacommunaute.notification.tasks import add_user_to_list_when_register +logger = getLogger("commands") + + class Command(BaseCommand): help = "Ajouter un utilisateur à une liste Sendinblue quand il s'inscrit" def handle(self, *args, **options): add_user_to_list_when_register() - self.stdout.write(self.style.SUCCESS("That's all, folks!")) + logger.info("That's all, folks!") diff --git a/lacommunaute/notification/management/commands/send_messages_notifications.py b/lacommunaute/notification/management/commands/send_messages_notifications.py index 68466f26e..0fba87826 100644 --- a/lacommunaute/notification/management/commands/send_messages_notifications.py +++ b/lacommunaute/notification/management/commands/send_messages_notifications.py @@ -1,9 +1,14 @@ +from logging import getLogger + from django.core.management.base import BaseCommand from lacommunaute.notification.enums import NotificationDelay from lacommunaute.notification.tasks import send_messages_notifications, send_missyou_notifications +logger = getLogger("commands") + + class Command(BaseCommand): help = "Envoyer les notifications en file d'attente avec le délai paramétré" @@ -14,10 +19,8 @@ def handle(self, *args, **options): try: delay = NotificationDelay(options["delay"]) except ValueError: - self.stdout.write( - self.style.ERROR(f"le délai fournit doit être un valeuer de {str(NotificationDelay.values)}") - ) + logger.error("le délai fournit doit être un valeuer de %s", str(NotificationDelay.values)) send_messages_notifications(delay) send_missyou_notifications(delay) - self.stdout.write(self.style.SUCCESS("That's all, folks!")) + logger.info("That's all, folks!") diff --git a/lacommunaute/notification/management/commands/send_notifs_on_unanswered_topics.py b/lacommunaute/notification/management/commands/send_notifs_on_unanswered_topics.py index 99f2207e1..65e148b40 100644 --- a/lacommunaute/notification/management/commands/send_notifs_on_unanswered_topics.py +++ b/lacommunaute/notification/management/commands/send_notifs_on_unanswered_topics.py @@ -1,11 +1,16 @@ +from logging import getLogger + from django.core.management.base import BaseCommand from lacommunaute.notification.tasks import send_notifs_on_unanswered_topics +logger = getLogger("commands") + + class Command(BaseCommand): help = "Envoyer une notification par email aux utilisateurs volontaires quand il y a des sujets sans réponse" def handle(self, *args, **options): send_notifs_on_unanswered_topics() - self.stdout.write(self.style.SUCCESS("That's all, folks!")) + logger.info("That's all, folks!") diff --git a/lacommunaute/openid_connect/views.py b/lacommunaute/openid_connect/views.py index fae71fb28..07963cbf5 100644 --- a/lacommunaute/openid_connect/views.py +++ b/lacommunaute/openid_connect/views.py @@ -1,5 +1,5 @@ import dataclasses -import logging +from logging import getLogger import httpx import jwt @@ -14,7 +14,7 @@ from lacommunaute.openid_connect.models import OIDConnectUserData, OpenID_State -logger = logging.getLogger(__name__) +logger = getLogger("lacommunaute") @dataclasses.dataclass diff --git a/lacommunaute/pages/views.py b/lacommunaute/pages/views.py index 228a88b0c..669ebd54b 100644 --- a/lacommunaute/pages/views.py +++ b/lacommunaute/pages/views.py @@ -1,4 +1,4 @@ -import logging +from logging import getLogger from typing import Any from django.contrib.auth.mixins import UserPassesTestMixin @@ -12,7 +12,7 @@ from lacommunaute.forum_conversation.models import Topic -logger = logging.getLogger(__name__) +logger = getLogger("lacommunaute") class LandingPagesListView(UserPassesTestMixin, TemplateView): diff --git a/lacommunaute/partner/views.py b/lacommunaute/partner/views.py index a1cb33890..7de5e271b 100644 --- a/lacommunaute/partner/views.py +++ b/lacommunaute/partner/views.py @@ -1,3 +1,5 @@ +from logging import getLogger + from django.contrib.auth.mixins import UserPassesTestMixin from django.urls import reverse from django.views.generic import CreateView, DetailView, ListView, UpdateView @@ -8,6 +10,9 @@ from lacommunaute.utils.perms import forum_visibility_content_tree_from_forums +logger = getLogger("lacommunaute") + + class PartnerListView(ListView): model = Partner template_name = "partner/list.html" diff --git a/lacommunaute/search/views.py b/lacommunaute/search/views.py index 4787f25eb..7c298a37f 100644 --- a/lacommunaute/search/views.py +++ b/lacommunaute/search/views.py @@ -1,3 +1,5 @@ +from logging import getLogger + from django.contrib.postgres.search import SearchHeadline, SearchQuery, SearchRank from django.db.models import F from django.views.generic import ListView @@ -9,6 +11,9 @@ from lacommunaute.search.models import CommonIndex +logger = getLogger("lacommunaute") + + class SearchView(FormMixin, ListView): model = CommonIndex form_class = SearchForm diff --git a/lacommunaute/stats/management/commands/collect_django_stats.py b/lacommunaute/stats/management/commands/collect_django_stats.py index 7774dcdc2..79b6f25b9 100644 --- a/lacommunaute/stats/management/commands/collect_django_stats.py +++ b/lacommunaute/stats/management/commands/collect_django_stats.py @@ -1,12 +1,17 @@ +from logging import getLogger + from django.core.management.base import BaseCommand from lacommunaute.surveys.stats import collect_dsp_stats +logger = getLogger("commands") + + class Command(BaseCommand): help = "Collecter les stats django, jusqu'à la veille de l'execution" def handle(self, *args, **options): from_date, count = collect_dsp_stats() - self.stdout.write(self.style.SUCCESS(f"Collecting DSP stats from {from_date} to yesterday: {count} new stats")) - self.stdout.write(self.style.SUCCESS("That's all, folks!")) + logger.info("Collecting DSP stats from %s to yesterday: %s new stats", from_date, count) + logger.info("That's all, folks!") diff --git a/lacommunaute/stats/management/commands/collect_events.py b/lacommunaute/stats/management/commands/collect_events.py index 9973e708e..2748e2bde 100644 --- a/lacommunaute/stats/management/commands/collect_events.py +++ b/lacommunaute/stats/management/commands/collect_events.py @@ -1,8 +1,8 @@ # vincenporte ~ assumed quick'n'dirty solution # TODO: refactor it code base and test it - import json from datetime import datetime +from logging import getLogger from dateutil.relativedelta import relativedelta from django.core.management.base import BaseCommand @@ -15,6 +15,9 @@ from lacommunaute.utils.matomo import get_matomo_data +logger = getLogger("commands") + + def save_to_json(stats, period): with open(f"exports/{period}ly_events.json", "w") as f: json.dump(stats, f) @@ -93,10 +96,10 @@ def handle(self, *args, **options): ("month", timezone.make_aware(datetime(2023, 1, 1), timezone.get_current_timezone())), ("week", timezone.make_aware(datetime(2023, 1, 2), timezone.get_current_timezone())), ): - self.stdout.write(f"Collecting {period}ly events from {search_date}") + logger.info("Collecting %sly events from %s", period, search_date) stats = [] stats = collect_matomo_events(period, search_date, stats) stats = collect_db_events(period, search_date, stats) save_to_json(stats, period) - self.stdout.write(self.style.SUCCESS("That's all, folks!")) + logger.info("That's all, folks!") diff --git a/lacommunaute/stats/management/commands/collect_matomo_forum_stats.py b/lacommunaute/stats/management/commands/collect_matomo_forum_stats.py index 009de28d3..ea41477eb 100644 --- a/lacommunaute/stats/management/commands/collect_matomo_forum_stats.py +++ b/lacommunaute/stats/management/commands/collect_matomo_forum_stats.py @@ -1,4 +1,5 @@ from datetime import date +from logging import getLogger from dateutil.relativedelta import relativedelta from django.core.management.base import BaseCommand @@ -8,6 +9,9 @@ from lacommunaute.utils.matomo import collect_forum_stats_from_matomo_api +logger = getLogger("commands") + + class Command(BaseCommand): help = "Collecter les stats des forum dans matomo, jusqu'au dimanche précédent l'execution" @@ -25,4 +29,4 @@ def handle(self, *args, **options): collect_forum_stats_from_matomo_api(from_date=from_date, to_date=to_date, period=period) - self.stdout.write(self.style.SUCCESS("That's all, folks!")) + logger.info("That's all, folks!") diff --git a/lacommunaute/stats/management/commands/collect_matomo_stats.py b/lacommunaute/stats/management/commands/collect_matomo_stats.py index 75da1a96c..f6c81a7cf 100644 --- a/lacommunaute/stats/management/commands/collect_matomo_stats.py +++ b/lacommunaute/stats/management/commands/collect_matomo_stats.py @@ -1,4 +1,5 @@ from datetime import date +from logging import getLogger from dateutil.relativedelta import relativedelta from django.core.management.base import BaseCommand @@ -7,6 +8,8 @@ from lacommunaute.utils.matomo import collect_stats_from_matomo_api +logger = getLogger("commands") + matomo_stats_names = [ "nb_uniq_visitors", "nb_uniq_visitors_returning", @@ -44,4 +47,4 @@ def handle(self, *args, **options): collect_stats_from_matomo_api(from_date=from_date, to_date=to_date, period=period) - self.stdout.write(self.style.SUCCESS("That's all, folks!")) + logger.info("That's all, folks!") diff --git a/lacommunaute/stats/management/commands/compute_answering_delay.py b/lacommunaute/stats/management/commands/compute_answering_delay.py index 172c147d9..b8f61cbbb 100644 --- a/lacommunaute/stats/management/commands/compute_answering_delay.py +++ b/lacommunaute/stats/management/commands/compute_answering_delay.py @@ -1,7 +1,7 @@ # vincenporte ~ untested code # TODO: refactor it code base and test it - from datetime import timedelta +from logging import getLogger from typing import Dict from django.core.management.base import BaseCommand @@ -12,6 +12,9 @@ from lacommunaute.forum_conversation.models import Post, Topic +logger = getLogger("commands") + + def get_answered_topics_of_a_month(month: int, year: int) -> QuerySet: related_posts = Post.objects.filter(topic=OuterRef("pk")).order_by("created") return Topic.objects.filter( @@ -54,5 +57,5 @@ def handle(self, *args, **options): year = options["year"] values = min_median_max_values(get_answered_topics_of_a_month(month, year), "time_diff_seconds") - self.stdout.write(self.style.SUCCESS(f"{month}/{year}: {values}")) - self.stdout.write(self.style.SUCCESS("That's all, folks!")) + logger.info("%s/%s: %s", month, year, values) + logger.info("That's all, folks!") diff --git a/lacommunaute/stats/tests/tests_management_commands.py b/lacommunaute/stats/tests/tests_management_commands.py index 1457b8bec..c058812a0 100644 --- a/lacommunaute/stats/tests/tests_management_commands.py +++ b/lacommunaute/stats/tests/tests_management_commands.py @@ -3,15 +3,16 @@ from lacommunaute.stats.factories import StatFactory from lacommunaute.stats.management.commands.collect_matomo_stats import get_initial_from_date +from lacommunaute.stats.models import Stat from lacommunaute.surveys.factories import DSPFactory -def test_collect_django_stats(db, capsys): +def test_collect_django_stats(db, caplog): DSPFactory() StatFactory(for_dsp_snapshot=True) call_command("collect_django_stats") - captured = capsys.readouterr() - assert captured.out.strip() == "Collecting DSP stats from 2024-05-18 to yesterday: 1 new stats\nThat's all, folks!" + assert "Collecting DSP stats from 2024-05-18 to yesterday: 1 new stats" in caplog.text + assert Stat.objects.count() == 2 @pytest.mark.parametrize( diff --git a/lacommunaute/stats/views.py b/lacommunaute/stats/views.py index 80d65f6fa..46d035c6d 100644 --- a/lacommunaute/stats/views.py +++ b/lacommunaute/stats/views.py @@ -1,4 +1,4 @@ -import logging +from logging import getLogger from dateutil.relativedelta import relativedelta from django.db.models import Avg, CharField, Count, OuterRef, Subquery, Sum @@ -16,7 +16,7 @@ from lacommunaute.utils.math import percent -logger = logging.getLogger(__name__) +logger = getLogger("lacommunaute") def get_daily_visits_stats(from_date, to_date): diff --git a/lacommunaute/surveys/views.py b/lacommunaute/surveys/views.py index 8682ed51f..ce4cfdaa1 100644 --- a/lacommunaute/surveys/views.py +++ b/lacommunaute/surveys/views.py @@ -1,3 +1,5 @@ +from logging import getLogger + from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse_lazy @@ -9,6 +11,9 @@ from lacommunaute.surveys.models import DSP +logger = getLogger("lacommunaute") + + class DSPCreateView(CreateView): model = DSP template_name = "surveys/dsp_form.html" diff --git a/lacommunaute/users/middleware.py b/lacommunaute/users/middleware.py index c80c31e2d..b9ca2a7cb 100644 --- a/lacommunaute/users/middleware.py +++ b/lacommunaute/users/middleware.py @@ -1,4 +1,4 @@ -import logging +from logging import getLogger from django.utils.deprecation import MiddlewareMixin @@ -6,7 +6,7 @@ from lacommunaute.users.models import EmailLastSeen -logger = logging.getLogger(__name__) +logger = getLogger("lacommunaute") class MarkAsSeenLoggedUserMiddleware(MiddlewareMixin): diff --git a/lacommunaute/users/views.py b/lacommunaute/users/views.py index 7feee8ecb..2659391e2 100644 --- a/lacommunaute/users/views.py +++ b/lacommunaute/users/views.py @@ -1,4 +1,4 @@ -import logging +from logging import getLogger from urllib.parse import urlencode from django.conf import settings @@ -24,7 +24,7 @@ from lacommunaute.utils.urls import clean_next_url -logger = logging.getLogger(__name__) +logger = getLogger("lacommunaute") def send_magic_link(request, user, next_url): diff --git a/lacommunaute/utils/loggers.py b/lacommunaute/utils/loggers.py new file mode 100644 index 000000000..7cafbdde5 --- /dev/null +++ b/lacommunaute/utils/loggers.py @@ -0,0 +1,19 @@ +from json_log_formatter import JSONFormatter + + +class CustomJsonFormatter(JSONFormatter): + def json_record(self, message, extra, record): + extra["logger_name"] = record.name + + if "context" in extra: + context = extra.pop("context") + if hasattr(context, "request"): + extra = extra | { + "view": context.request.resolver_match.view_name, + "kwargs": context.request.resolver_match.kwargs, + "method": context.request.method, + "user_id": context.request.user.id if context.request.user.is_authenticated else None, + "session_key": context.request.session.session_key, + } + + return super().json_record(message, extra, record) diff --git a/pyproject.toml b/pyproject.toml index 84230e213..a302c46cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "django-permissions-policy>=4.24", "langdetect>=1.0.9", "pyjwt>=2.10", + "json-log-formatter>=1.1", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index 79a124ff8..615d44142 100644 --- a/uv.lock +++ b/uv.lock @@ -671,6 +671,12 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/69/3e/dd37e1a7223247e3ef94714abf572415b89c4e121c4af48e9e4c392e2ca0/jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24", size = 75606 } +[[package]] +name = "json-log-formatter" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/34/02eee63c3871b9f3ea340f58d0675dae8d9cc95a8a961f379f5a1b325911/json_log_formatter-1.1.tar.gz", hash = "sha256:fe8fd801c58c1234df86211720921f60149105ef8d1e2a72966bb61da9bed584", size = 5858 } + [[package]] name = "json5" version = "0.10.0" @@ -682,7 +688,7 @@ wheels = [ [[package]] name = "lacommunaute" -version = "2.20.0" +version = "2.21.0" source = { virtual = "." } dependencies = [ { name = "boto3" }, @@ -697,6 +703,7 @@ dependencies = [ { name = "django-storages" }, { name = "django-taggit" }, { name = "httpx" }, + { name = "json-log-formatter" }, { name = "langdetect" }, { name = "psycopg" }, { name = "pyjwt" }, @@ -730,7 +737,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "boto3", specifier = "==1.35.99" }, + { name = "boto3", specifier = "<1.36" }, { name = "django", specifier = ">=5.1" }, { name = "django-compressor", specifier = ">=4.5" }, { name = "django-csp", specifier = ">=3.8" }, @@ -742,6 +749,7 @@ requires-dist = [ { name = "django-storages", specifier = ">=1.14" }, { name = "django-taggit", specifier = ">=6.1" }, { name = "httpx", specifier = ">=0.28" }, + { name = "json-log-formatter", specifier = ">=1.1" }, { name = "langdetect", specifier = ">=1.0.9" }, { name = "psycopg", specifier = ">=3.2" }, { name = "pyjwt", specifier = ">=2.10" },