open-api-framework 0.12.0__tar.gz → 0.13.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/CHANGELOG.rst +43 -0
  2. {open_api_framework-0.12.0/open_api_framework.egg-info → open_api_framework-0.13.0}/PKG-INFO +9 -3
  3. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/README.rst +1 -1
  4. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/conf/base.py +444 -222
  5. {open_api_framework-0.12.0 → open_api_framework-0.13.0/open_api_framework.egg-info}/PKG-INFO +9 -3
  6. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework.egg-info/SOURCES.txt +1 -0
  7. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework.egg-info/requires.txt +9 -1
  8. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/pyproject.toml +12 -3
  9. open_api_framework-0.13.0/tests/test_csp.py +66 -0
  10. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/tests/test_generate_envvar_docs.py +8 -6
  11. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/LICENSE +0 -0
  12. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/MANIFEST.in +0 -0
  13. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/__init__.py +0 -0
  14. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/admin.py +0 -0
  15. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/apps.py +0 -0
  16. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/conf/__init__.py +0 -0
  17. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/conf/utils.py +0 -0
  18. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/context_processors.py +0 -0
  19. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/management/__init__.py +0 -0
  20. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/management/commands/__init__.py +0 -0
  21. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/management/commands/generate_envvar_docs.py +0 -0
  22. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/py.typed +0 -0
  23. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/static/open_api_framework/css/admin.css +0 -0
  24. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/templates/open_api_framework/admin/base_site.html +0 -0
  25. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/templates/open_api_framework/components/environment.html +0 -0
  26. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/templates/open_api_framework/components/version.html +0 -0
  27. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/templates/open_api_framework/env_config.rst +0 -0
  28. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/templatetags/__init__.py +0 -0
  29. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/templatetags/doc_tags.py +0 -0
  30. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/templatetags/open_api_framework.py +0 -0
  31. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework/utils.py +0 -0
  32. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework.egg-info/dependency_links.txt +0 -0
  33. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/open_api_framework.egg-info/top_level.txt +0 -0
  34. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/setup.cfg +0 -0
  35. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/tests/test_admin.py +0 -0
  36. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/tests/test_config_helpers.py +0 -0
  37. {open_api_framework-0.12.0 → open_api_framework-0.13.0}/tests/test_settings.py +0 -0
@@ -1,6 +1,49 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ 0.13.0 (2025-09-18)
5
+ -------------------
6
+
7
+ **New features**
8
+
9
+ * [#178] Add logging envvars:
10
+
11
+ * ``LOG_FORMAT_CONSOLE``
12
+ * ``ENABLE_STRUCTLOG_REQUESTS``
13
+
14
+ * [#178] Specify optional dependency groups:
15
+
16
+ * **structlog**: ``structlog``, ``django-structlog``
17
+ * **structlog-celery**: ``structlog``, ``django-structlog[celery]``
18
+
19
+ * [#178] Add option to configure whether to use structlog or standard logging (default: stdlib logging).
20
+ To enable structlog, downstream projects can do the following in their base settings:
21
+
22
+ .. code-block:: python
23
+
24
+ os.environ["_USE_STRUCTLOG"] = "True"
25
+
26
+ from open_api_framework.conf.base import * # noqa
27
+
28
+ **Bugfixes/QOL**
29
+
30
+ * Fix casting DB_PORT "" to int
31
+
32
+ **Maintenance**
33
+
34
+ * Pin ```django-csp``` to 4.0 or higher.
35
+
36
+ .. warning::
37
+
38
+ The CSP environment variables function the same as with 3.8, but if any changes are made to
39
+ CSP settings for downstream projects (like adding extra values to directives via the old 3.8 settings),
40
+ manual action is needed to make sure it works with 4.0
41
+ (`see documentation <https://django-csp.readthedocs.io/en/latest/configuration.html#migrating-from-django-csp-3-8>`_)
42
+
43
+ **Documentation**
44
+
45
+ * [#171] Add documentation for connection pooling behaviour.
46
+
4
47
  0.12.0 (2025-07-15)
5
48
  -------------------
6
49
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-api-framework
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary: A metapackage for registration components, that bundles the dependencies shared between these components and provides generic settings
5
5
  Author-email: Maykin Media <support@maykinmedia.nl>
6
6
  License: Copyright © Maykin 2024
@@ -74,7 +74,7 @@ Requires-Dist: django-markup>=1.8.1; extra == "markup"
74
74
  Provides-Extra: geo
75
75
  Requires-Dist: djangorestframework-gis>=1.0; extra == "geo"
76
76
  Provides-Extra: csp
77
- Requires-Dist: django-csp>=3.8; extra == "csp"
77
+ Requires-Dist: django-csp>=4.0; extra == "csp"
78
78
  Provides-Extra: commonground
79
79
  Requires-Dist: commonground-api-common>=2.7.0; extra == "commonground"
80
80
  Provides-Extra: inclusions
@@ -85,12 +85,18 @@ Provides-Extra: server
85
85
  Requires-Dist: uwsgi>=2.0.23; extra == "server"
86
86
  Provides-Extra: redis
87
87
  Requires-Dist: django-redis>=5.4.0; extra == "redis"
88
+ Provides-Extra: structlog
89
+ Requires-Dist: structlog>=25.4.0; extra == "structlog"
90
+ Requires-Dist: django-structlog>=9.1.1; extra == "structlog"
91
+ Provides-Extra: structlog-celery
92
+ Requires-Dist: structlog>=25.4.0; extra == "structlog-celery"
93
+ Requires-Dist: django-structlog[celery]>=9.1.1; extra == "structlog-celery"
88
94
  Dynamic: license-file
89
95
 
90
96
  Open API Framework
91
97
  ==================
92
98
 
93
- :Version: 0.12.0
99
+ :Version: 0.13.0
94
100
  :Source: https://github.com/maykinmedia/open-api-framework
95
101
  :Keywords: metapackage, dependencies
96
102
 
@@ -1,7 +1,7 @@
1
1
  Open API Framework
2
2
  ==================
3
3
 
4
- :Version: 0.12.0
4
+ :Version: 0.13.0
5
5
  :Source: https://github.com/maykinmedia/open-api-framework
6
6
  :Keywords: metapackage, dependencies
7
7
 
@@ -7,6 +7,7 @@ from django.urls import reverse_lazy
7
7
 
8
8
  import sentry_sdk
9
9
  from corsheaders.defaults import default_headers as default_cors_headers
10
+ from csp.constants import NONCE, NONE, SELF
10
11
  from log_outgoing_requests.formatters import HttpFormatter
11
12
  from notifications_api_common.settings import * # noqa
12
13
 
@@ -133,7 +134,11 @@ DATABASES = {
133
134
  auto_display_default=False,
134
135
  ),
135
136
  "PORT": config(
136
- "DB_PORT", 5432, group="Database", help_text="port number of the database"
137
+ "DB_PORT",
138
+ 5432,
139
+ group="Database",
140
+ help_text="port number of the database",
141
+ cast=lambda s: s and int(s),
137
142
  ),
138
143
  }
139
144
  }
@@ -543,6 +548,15 @@ if LOG_QUERIES and not DEBUG:
543
548
  RuntimeWarning,
544
549
  )
545
550
 
551
+ LOG_FORMAT_CONSOLE = config(
552
+ "LOG_FORMAT_CONSOLE",
553
+ default="json",
554
+ help_text=(
555
+ "The format for the console logging handler, possible options: ``json``, ``plain_console``."
556
+ ),
557
+ group="Logging",
558
+ )
559
+
546
560
  CELERY_LOGLEVEL = config(
547
561
  "CELERY_LOGLEVEL",
548
562
  default="INFO",
@@ -551,156 +565,373 @@ CELERY_LOGLEVEL = config(
551
565
  group="Celery",
552
566
  )
553
567
 
568
+ _USE_STRUCTLOG = config("_USE_STRUCTLOG", default=False, add_to_docs=False)
569
+
570
+ # XXX: this should be renamed to `LOG_REQUESTS` in the next major release
571
+ ENABLE_STRUCTLOG_REQUESTS = config(
572
+ "ENABLE_STRUCTLOG_REQUESTS",
573
+ default=True,
574
+ help_text=("enable structured logging of requests"),
575
+ group="Logging",
576
+ )
577
+
578
+
554
579
  LOGGING_DIR = Path(BASE_DIR) / "log"
555
580
 
556
- logging_root_handlers = ["console"] if LOG_STDOUT else ["project"]
557
- logging_django_handlers = ["console"] if LOG_STDOUT else ["django"]
581
+ if _USE_STRUCTLOG:
582
+ import structlog
558
583
 
559
- LOGGING = {
560
- "version": 1,
561
- "disable_existing_loggers": False,
562
- "formatters": {
563
- "verbose": {
564
- "format": "%(asctime)s %(levelname)s %(name)s %(module)s %(process)d %(thread)d %(message)s"
565
- },
566
- "timestamped": {"format": "%(asctime)s %(levelname)s %(name)s %(message)s"},
567
- "simple": {"format": "%(levelname)s %(message)s"},
568
- "performance": {"format": "%(asctime)s %(process)d | %(thread)d | %(message)s"},
569
- "db": {"format": "%(asctime)s | %(message)s"},
570
- "outgoing_requests": {"()": HttpFormatter},
571
- },
572
- "filters": {
573
- "require_debug_false": {"()": "django.utils.log.RequireDebugFalse"},
574
- },
575
- "handlers": {
576
- "mail_admins": {
577
- "level": "ERROR",
578
- "filters": ["require_debug_false"],
579
- "class": "django.utils.log.AdminEmailHandler",
580
- },
581
- "null": {"level": "DEBUG", "class": "logging.NullHandler"},
582
- "console": {
583
- "level": LOG_LEVEL,
584
- "class": "logging.StreamHandler",
585
- "formatter": "timestamped",
586
- },
587
- "console_db": {
588
- "level": "DEBUG",
589
- "class": "logging.StreamHandler",
590
- "formatter": "db",
591
- },
592
- "celery_console": {
593
- "level": CELERY_LOGLEVEL,
594
- "class": "logging.StreamHandler",
595
- "formatter": "timestamped",
596
- },
597
- "celery_file": {
598
- "level": CELERY_LOGLEVEL,
599
- "class": "logging.handlers.RotatingFileHandler",
600
- "filename": Path(LOGGING_DIR) / "celery.log",
601
- "formatter": "verbose",
602
- "maxBytes": 1024 * 1024 * 10, # 10 MB
603
- "backupCount": 10,
604
- },
605
- "django": {
606
- "level": LOG_LEVEL,
607
- "class": "logging.handlers.RotatingFileHandler",
608
- "filename": Path(LOGGING_DIR) / "django.log",
609
- "formatter": "verbose",
610
- "maxBytes": 1024 * 1024 * 10, # 10 MB
611
- "backupCount": 10,
612
- },
613
- "project": {
614
- "level": LOG_LEVEL,
615
- "class": "logging.handlers.RotatingFileHandler",
616
- "filename": Path(LOGGING_DIR) / f"{PROJECT_DIRNAME}.log",
617
- "formatter": "verbose",
618
- "maxBytes": 1024 * 1024 * 10, # 10 MB
619
- "backupCount": 10,
620
- },
621
- "performance": {
622
- "level": "INFO",
623
- "class": "logging.handlers.RotatingFileHandler",
624
- "filename": Path(LOGGING_DIR) / "performance.log",
625
- "formatter": "performance",
626
- "maxBytes": 1024 * 1024 * 10, # 10 MB
627
- "backupCount": 10,
628
- },
629
- "requests": {
630
- "level": "DEBUG",
631
- "class": "logging.handlers.RotatingFileHandler",
632
- "filename": Path(LOGGING_DIR) / "requests.log",
633
- "formatter": "timestamped",
634
- "maxBytes": 1024 * 1024 * 10, # 10 MB
635
- "backupCount": 10,
636
- },
637
- "log_outgoing_requests": {
638
- "level": "DEBUG",
639
- "formatter": "outgoing_requests",
640
- "class": "logging.StreamHandler", # to write to stdout
641
- },
642
- "save_outgoing_requests": {
643
- "level": "DEBUG",
644
- # enabling saving to database
645
- "class": "log_outgoing_requests.handlers.DatabaseOutgoingRequestsHandler",
646
- },
647
- },
648
- "loggers": {
649
- "": {
650
- "handlers": logging_root_handlers,
651
- "level": "ERROR",
652
- "propagate": False,
653
- },
654
- PROJECT_DIRNAME: {
655
- "handlers": logging_root_handlers,
656
- "level": LOG_LEVEL,
657
- "propagate": True,
658
- },
659
- "mozilla_django_oidc": {
660
- "handlers": logging_root_handlers,
661
- "level": LOG_LEVEL,
584
+ INSTALLED_APPS += [
585
+ "django_structlog",
586
+ ]
587
+
588
+ if ENABLE_STRUCTLOG_REQUESTS:
589
+ MIDDLEWARE.insert(
590
+ MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware")
591
+ + 1,
592
+ "django_structlog.middlewares.RequestMiddleware",
593
+ )
594
+
595
+ logging_root_handlers = ["console"] if LOG_STDOUT else ["json_file"]
596
+
597
+ LOGGING = {
598
+ "version": 1,
599
+ "disable_existing_loggers": False,
600
+ "formatters": {
601
+ # structlog - foreign_pre_chain handles logs coming from stdlib logging module,
602
+ # while the `structlog.configure` call handles everything coming from structlog.
603
+ # They are mutually exclusive.
604
+ "json": {
605
+ "()": structlog.stdlib.ProcessorFormatter,
606
+ "processor": structlog.processors.JSONRenderer(),
607
+ "foreign_pre_chain": [
608
+ structlog.contextvars.merge_contextvars,
609
+ structlog.processors.TimeStamper(fmt="iso"),
610
+ structlog.stdlib.add_logger_name,
611
+ structlog.stdlib.add_log_level,
612
+ structlog.stdlib.PositionalArgumentsFormatter(),
613
+ ],
614
+ },
615
+ "plain_console": {
616
+ "()": structlog.stdlib.ProcessorFormatter,
617
+ "processor": structlog.dev.ConsoleRenderer(),
618
+ "foreign_pre_chain": [
619
+ structlog.contextvars.merge_contextvars,
620
+ structlog.processors.TimeStamper(fmt="iso"),
621
+ structlog.stdlib.add_logger_name,
622
+ structlog.stdlib.add_log_level,
623
+ structlog.stdlib.PositionalArgumentsFormatter(),
624
+ ],
625
+ },
626
+ "verbose": {
627
+ "format": "%(asctime)s %(levelname)s %(name)s %(module)s %(process)d %(thread)d %(message)s"
628
+ },
629
+ "timestamped": {
630
+ "format": "%(asctime)s %(levelname)s %(name)s %(message)s"
631
+ },
632
+ "simple": {"format": "%(levelname)s %(message)s"},
633
+ "performance": {
634
+ "format": "%(asctime)s %(process)d | %(thread)d | %(message)s"
635
+ },
636
+ "db": {"format": "%(asctime)s | %(message)s"},
637
+ "outgoing_requests": {"()": HttpFormatter},
662
638
  },
663
- f"{PROJECT_DIRNAME}.utils.middleware": {
664
- "handlers": logging_root_handlers,
665
- "level": LOG_LEVEL,
666
- "propagate": False,
639
+ # TODO can be removed?
640
+ "filters": {
641
+ "require_debug_false": {"()": "django.utils.log.RequireDebugFalse"},
667
642
  },
668
- "vng_api_common": {
669
- "handlers": ["console"],
670
- "level": LOG_LEVEL,
671
- "propagate": True,
643
+ "handlers": {
644
+ # TODO can be removed?
645
+ "mail_admins": {
646
+ "level": "ERROR",
647
+ "filters": ["require_debug_false"],
648
+ "class": "django.utils.log.AdminEmailHandler",
649
+ },
650
+ "null": {"level": "DEBUG", "class": "logging.NullHandler"},
651
+ "console": {
652
+ "level": LOG_LEVEL,
653
+ "class": "logging.StreamHandler",
654
+ "formatter": LOG_FORMAT_CONSOLE,
655
+ },
656
+ "console_db": {
657
+ "level": "DEBUG",
658
+ "class": "logging.StreamHandler",
659
+ "formatter": "db",
660
+ },
661
+ # replaces the "django" and "project" handlers - in containerized applications
662
+ # the best practices is to log to stdout (use the console handler).
663
+ "json_file": {
664
+ "level": LOG_LEVEL, # always debug might be better?
665
+ "class": "logging.handlers.RotatingFileHandler",
666
+ "filename": Path(LOGGING_DIR) / "application.jsonl",
667
+ "formatter": "json",
668
+ "maxBytes": 1024 * 1024 * 10, # 10 MB
669
+ "backupCount": 10,
670
+ },
671
+ "performance": {
672
+ "level": "INFO",
673
+ "class": "logging.handlers.RotatingFileHandler",
674
+ "filename": Path(LOGGING_DIR) / "performance.log",
675
+ "formatter": "performance",
676
+ "maxBytes": 1024 * 1024 * 10, # 10 MB
677
+ "backupCount": 10,
678
+ },
679
+ "requests": {
680
+ "level": "DEBUG",
681
+ "class": "logging.handlers.RotatingFileHandler",
682
+ "filename": Path(LOGGING_DIR) / "requests.log",
683
+ "formatter": "timestamped",
684
+ "maxBytes": 1024 * 1024 * 10, # 10 MB
685
+ "backupCount": 10,
686
+ },
687
+ "log_outgoing_requests": {
688
+ "level": "DEBUG",
689
+ "formatter": "outgoing_requests",
690
+ "class": "logging.StreamHandler", # to write to stdout
691
+ },
692
+ "save_outgoing_requests": {
693
+ "level": "DEBUG",
694
+ # enabling saving to database
695
+ "class": "log_outgoing_requests.handlers.DatabaseOutgoingRequestsHandler",
696
+ },
672
697
  },
673
- "django.db.backends": {
674
- "handlers": ["console_db"] if LOG_QUERIES else [],
675
- "level": "DEBUG",
676
- "propagate": False,
698
+ "loggers": {
699
+ "": {
700
+ "handlers": logging_root_handlers,
701
+ "level": "ERROR",
702
+ "propagate": False,
703
+ },
704
+ PROJECT_DIRNAME: {
705
+ "handlers": logging_root_handlers,
706
+ "level": LOG_LEVEL,
707
+ "propagate": False,
708
+ },
709
+ "mozilla_django_oidc": {
710
+ "handlers": logging_root_handlers,
711
+ "level": LOG_LEVEL,
712
+ },
713
+ f"{PROJECT_DIRNAME}.utils.middleware": {
714
+ "handlers": logging_root_handlers,
715
+ "level": LOG_LEVEL,
716
+ "propagate": False,
717
+ },
718
+ "vng_api_common": {
719
+ "handlers": ["console"],
720
+ "level": LOG_LEVEL,
721
+ "propagate": True,
722
+ },
723
+ "django.db.backends": {
724
+ "handlers": ["console_db"] if LOG_QUERIES else [],
725
+ "level": "DEBUG",
726
+ "propagate": False,
727
+ },
728
+ "django.request": {
729
+ "handlers": logging_root_handlers,
730
+ "level": LOG_LEVEL,
731
+ "propagate": False,
732
+ },
733
+ # suppress django.server request logs because those are already emitted by
734
+ # django-structlog middleware
735
+ "django.server": {
736
+ "handlers": ["console"],
737
+ "level": "WARNING" if ENABLE_STRUCTLOG_REQUESTS else "INFO",
738
+ "propagate": False,
739
+ },
740
+ "django.template": {
741
+ "handlers": ["console"],
742
+ "level": "INFO",
743
+ "propagate": False,
744
+ },
745
+ "log_outgoing_requests": {
746
+ "handlers": (
747
+ ["log_outgoing_requests", "save_outgoing_requests"]
748
+ if LOG_REQUESTS
749
+ else []
750
+ ),
751
+ "level": "DEBUG",
752
+ "propagate": True,
753
+ },
754
+ "django_structlog": {
755
+ "handlers": logging_root_handlers,
756
+ "level": "INFO",
757
+ "propagate": False,
758
+ },
677
759
  },
678
- "django.request": {
679
- "handlers": logging_django_handlers,
680
- "level": LOG_LEVEL,
681
- "propagate": True,
760
+ }
761
+
762
+ structlog.configure(
763
+ processors=[
764
+ structlog.contextvars.merge_contextvars,
765
+ structlog.stdlib.filter_by_level,
766
+ structlog.processors.TimeStamper(fmt="iso"),
767
+ structlog.stdlib.add_logger_name,
768
+ structlog.stdlib.add_log_level,
769
+ structlog.stdlib.PositionalArgumentsFormatter(),
770
+ structlog.processors.StackInfoRenderer(),
771
+ structlog.processors.format_exc_info,
772
+ structlog.processors.UnicodeDecoder(),
773
+ structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
774
+ ],
775
+ logger_factory=structlog.stdlib.LoggerFactory(),
776
+ cache_logger_on_first_use=True,
777
+ )
778
+
779
+ # Optional django-structlog settings
780
+ DJANGO_STRUCTLOG_IP_LOGGING_ENABLED = False
781
+ DJANGO_STRUCTLOG_CELERY_ENABLED = True
782
+ else:
783
+ logging_root_handlers = ["console"] if LOG_STDOUT else ["project"]
784
+ logging_django_handlers = ["console"] if LOG_STDOUT else ["django"]
785
+
786
+ LOGGING = {
787
+ "version": 1,
788
+ "disable_existing_loggers": False,
789
+ "formatters": {
790
+ "verbose": {
791
+ "format": "%(asctime)s %(levelname)s %(name)s %(module)s %(process)d %(thread)d %(message)s"
792
+ },
793
+ "timestamped": {
794
+ "format": "%(asctime)s %(levelname)s %(name)s %(message)s"
795
+ },
796
+ "simple": {"format": "%(levelname)s %(message)s"},
797
+ "performance": {
798
+ "format": "%(asctime)s %(process)d | %(thread)d | %(message)s"
799
+ },
800
+ "db": {"format": "%(asctime)s | %(message)s"},
801
+ "outgoing_requests": {"()": HttpFormatter},
682
802
  },
683
- "django.template": {
684
- "handlers": ["console"],
685
- "level": "INFO",
686
- "propagate": False,
803
+ "filters": {
804
+ "require_debug_false": {"()": "django.utils.log.RequireDebugFalse"},
687
805
  },
688
- "log_outgoing_requests": {
689
- "handlers": (
690
- ["log_outgoing_requests", "save_outgoing_requests"]
691
- if LOG_REQUESTS
692
- else []
693
- ),
694
- "level": "DEBUG",
695
- "propagate": True,
806
+ "handlers": {
807
+ "mail_admins": {
808
+ "level": "ERROR",
809
+ "filters": ["require_debug_false"],
810
+ "class": "django.utils.log.AdminEmailHandler",
811
+ },
812
+ "null": {"level": "DEBUG", "class": "logging.NullHandler"},
813
+ "console": {
814
+ "level": LOG_LEVEL,
815
+ "class": "logging.StreamHandler",
816
+ "formatter": "timestamped",
817
+ },
818
+ "console_db": {
819
+ "level": "DEBUG",
820
+ "class": "logging.StreamHandler",
821
+ "formatter": "db",
822
+ },
823
+ "celery_console": {
824
+ "level": CELERY_LOGLEVEL,
825
+ "class": "logging.StreamHandler",
826
+ "formatter": "timestamped",
827
+ },
828
+ "celery_file": {
829
+ "level": CELERY_LOGLEVEL,
830
+ "class": "logging.handlers.RotatingFileHandler",
831
+ "filename": Path(LOGGING_DIR) / "celery.log",
832
+ "formatter": "verbose",
833
+ "maxBytes": 1024 * 1024 * 10, # 10 MB
834
+ "backupCount": 10,
835
+ },
836
+ "django": {
837
+ "level": LOG_LEVEL,
838
+ "class": "logging.handlers.RotatingFileHandler",
839
+ "filename": Path(LOGGING_DIR) / "django.log",
840
+ "formatter": "verbose",
841
+ "maxBytes": 1024 * 1024 * 10, # 10 MB
842
+ "backupCount": 10,
843
+ },
844
+ "project": {
845
+ "level": LOG_LEVEL,
846
+ "class": "logging.handlers.RotatingFileHandler",
847
+ "filename": Path(LOGGING_DIR) / f"{PROJECT_DIRNAME}.log",
848
+ "formatter": "verbose",
849
+ "maxBytes": 1024 * 1024 * 10, # 10 MB
850
+ "backupCount": 10,
851
+ },
852
+ "performance": {
853
+ "level": "INFO",
854
+ "class": "logging.handlers.RotatingFileHandler",
855
+ "filename": Path(LOGGING_DIR) / "performance.log",
856
+ "formatter": "performance",
857
+ "maxBytes": 1024 * 1024 * 10, # 10 MB
858
+ "backupCount": 10,
859
+ },
860
+ "requests": {
861
+ "level": "DEBUG",
862
+ "class": "logging.handlers.RotatingFileHandler",
863
+ "filename": Path(LOGGING_DIR) / "requests.log",
864
+ "formatter": "timestamped",
865
+ "maxBytes": 1024 * 1024 * 10, # 10 MB
866
+ "backupCount": 10,
867
+ },
868
+ "log_outgoing_requests": {
869
+ "level": "DEBUG",
870
+ "formatter": "outgoing_requests",
871
+ "class": "logging.StreamHandler", # to write to stdout
872
+ },
873
+ "save_outgoing_requests": {
874
+ "level": "DEBUG",
875
+ # enabling saving to database
876
+ "class": "log_outgoing_requests.handlers.DatabaseOutgoingRequestsHandler",
877
+ },
696
878
  },
697
- "celery": {
698
- "handlers": ["celery_console"] if LOG_STDOUT else ["celery_file"],
699
- "level": CELERY_LOGLEVEL,
700
- "propagate": True,
879
+ "loggers": {
880
+ "": {
881
+ "handlers": logging_root_handlers,
882
+ "level": "ERROR",
883
+ "propagate": False,
884
+ },
885
+ PROJECT_DIRNAME: {
886
+ "handlers": logging_root_handlers,
887
+ "level": LOG_LEVEL,
888
+ "propagate": True,
889
+ },
890
+ "mozilla_django_oidc": {
891
+ "handlers": logging_root_handlers,
892
+ "level": LOG_LEVEL,
893
+ },
894
+ f"{PROJECT_DIRNAME}.utils.middleware": {
895
+ "handlers": logging_root_handlers,
896
+ "level": LOG_LEVEL,
897
+ "propagate": False,
898
+ },
899
+ "vng_api_common": {
900
+ "handlers": ["console"],
901
+ "level": LOG_LEVEL,
902
+ "propagate": True,
903
+ },
904
+ "django.db.backends": {
905
+ "handlers": ["console_db"] if LOG_QUERIES else [],
906
+ "level": "DEBUG",
907
+ "propagate": False,
908
+ },
909
+ "django.request": {
910
+ "handlers": logging_django_handlers,
911
+ "level": LOG_LEVEL,
912
+ "propagate": True,
913
+ },
914
+ "django.template": {
915
+ "handlers": ["console"],
916
+ "level": "INFO",
917
+ "propagate": False,
918
+ },
919
+ "log_outgoing_requests": {
920
+ "handlers": (
921
+ ["log_outgoing_requests", "save_outgoing_requests"]
922
+ if LOG_REQUESTS
923
+ else []
924
+ ),
925
+ "level": "DEBUG",
926
+ "propagate": True,
927
+ },
928
+ "celery": {
929
+ "handlers": ["celery_console"] if LOG_STDOUT else ["celery_file"],
930
+ "level": CELERY_LOGLEVEL,
931
+ "propagate": True,
932
+ },
701
933
  },
702
- },
703
- }
934
+ }
704
935
 
705
936
  #
706
937
  # AUTH settings - user accounts, passwords, backends...
@@ -1153,95 +1384,86 @@ LOG_OUTGOING_REQUESTS_MAX_AGE = config(
1153
1384
  #
1154
1385
  # NOTE: make sure values are a tuple or list, and to quote special values like 'self'
1155
1386
 
1156
- # ideally we'd use BASE_URI but it'd have to be lazy or cause issues
1157
- CSP_DEFAULT_SRC = ["'self'"] + config(
1158
- "CSP_EXTRA_DEFAULT_SRC",
1159
- default=[],
1160
- split=True,
1161
- group="Content Security Policy",
1162
- help_text="Extra default source URLs for CSP other than ``self``. "
1163
- "Used for ``img-src``, ``style-src`` and ``script-src``",
1164
- )
1165
-
1166
-
1167
- CSP_REPORT_URI = config(
1168
- "CSP_REPORT_URI",
1169
- None,
1170
- group="Content Security Policy",
1171
- help_text="URI of the``report-uri`` directive.",
1172
- )
1173
- CSP_REPORT_PERCENTAGE = config(
1174
- "CSP_REPORT_PERCENTAGE",
1175
- 0,
1176
- group="Content Security Policy",
1177
- help_text="Percentage of requests that get the ``report-uri`` directive.",
1178
- ) # float between 0 and 1
1179
-
1180
- CSP_FORM_ACTION = (
1181
- config(
1182
- "CSP_FORM_ACTION",
1183
- default=["\"'self'\""]
1184
- + config(
1185
- "CSP_EXTRA_FORM_ACTION",
1186
- default=[],
1187
- split=True,
1188
- group="Content Security Policy",
1189
- help_text="Add additional ``form-action`` source to the default ",
1190
- ),
1191
- split=True,
1192
- group="Content Security Policy",
1193
- help_text="Override the default ``form-action`` source",
1194
- )
1195
- + CORS_ALLOWED_ORIGINS
1196
- )
1197
1387
 
1198
- CSP_IMG_SRC = (
1199
- CSP_DEFAULT_SRC
1200
- + ["data:", "cdn.redoc.ly"] # used by DRF spectacular
1201
- + config(
1202
- "CSP_EXTRA_IMG_SRC",
1388
+ def get_content_security_policy():
1389
+ # ideally we'd use BASE_URI but it'd have to be lazy or cause issues
1390
+ csp_default_src = [SELF] + config(
1391
+ "CSP_EXTRA_DEFAULT_SRC",
1203
1392
  default=[],
1204
1393
  split=True,
1205
1394
  group="Content Security Policy",
1206
- help_text="Extra ``img-src`` sources for CSP other than ``CSP_DEFAULT_SRC``.",
1395
+ help_text="Extra default source URLs for CSP other than ``self``. Used for ``img-src``, ``style-src`` and ``script-src``.",
1207
1396
  )
1208
- )
1209
-
1210
- # affects <object> and <embed> tags, block everything by default but allow deploy-time
1211
- # overrides.
1212
- CSP_OBJECT_SRC = config(
1213
- "CSP_OBJECT_SRC",
1214
- default=["\"'none'\""],
1215
- split=True,
1216
- group="Content Security Policy",
1217
- help_text="``object-src`` urls",
1218
- )
1219
-
1220
- # we must include this explicitly, otherwise the style-src only includes the nonce because
1221
- # of CSP_INCLUDE_NONCE_IN
1222
- CSP_STYLE_SRC = CSP_DEFAULT_SRC + [
1223
- "'unsafe-inline'",
1224
- "fonts.googleapis.com",
1225
- ] # used by DRF spectacular
1226
- CSP_SCRIPT_SRC = CSP_DEFAULT_SRC + ["'unsafe-inline'"]
1227
- CSP_FONT_SRC = ("'self'", "fonts.gstatic.com")
1228
- CSP_WORKER_SRC = ("'self'", "blob:")
1229
-
1230
- # firefox does not get the nonce from default-src, see
1231
- # https://stackoverflow.com/a/63376012
1232
- CSP_INCLUDE_NONCE_IN = ["style-src", "script-src"]
1233
-
1234
- # directives that don't fallback to default-src
1235
- CSP_BASE_URI = ["'self'"]
1236
-
1237
- # Frame directives do not fall back to default-src
1238
- CSP_FRAME_ANCESTORS = ["'none'"] # equivalent to X-Frame-Options: deny
1239
- CSP_FRAME_SRC = ["'self'"]
1240
- # CSP_NAVIGATE_TO = ["'self'"] # this will break all outgoing links etc # too much & tricky, see note on MDN
1241
- # CSP_SANDBOX # too much
1397
+ return {
1398
+ "DIRECTIVES": {
1399
+ "default-src": [SELF]
1400
+ + config(
1401
+ "CSP_EXTRA_DEFAULT_SRC",
1402
+ default=[],
1403
+ split=True,
1404
+ group="Content Security Policy",
1405
+ help_text="Extra default source URLs for CSP other than ``self``. Used for ``img-src``, ``style-src`` and ``script-src``.",
1406
+ ),
1407
+ "form-action": config(
1408
+ "CSP_FORM_ACTION",
1409
+ default=["\"'self'\""]
1410
+ + config(
1411
+ "CSP_EXTRA_FORM_ACTION",
1412
+ default=[],
1413
+ split=True,
1414
+ group="Content Security Policy",
1415
+ help_text="Additional `form-action` sources.",
1416
+ ),
1417
+ split=True,
1418
+ group="Content Security Policy",
1419
+ help_text="Override the default `form-action` sources.",
1420
+ )
1421
+ + CORS_ALLOWED_ORIGINS,
1422
+ "img-src": csp_default_src
1423
+ + ["data:", "cdn.redoc.ly"]
1424
+ + config(
1425
+ "CSP_EXTRA_IMG_SRC",
1426
+ default=[],
1427
+ split=True,
1428
+ group="Content Security Policy",
1429
+ help_text="Extra `img-src` sources.",
1430
+ ),
1431
+ "object-src": config(
1432
+ "CSP_OBJECT_SRC",
1433
+ default=["\"'none'\""],
1434
+ split=True,
1435
+ group="Content Security Policy",
1436
+ help_text="`object-src` sources.",
1437
+ ),
1438
+ "style-src": csp_default_src
1439
+ + [NONCE, "'unsafe-inline'", "fonts.googleapis.com"],
1440
+ "script-src": csp_default_src + [NONCE, "'unsafe-inline'"],
1441
+ "font-src": [SELF, "fonts.gstatic.com"],
1442
+ "worker-src": [SELF, "blob:"],
1443
+ "base-uri": [SELF],
1444
+ "frame-ancestors": [NONE],
1445
+ "frame-src": [SELF],
1446
+ "upgrade-insecure-requests": False, # Enable only in production
1447
+ "report-uri": config(
1448
+ "CSP_REPORT_URI",
1449
+ None,
1450
+ group="Content Security Policy",
1451
+ help_text="URI for CSP report-uri directive.",
1452
+ ),
1453
+ },
1454
+ # Envvar used for django-csp==3.8 was a float between 0 and 1, while django-csp==4.0
1455
+ # expects a percentage (between 0 and 100)
1456
+ "REPORT_PERCENTAGE": config(
1457
+ "CSP_REPORT_PERCENTAGE",
1458
+ 0.0,
1459
+ group="Content Security Policy",
1460
+ help_text="Fraction (between 0 and 1) of requests to include report-uri directive.",
1461
+ )
1462
+ * 100,
1463
+ }
1242
1464
 
1243
- CSP_UPGRADE_INSECURE_REQUESTS = False # TODO enable on production?
1244
1465
 
1466
+ CONTENT_SECURITY_POLICY = get_content_security_policy()
1245
1467
 
1246
1468
  #
1247
1469
  # Django Solo
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-api-framework
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary: A metapackage for registration components, that bundles the dependencies shared between these components and provides generic settings
5
5
  Author-email: Maykin Media <support@maykinmedia.nl>
6
6
  License: Copyright © Maykin 2024
@@ -74,7 +74,7 @@ Requires-Dist: django-markup>=1.8.1; extra == "markup"
74
74
  Provides-Extra: geo
75
75
  Requires-Dist: djangorestframework-gis>=1.0; extra == "geo"
76
76
  Provides-Extra: csp
77
- Requires-Dist: django-csp>=3.8; extra == "csp"
77
+ Requires-Dist: django-csp>=4.0; extra == "csp"
78
78
  Provides-Extra: commonground
79
79
  Requires-Dist: commonground-api-common>=2.7.0; extra == "commonground"
80
80
  Provides-Extra: inclusions
@@ -85,12 +85,18 @@ Provides-Extra: server
85
85
  Requires-Dist: uwsgi>=2.0.23; extra == "server"
86
86
  Provides-Extra: redis
87
87
  Requires-Dist: django-redis>=5.4.0; extra == "redis"
88
+ Provides-Extra: structlog
89
+ Requires-Dist: structlog>=25.4.0; extra == "structlog"
90
+ Requires-Dist: django-structlog>=9.1.1; extra == "structlog"
91
+ Provides-Extra: structlog-celery
92
+ Requires-Dist: structlog>=25.4.0; extra == "structlog-celery"
93
+ Requires-Dist: django-structlog[celery]>=9.1.1; extra == "structlog-celery"
88
94
  Dynamic: license-file
89
95
 
90
96
  Open API Framework
91
97
  ==================
92
98
 
93
- :Version: 0.12.0
99
+ :Version: 0.13.0
94
100
  :Source: https://github.com/maykinmedia/open-api-framework
95
101
  :Keywords: metapackage, dependencies
96
102
 
@@ -30,5 +30,6 @@ open_api_framework/templatetags/doc_tags.py
30
30
  open_api_framework/templatetags/open_api_framework.py
31
31
  tests/test_admin.py
32
32
  tests/test_config_helpers.py
33
+ tests/test_csp.py
33
34
  tests/test_generate_envvar_docs.py
34
35
  tests/test_settings.py
@@ -32,7 +32,7 @@ django-cors-headers>=4.4.0
32
32
  pytest-cov
33
33
 
34
34
  [csp]
35
- django-csp>=3.8
35
+ django-csp>=4.0
36
36
 
37
37
  [docs]
38
38
  sphinx
@@ -60,6 +60,14 @@ bleach>=6.1.0
60
60
  [server]
61
61
  uwsgi>=2.0.23
62
62
 
63
+ [structlog]
64
+ structlog>=25.4.0
65
+ django-structlog>=9.1.1
66
+
67
+ [structlog-celery]
68
+ structlog>=25.4.0
69
+ django-structlog[celery]>=9.1.1
70
+
63
71
  [tests]
64
72
  pytest
65
73
  pytest-django
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "open-api-framework"
7
- version = "0.12.0"
7
+ version = "0.13.0"
8
8
  description = "A metapackage for registration components, that bundles the dependencies shared between these components and provides generic settings"
9
9
  authors = [
10
10
  {name = "Maykin Media", email = "support@maykinmedia.nl"}
@@ -88,7 +88,7 @@ geo = [
88
88
  "djangorestframework-gis>=1.0"
89
89
  ]
90
90
  csp = [
91
- "django-csp>=3.8"
91
+ "django-csp>=4.0"
92
92
  ]
93
93
  commonground = [
94
94
  "commonground-api-common>=2.7.0"
@@ -105,6 +105,15 @@ server = [
105
105
  redis = [
106
106
  "django-redis>=5.4.0"
107
107
  ]
108
+ structlog = [
109
+ "structlog>=25.4.0",
110
+ "django-structlog>=9.1.1"
111
+ ]
112
+ structlog-celery = [
113
+ "structlog>=25.4.0",
114
+ "django-structlog[celery]>=9.1.1"
115
+ ]
116
+
108
117
 
109
118
  [tool.setuptools.packages.find]
110
119
  include = ["open_api_framework*"]
@@ -120,7 +129,7 @@ env = [
120
129
  ]
121
130
 
122
131
  [tool.bumpversion]
123
- current_version = "0.12.0"
132
+ current_version = "0.13.0"
124
133
  files = [
125
134
  {filename = "pyproject.toml"},
126
135
  {filename = "README.rst"},
@@ -0,0 +1,66 @@
1
+ import testapp.settings
2
+
3
+
4
+ def test_csp_default(client):
5
+ response = client.get("/dummy/")
6
+
7
+ csp_header = response.headers.get("Content-Security-Policy")
8
+
9
+ assert csp_header is not None
10
+ assert "default-src 'self'" in csp_header
11
+ assert "base-uri 'self'" in csp_header
12
+ assert "object-src 'none'" in csp_header
13
+ assert "worker-src 'self' blob:" in csp_header
14
+ assert "style-src 'self' 'unsafe-inline' fonts.googleapis.com" in csp_header
15
+ assert "frame-ancestors 'none'" in csp_header
16
+ assert "script-src 'self' 'unsafe-inline'" in csp_header
17
+ assert "font-src 'self' fonts.gstatic.com" in csp_header
18
+ assert "img-src 'self' data: cdn.redoc.ly" in csp_header
19
+ assert "form-action 'self'" in csp_header
20
+ assert "frame-src 'self'" in csp_header
21
+
22
+
23
+ def test_csp_with_envvars_configured(client, monkeypatch, settings):
24
+ monkeypatch.setenv("CSP_EXTRA_DEFAULT_SRC", "https://default.local")
25
+ monkeypatch.setenv("CSP_EXTRA_FORM_ACTION", "https://form-action.local")
26
+ monkeypatch.setenv("CSP_EXTRA_IMG_SRC", "https://img.local")
27
+ monkeypatch.setenv("CSP_OBJECT_SRC", "https://object.local")
28
+ monkeypatch.setenv("CSP_REPORT_URI", "https://report-uri.local")
29
+ monkeypatch.setenv("CSP_REPORT_PERCENTAGE", "0.5")
30
+
31
+ # Unfortunately it's not possible to reload settings with envvars, so we manually
32
+ # recompute the CSP setting instead
33
+ settings.CONTENT_SECURITY_POLICY = testapp.settings.get_content_security_policy()
34
+ try:
35
+ response = client.get("/dummy/")
36
+
37
+ csp_header = response.headers.get("Content-Security-Policy")
38
+
39
+ # Number between 0 and 1 should be converted to number between 0 and 100
40
+ assert settings.CONTENT_SECURITY_POLICY["REPORT_PERCENTAGE"] == 50
41
+
42
+ assert csp_header is not None
43
+ assert "default-src 'self' https://default.local" in csp_header
44
+ assert "base-uri 'self'" in csp_header
45
+ assert "object-src https://object.local" in csp_header
46
+ assert "worker-src 'self' blob:" in csp_header
47
+ assert (
48
+ "style-src 'self' https://default.local 'unsafe-inline' fonts.googleapis.com"
49
+ in csp_header
50
+ )
51
+ assert "frame-ancestors 'none'" in csp_header
52
+ assert "script-src 'self' https://default.local 'unsafe-inline'" in csp_header
53
+ assert "font-src 'self' fonts.gstatic.com" in csp_header
54
+ assert (
55
+ "img-src 'self' https://default.local data: cdn.redoc.ly https://img.local"
56
+ in csp_header
57
+ )
58
+ assert "form-action 'self'" in csp_header
59
+ assert "frame-src 'self'" in csp_header
60
+ finally:
61
+ # Manually revert the CSP back to the default, to make sure the envvar docs
62
+ # do not use the envvar values used in this test
63
+ monkeypatch.undo()
64
+ settings.CONTENT_SECURITY_POLICY = (
65
+ testapp.settings.get_content_security_policy()
66
+ )
@@ -52,6 +52,8 @@ Logging
52
52
  * ``LOG_LEVEL``: control the verbosity of logging output. Available values are ``CRITICAL``, ``ERROR``, ``WARNING``, ``INFO`` and ``DEBUG``. Defaults to: ``INFO``.
53
53
  * ``LOG_QUERIES``: enable (query) logging at the database backend level. Note that you must also set ``DEBUG=1``, which should be done very sparingly!. Defaults to: ``False``.
54
54
  * ``LOG_REQUESTS``: enable logging of the outgoing requests. This must be enabled along with `LOG_OUTGOING_REQUESTS_DB_SAVE` to save outgoing request logs in the database. Defaults to: ``False``.
55
+ * ``LOG_FORMAT_CONSOLE``: The format for the console logging handler, possible options: ``json``, ``plain_console``. Defaults to: ``json``.
56
+ * ``ENABLE_STRUCTLOG_REQUESTS``: enable structured logging of requests. Defaults to: ``True``.
55
57
  * ``LOG_OUTGOING_REQUESTS_EMIT_BODY``: Whether or not outgoing request bodies should be logged. Defaults to: ``True``.
56
58
  * ``LOG_OUTGOING_REQUESTS_DB_SAVE``: Whether or not outgoing request logs should be saved to the database. Defaults to: ``False``.
57
59
  * ``LOG_OUTGOING_REQUESTS_DB_SAVE_BODY``: Whether or not outgoing request bodies should be saved to the database. Defaults to: ``True``.
@@ -87,12 +89,12 @@ Content Security Policy
87
89
  -----------------------
88
90
 
89
91
  * ``CSP_EXTRA_DEFAULT_SRC``: Extra default source URLs for CSP other than ``self``. Used for ``img-src``, ``style-src`` and ``script-src``. Defaults to: ``[]``.
90
- * ``CSP_REPORT_URI``: URI of the``report-uri`` directive. Defaults to: ``None``.
91
- * ``CSP_REPORT_PERCENTAGE``: Percentage of requests that get the ``report-uri`` directive. Defaults to: ``0``.
92
- * ``CSP_EXTRA_FORM_ACTION``: Add additional ``form-action`` source to the default . Defaults to: ``[]``.
93
- * ``CSP_FORM_ACTION``: Override the default ``form-action`` source. Defaults to: ``['"\\'self\\'"']``.
94
- * ``CSP_EXTRA_IMG_SRC``: Extra ``img-src`` sources for CSP other than ``CSP_DEFAULT_SRC``. Defaults to: ``[]``.
95
- * ``CSP_OBJECT_SRC``: ``object-src`` urls. Defaults to: ``['"\\'none\\'"']``.
92
+ * ``CSP_EXTRA_FORM_ACTION``: Additional `form-action` sources. Defaults to: ``[]``.
93
+ * ``CSP_FORM_ACTION``: Override the default `form-action` sources. Defaults to: ``['"\\'self\\'"']``.
94
+ * ``CSP_EXTRA_IMG_SRC``: Extra `img-src` sources. Defaults to: ``[]``.
95
+ * ``CSP_OBJECT_SRC``: `object-src` sources. Defaults to: ``['"\\'none\\'"']``.
96
+ * ``CSP_REPORT_URI``: URI for CSP report-uri directive. Defaults to: ``None``.
97
+ * ``CSP_REPORT_PERCENTAGE``: Fraction (between 0 and 1) of requests to include report-uri directive. Defaults to: ``0.0``.
96
98
 
97
99
 
98
100
  Optional