open-api-framework 0.2.0__tar.gz → 0.4.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 (20) hide show
  1. open_api_framework-0.4.0/CHANGELOG.rst +34 -0
  2. {open_api_framework-0.2.0/open_api_framework.egg-info → open_api_framework-0.4.0}/PKG-INFO +7 -5
  3. {open_api_framework-0.2.0 → open_api_framework-0.4.0}/README.rst +2 -2
  4. open_api_framework-0.4.0/open_api_framework/conf/base.py +651 -0
  5. open_api_framework-0.4.0/open_api_framework/conf/utils.py +63 -0
  6. open_api_framework-0.4.0/open_api_framework/py.typed +0 -0
  7. {open_api_framework-0.2.0 → open_api_framework-0.4.0/open_api_framework.egg-info}/PKG-INFO +7 -5
  8. {open_api_framework-0.2.0 → open_api_framework-0.4.0}/open_api_framework.egg-info/SOURCES.txt +5 -1
  9. {open_api_framework-0.2.0 → open_api_framework-0.4.0}/open_api_framework.egg-info/requires.txt +2 -0
  10. {open_api_framework-0.2.0 → open_api_framework-0.4.0}/pyproject.toml +6 -4
  11. {open_api_framework-0.2.0 → open_api_framework-0.4.0}/setup.cfg +1 -1
  12. open_api_framework-0.4.0/tests/test_config_helpers.py +13 -0
  13. open_api_framework-0.2.0/tests/test_dummy.py +0 -2
  14. {open_api_framework-0.2.0 → open_api_framework-0.4.0}/LICENSE +0 -0
  15. {open_api_framework-0.2.0 → open_api_framework-0.4.0}/MANIFEST.in +0 -0
  16. {open_api_framework-0.2.0 → open_api_framework-0.4.0}/open_api_framework/__init__.py +0 -0
  17. {open_api_framework-0.2.0 → open_api_framework-0.4.0}/open_api_framework/apps.py +0 -0
  18. /open_api_framework-0.2.0/open_api_framework/py.typed → /open_api_framework-0.4.0/open_api_framework/conf/__init__.py +0 -0
  19. {open_api_framework-0.2.0 → open_api_framework-0.4.0}/open_api_framework.egg-info/dependency_links.txt +0 -0
  20. {open_api_framework-0.2.0 → open_api_framework-0.4.0}/open_api_framework.egg-info/top_level.txt +0 -0
@@ -0,0 +1,34 @@
1
+ Changelog
2
+ =========
3
+
4
+ 0.4.0 (2024-06-06)
5
+ ------------------
6
+
7
+ **New features**
8
+
9
+ * Add django-setup-configuration to deps
10
+ * Add ELASTIC_APM_TRANSACTION_SAMPLE_RATE
11
+
12
+ 0.3.0 (2024-05-17)
13
+ ------------------
14
+
15
+ **New features**
16
+
17
+ * [#14] Add django-log-outgoing-requests to deps
18
+ * [open-zaak/open-zaak#1629] Add generic base settings file
19
+
20
+
21
+ 0.2.0 (2024-03-22)
22
+ ------------------
23
+
24
+ **New features**
25
+
26
+ * Add support for python 3.10
27
+ * Upgrade to Django 4.2
28
+ * Add maykin-2fa
29
+
30
+
31
+ 0.1.0 (2024-01-30)
32
+ ------------------
33
+
34
+ * Initial release as a metapackage to pin several dependencies
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: open_api_framework
3
- Version: 0.2.0
4
- Summary: TODO
3
+ Version: 0.4.0
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 2024 Maykin Media
7
7
 
@@ -15,7 +15,7 @@ Project-URL: Homepage, https://github.com/maykinmedia/open_api_framework
15
15
  Project-URL: Documentation, http://open_api_framework.readthedocs.io/en/latest/
16
16
  Project-URL: Bug Tracker, https://github.com/maykinmedia/open_api_framework/issues
17
17
  Project-URL: Source Code, https://github.com/maykinmedia/open_api_framework
18
- Keywords: TODO
18
+ Keywords: metapackage,dependencies,settings
19
19
  Classifier: Development Status :: 3 - Alpha
20
20
  Classifier: Framework :: Django
21
21
  Classifier: Framework :: Django :: 4.2
@@ -35,6 +35,7 @@ Requires-Dist: django-axes>=6.3.0
35
35
  Requires-Dist: django-cors-headers>=4.3.1
36
36
  Requires-Dist: django-markup>=1.8.1
37
37
  Requires-Dist: django-jsonform>=2.21.4
38
+ Requires-Dist: django-log-outgoing-requests>=0.6.1
38
39
  Requires-Dist: django-admin-index>=3.1.0
39
40
  Requires-Dist: django-redis>=5.4.0
40
41
  Requires-Dist: djangorestframework>=3.12.4
@@ -56,6 +57,7 @@ Requires-Dist: elastic-apm>=6.20.0
56
57
  Requires-Dist: celery>=5.2.7
57
58
  Requires-Dist: flower>=2.0.1
58
59
  Requires-Dist: maykin-2fa>=1.0.0
60
+ Requires-Dist: django-setup-configuration>=0.1.0
59
61
  Provides-Extra: tests
60
62
  Requires-Dist: pytest; extra == "tests"
61
63
  Requires-Dist: pytest-django; extra == "tests"
@@ -77,7 +79,7 @@ Requires-Dist: twine; extra == "release"
77
79
  Welcome to open-api-framework's documentation!
78
80
  =================================================
79
81
 
80
- :Version: 0.2.0
82
+ :Version: 0.4.0
81
83
  :Source: https://github.com/maykinmedia/open-api-framework
82
84
  :Keywords: metapackage, dependencies
83
85
 
@@ -116,7 +118,7 @@ Install
116
118
  Local development
117
119
  =================
118
120
 
119
- To install and develop the library locally, use::
121
+ To install and develop the library locally, use:
120
122
 
121
123
  .. code-block:: bash
122
124
 
@@ -3,7 +3,7 @@
3
3
  Welcome to open-api-framework's documentation!
4
4
  =================================================
5
5
 
6
- :Version: 0.2.0
6
+ :Version: 0.4.0
7
7
  :Source: https://github.com/maykinmedia/open-api-framework
8
8
  :Keywords: metapackage, dependencies
9
9
 
@@ -42,7 +42,7 @@ Install
42
42
  Local development
43
43
  =================
44
44
 
45
- To install and develop the library locally, use::
45
+ To install and develop the library locally, use:
46
46
 
47
47
  .. code-block:: bash
48
48
 
@@ -0,0 +1,651 @@
1
+ import os
2
+ import warnings
3
+ from pathlib import Path
4
+ from typing import Callable
5
+
6
+ from django.urls import reverse_lazy
7
+
8
+ import sentry_sdk
9
+ from corsheaders.defaults import default_headers as default_cors_headers
10
+ from log_outgoing_requests.formatters import HttpFormatter
11
+ from notifications_api_common.settings import * # noqa
12
+
13
+ from .utils import (
14
+ config,
15
+ get_django_project_dir,
16
+ get_project_dirname,
17
+ get_sentry_integrations,
18
+ strip_protocol_from_origin,
19
+ )
20
+
21
+ PROJECT_DIRNAME = get_project_dirname()
22
+
23
+ # Build paths inside the project, so further paths can be defined relative to
24
+ # the code root.
25
+ DJANGO_PROJECT_DIR = get_django_project_dir()
26
+ BASE_DIR = Path(DJANGO_PROJECT_DIR).resolve().parents[1]
27
+
28
+
29
+ #
30
+ # Core Django settings
31
+ #
32
+ SITE_ID = config("SITE_ID", default=1)
33
+
34
+ # SECURITY WARNING: keep the secret key used in production secret!
35
+ SECRET_KEY = config("SECRET_KEY")
36
+
37
+ # NEVER run with DEBUG=True in production-like environments
38
+ DEBUG = config("DEBUG", default=False)
39
+
40
+ # = domains we're running on
41
+ ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="", split=True)
42
+ USE_X_FORWARDED_HOST = config("USE_X_FORWARDED_HOST", default=False)
43
+
44
+ IS_HTTPS = config("IS_HTTPS", default=not DEBUG)
45
+
46
+ # Internationalization
47
+ # https://docs.djangoproject.com/en/2.0/topics/i18n/
48
+
49
+ LANGUAGE_CODE = "nl-nl"
50
+
51
+ TIME_ZONE = "UTC" # note: this *may* affect the output of DRF datetimes
52
+
53
+ USE_I18N = True
54
+
55
+ USE_L10N = True
56
+
57
+ USE_TZ = True
58
+
59
+ USE_THOUSAND_SEPARATOR = True
60
+
61
+ #
62
+ # DATABASE and CACHING setup
63
+ #
64
+
65
+ DATABASES = {
66
+ "default": {
67
+ "ENGINE": "django.db.backends.postgresql",
68
+ "NAME": config("DB_NAME", PROJECT_DIRNAME),
69
+ "USER": config("DB_USER", PROJECT_DIRNAME),
70
+ "PASSWORD": config("DB_PASSWORD", PROJECT_DIRNAME),
71
+ "HOST": config("DB_HOST", "localhost"),
72
+ "PORT": config("DB_PORT", 5432),
73
+ }
74
+ }
75
+
76
+ # keep the current schema for now and deal with migrating to BigAutoField later, see
77
+ # https://docs.djangoproject.com/en/4.0/ref/settings/#std:setting-DEFAULT_AUTO_FIELD
78
+ DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
79
+
80
+ CACHES = {
81
+ "default": {
82
+ "BACKEND": "django_redis.cache.RedisCache",
83
+ "LOCATION": f"redis://{config('CACHE_DEFAULT', 'localhost:6379/0')}",
84
+ "OPTIONS": {
85
+ "CLIENT_CLASS": "django_redis.client.DefaultClient",
86
+ "IGNORE_EXCEPTIONS": True,
87
+ },
88
+ },
89
+ "axes": {
90
+ "BACKEND": "django_redis.cache.RedisCache",
91
+ "LOCATION": f"redis://{config('CACHE_AXES', 'localhost:6379/0')}",
92
+ "OPTIONS": {
93
+ "CLIENT_CLASS": "django_redis.client.DefaultClient",
94
+ "IGNORE_EXCEPTIONS": True,
95
+ },
96
+ },
97
+ "oidc": {
98
+ "BACKEND": "django_redis.cache.RedisCache",
99
+ "LOCATION": f"redis://{config('CACHE_DEFAULT', 'localhost:6379/0')}",
100
+ "OPTIONS": {
101
+ "CLIENT_CLASS": "django_redis.client.DefaultClient",
102
+ "IGNORE_EXCEPTIONS": True,
103
+ },
104
+ },
105
+ }
106
+
107
+ #
108
+ # APPLICATIONS enabled for this project
109
+ #
110
+ INSTALLED_APPS = [
111
+ # Note: contenttypes should be first, see Django ticket #10827
112
+ "django.contrib.contenttypes",
113
+ "django.contrib.auth",
114
+ "django.contrib.sessions",
115
+ # Note: If enabled, at least one Site object is required
116
+ "django.contrib.sites",
117
+ "django.contrib.messages",
118
+ "django.contrib.staticfiles",
119
+ # Optional applications.
120
+ "django_admin_index",
121
+ "django.contrib.admin",
122
+ # External applications.
123
+ "axes",
124
+ "django_filters",
125
+ "corsheaders",
126
+ "vng_api_common",
127
+ "notifications_api_common",
128
+ "drf_spectacular",
129
+ "rest_framework",
130
+ "django_markup",
131
+ "solo",
132
+ # Two-factor authentication in the Django admin, enforced.
133
+ "django_otp",
134
+ "django_otp.plugins.otp_static",
135
+ "django_otp.plugins.otp_totp",
136
+ "two_factor",
137
+ "maykin_2fa",
138
+ "privates",
139
+ "django_jsonform",
140
+ "simple_certmanager",
141
+ "zgw_consumers",
142
+ "mozilla_django_oidc",
143
+ "mozilla_django_oidc_db",
144
+ "log_outgoing_requests",
145
+ "django_setup_configuration",
146
+ ]
147
+
148
+ MIDDLEWARE = [
149
+ "django.middleware.security.SecurityMiddleware",
150
+ "django.contrib.sessions.middleware.SessionMiddleware",
151
+ "corsheaders.middleware.CorsMiddleware",
152
+ "django.middleware.common.CommonMiddleware",
153
+ "django.middleware.csrf.CsrfViewMiddleware",
154
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
155
+ "maykin_2fa.middleware.OTPMiddleware",
156
+ "mozilla_django_oidc_db.middleware.SessionRefresh",
157
+ "django.contrib.messages.middleware.MessageMiddleware",
158
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
159
+ "axes.middleware.AxesMiddleware",
160
+ ]
161
+
162
+ ROOT_URLCONF = f"{PROJECT_DIRNAME}.urls"
163
+
164
+ # List of callables that know how to import templates from various sources.
165
+ TEMPLATE_LOADERS = (
166
+ "django.template.loaders.filesystem.Loader",
167
+ "django.template.loaders.app_directories.Loader",
168
+ )
169
+
170
+ TEMPLATES = [
171
+ {
172
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
173
+ "DIRS": [Path(DJANGO_PROJECT_DIR) / "templates"],
174
+ "APP_DIRS": False, # conflicts with explicity specifying the loaders
175
+ "OPTIONS": {
176
+ "context_processors": [
177
+ "django.template.context_processors.debug",
178
+ "django.template.context_processors.request",
179
+ "django.contrib.auth.context_processors.auth",
180
+ "django.contrib.messages.context_processors.messages",
181
+ f"{PROJECT_DIRNAME}.utils.context_processors.settings",
182
+ ],
183
+ "loaders": TEMPLATE_LOADERS,
184
+ },
185
+ }
186
+ ]
187
+
188
+ WSGI_APPLICATION = f"{PROJECT_DIRNAME}.wsgi.application"
189
+
190
+ # Translations
191
+ LOCALE_PATHS = (Path(DJANGO_PROJECT_DIR) / "conf" / "locale",)
192
+
193
+ #
194
+ # SERVING of static and media files
195
+ #
196
+
197
+ STATIC_URL = "/static/"
198
+
199
+ STATIC_ROOT = Path(BASE_DIR) / "static"
200
+
201
+ # Additional locations of static files
202
+ STATICFILES_DIRS = [Path(DJANGO_PROJECT_DIR) / "static"]
203
+
204
+ # List of finder classes that know how to find static files in
205
+ # various locations.
206
+ STATICFILES_FINDERS = [
207
+ "django.contrib.staticfiles.finders.FileSystemFinder",
208
+ "django.contrib.staticfiles.finders.AppDirectoriesFinder",
209
+ ]
210
+
211
+ MEDIA_ROOT = Path(BASE_DIR) / "media"
212
+
213
+ MEDIA_URL = "/media/"
214
+
215
+ FILE_UPLOAD_PERMISSIONS = 0o644
216
+
217
+ FIXTURE_DIRS = (Path(DJANGO_PROJECT_DIR) / "fixtures",)
218
+
219
+ #
220
+ # Sending EMAIL
221
+ #
222
+ EMAIL_HOST = config("EMAIL_HOST", default="localhost")
223
+ EMAIL_PORT = config(
224
+ "EMAIL_PORT", default=25
225
+ ) # disabled on Google Cloud, use 487 instead
226
+ EMAIL_HOST_USER = config("EMAIL_HOST_USER", default="")
227
+ EMAIL_HOST_PASSWORD = config("EMAIL_HOST_PASSWORD", default="")
228
+ EMAIL_USE_TLS = config("EMAIL_USE_TLS", default=False)
229
+ EMAIL_TIMEOUT = 10
230
+
231
+ DEFAULT_FROM_EMAIL = config("DEFAULT_FROM_EMAIL", f"{PROJECT_DIRNAME}@example.com")
232
+
233
+ #
234
+ # LOGGING
235
+ #
236
+ LOG_STDOUT = config("LOG_STDOUT", default=False)
237
+ LOG_LEVEL = config("LOG_LEVEL", default="WARNING")
238
+ LOG_QUERIES = config("LOG_QUERIES", default=False)
239
+ LOG_REQUESTS = config("LOG_REQUESTS", default=False)
240
+ if LOG_QUERIES and not DEBUG:
241
+ warnings.warn(
242
+ "Requested LOG_QUERIES=1 but DEBUG is false, no query logs will be emited.",
243
+ RuntimeWarning,
244
+ )
245
+
246
+ LOGGING_DIR = Path(BASE_DIR) / "log"
247
+
248
+ logging_root_handlers = ["console"] if LOG_STDOUT else ["project"]
249
+ logging_django_handlers = ["console"] if LOG_STDOUT else ["django"]
250
+
251
+ LOGGING = {
252
+ "version": 1,
253
+ "disable_existing_loggers": False,
254
+ "formatters": {
255
+ "verbose": {
256
+ "format": "%(asctime)s %(levelname)s %(name)s %(module)s %(process)d %(thread)d %(message)s"
257
+ },
258
+ "timestamped": {"format": "%(asctime)s %(levelname)s %(name)s %(message)s"},
259
+ "simple": {"format": "%(levelname)s %(message)s"},
260
+ "performance": {"format": "%(asctime)s %(process)d | %(thread)d | %(message)s"},
261
+ "db": {"format": "%(asctime)s | %(message)s"},
262
+ "outgoing_requests": {"()": HttpFormatter},
263
+ },
264
+ "filters": {
265
+ "require_debug_false": {"()": "django.utils.log.RequireDebugFalse"},
266
+ },
267
+ "handlers": {
268
+ "mail_admins": {
269
+ "level": "ERROR",
270
+ "filters": ["require_debug_false"],
271
+ "class": "django.utils.log.AdminEmailHandler",
272
+ },
273
+ "null": {"level": "DEBUG", "class": "logging.NullHandler"},
274
+ "console": {
275
+ "level": LOG_LEVEL,
276
+ "class": "logging.StreamHandler",
277
+ "formatter": "timestamped",
278
+ },
279
+ "console_db": {
280
+ "level": "DEBUG",
281
+ "class": "logging.StreamHandler",
282
+ "formatter": "db",
283
+ },
284
+ "django": {
285
+ "level": LOG_LEVEL,
286
+ "class": "logging.handlers.RotatingFileHandler",
287
+ "filename": Path(LOGGING_DIR) / "django.log",
288
+ "formatter": "verbose",
289
+ "maxBytes": 1024 * 1024 * 10, # 10 MB
290
+ "backupCount": 10,
291
+ },
292
+ "project": {
293
+ "level": LOG_LEVEL,
294
+ "class": "logging.handlers.RotatingFileHandler",
295
+ "filename": Path(LOGGING_DIR) / f"{PROJECT_DIRNAME}.log",
296
+ "formatter": "verbose",
297
+ "maxBytes": 1024 * 1024 * 10, # 10 MB
298
+ "backupCount": 10,
299
+ },
300
+ "performance": {
301
+ "level": "INFO",
302
+ "class": "logging.handlers.RotatingFileHandler",
303
+ "filename": Path(LOGGING_DIR) / "performance.log",
304
+ "formatter": "performance",
305
+ "maxBytes": 1024 * 1024 * 10, # 10 MB
306
+ "backupCount": 10,
307
+ },
308
+ "requests": {
309
+ "level": "DEBUG",
310
+ "class": "logging.handlers.RotatingFileHandler",
311
+ "filename": Path(LOGGING_DIR) / "requests.log",
312
+ "formatter": "timestamped",
313
+ "maxBytes": 1024 * 1024 * 10, # 10 MB
314
+ "backupCount": 10,
315
+ },
316
+ "log_outgoing_requests": {
317
+ "level": "DEBUG",
318
+ "formatter": "outgoing_requests",
319
+ "class": "logging.StreamHandler", # to write to stdout
320
+ },
321
+ "save_outgoing_requests": {
322
+ "level": "DEBUG",
323
+ # enabling saving to database
324
+ "class": "log_outgoing_requests.handlers.DatabaseOutgoingRequestsHandler",
325
+ },
326
+ },
327
+ "loggers": {
328
+ "": {
329
+ "handlers": logging_root_handlers,
330
+ "level": "ERROR",
331
+ "propagate": False,
332
+ },
333
+ PROJECT_DIRNAME: {
334
+ "handlers": logging_root_handlers,
335
+ "level": LOG_LEVEL,
336
+ "propagate": True,
337
+ },
338
+ "mozilla_django_oidc": {
339
+ "handlers": logging_root_handlers,
340
+ "level": LOG_LEVEL,
341
+ },
342
+ f"{PROJECT_DIRNAME}.utils.middleware": {
343
+ "handlers": logging_root_handlers,
344
+ "level": LOG_LEVEL,
345
+ "propagate": False,
346
+ },
347
+ "vng_api_common": {
348
+ "handlers": ["console"],
349
+ "level": LOG_LEVEL,
350
+ "propagate": True,
351
+ },
352
+ "django.db.backends": {
353
+ "handlers": ["console_db"] if LOG_QUERIES else [],
354
+ "level": "DEBUG",
355
+ "propagate": False,
356
+ },
357
+ "django.request": {
358
+ "handlers": logging_django_handlers,
359
+ "level": LOG_LEVEL,
360
+ "propagate": True,
361
+ },
362
+ "django.template": {
363
+ "handlers": ["console"],
364
+ "level": "INFO",
365
+ "propagate": False,
366
+ },
367
+ "log_outgoing_requests": {
368
+ "handlers": (
369
+ ["log_outgoing_requests", "save_outgoing_requests"]
370
+ if LOG_REQUESTS
371
+ else []
372
+ ),
373
+ "level": "DEBUG",
374
+ "propagate": True,
375
+ },
376
+ },
377
+ }
378
+
379
+ #
380
+ # AUTH settings - user accounts, passwords, backends...
381
+ #
382
+ AUTH_USER_MODEL = "accounts.User"
383
+
384
+ # Password validation
385
+ # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
386
+ AUTH_PASSWORD_VALIDATORS = [
387
+ {
388
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
389
+ },
390
+ {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
391
+ {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
392
+ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
393
+ ]
394
+
395
+ # Allow logging in with both username+password and email+password
396
+ AUTHENTICATION_BACKENDS = [
397
+ "axes.backends.AxesBackend",
398
+ f"{PROJECT_DIRNAME}.accounts.backends.UserModelEmailBackend",
399
+ "django.contrib.auth.backends.ModelBackend",
400
+ "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend",
401
+ ]
402
+
403
+ SESSION_COOKIE_NAME = f"{PROJECT_DIRNAME}_sessionid"
404
+ SESSION_ENGINE = "django.contrib.sessions.backends.cache"
405
+
406
+ LOGIN_URL = reverse_lazy("admin:login")
407
+ LOGIN_REDIRECT_URL = reverse_lazy("admin:index")
408
+ LOGOUT_REDIRECT_URL = reverse_lazy("admin:index")
409
+
410
+ #
411
+ # SECURITY settings
412
+ #
413
+ SESSION_COOKIE_SECURE = IS_HTTPS
414
+ SESSION_COOKIE_HTTPONLY = True
415
+
416
+ CSRF_COOKIE_SECURE = IS_HTTPS
417
+
418
+ X_FRAME_OPTIONS = "DENY"
419
+
420
+ #
421
+ # Silenced checks
422
+ #
423
+ SILENCED_SYSTEM_CHECKS = [
424
+ "rest_framework.W001",
425
+ "debug_toolbar.W006",
426
+ ]
427
+
428
+
429
+ #
430
+ # Increase number of parameters for GET/POST requests
431
+ #
432
+ DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000
433
+
434
+ #
435
+ # Custom settings
436
+ #
437
+ ENVIRONMENT = config("ENVIRONMENT", "")
438
+ ENVIRONMENT_SHOWN_IN_ADMIN = True
439
+
440
+ # Generating the schema, depending on the component
441
+ subpath = config("SUBPATH", None)
442
+ if subpath:
443
+ if not subpath.startswith("/"):
444
+ subpath = f"/{subpath}"
445
+ SUBPATH = subpath
446
+
447
+ if "GIT_SHA" in os.environ:
448
+ GIT_SHA = config("GIT_SHA", "")
449
+ # in docker (build) context, there is no .git directory
450
+ elif (Path(BASE_DIR) / ".git").exists():
451
+ try:
452
+ import git
453
+ except ImportError:
454
+ GIT_SHA = None
455
+ else:
456
+ repo = git.Repo(search_parent_directories=True)
457
+ GIT_SHA = repo.head.object.hexsha
458
+ else:
459
+ GIT_SHA = None
460
+
461
+ RELEASE = config("RELEASE", GIT_SHA)
462
+
463
+ NUM_PROXIES = config( # TODO: this also is relevant for DRF settings if/when we have rate-limited endpoints
464
+ "NUM_PROXIES",
465
+ default=1,
466
+ cast=lambda val: int(val) if val is not None else None,
467
+ )
468
+
469
+ ##############################
470
+ # #
471
+ # 3RD PARTY LIBRARY SETTINGS #
472
+ # #
473
+ ##############################
474
+
475
+ #
476
+ # DJANGO-AXES (6.0+)
477
+ #
478
+ AXES_CACHE = "axes" # refers to CACHES setting
479
+ # The number of login attempts allowed before a record is created for the
480
+ # failed logins. Default: 3
481
+ AXES_FAILURE_LIMIT = 10
482
+ # If set, defines a period of inactivity after which old failed login attempts
483
+ # will be forgotten. Can be set to a python timedelta object or an integer. If
484
+ # an integer, will be interpreted as a number of hours. Default: None
485
+ AXES_COOLOFF_TIME = 1
486
+ # The number of reverse proxies
487
+ AXES_IPWARE_PROXY_COUNT = NUM_PROXIES - 1 if NUM_PROXIES else None
488
+ # If set, specifies a template to render when a user is locked out. Template
489
+ # receives cooloff_time and failure_limit as context variables. Default: None
490
+ AXES_LOCKOUT_TEMPLATE = "account_blocked.html"
491
+ AXES_LOCKOUT_PARAMETERS = [["ip_address", "user_agent", "username"]]
492
+ AXES_BEHIND_REVERSE_PROXY = IS_HTTPS
493
+ # By default, Axes obfuscates values for formfields named "password", but the admin
494
+ # interface login formfield name is "auth-password", so we want to obfuscate that
495
+ AXES_SENSITIVE_PARAMETERS = ["auth-password"] # nosec
496
+
497
+ # The default meta precedence order
498
+ IPWARE_META_PRECEDENCE_ORDER = (
499
+ "HTTP_X_FORWARDED_FOR",
500
+ "X_FORWARDED_FOR", # <client>, <proxy1>, <proxy2>
501
+ "HTTP_CLIENT_IP",
502
+ "HTTP_X_REAL_IP",
503
+ "HTTP_X_FORWARDED",
504
+ "HTTP_X_CLUSTER_CLIENT_IP",
505
+ "HTTP_FORWARDED_FOR",
506
+ "HTTP_FORWARDED",
507
+ "HTTP_VIA",
508
+ "REMOTE_ADDR",
509
+ )
510
+
511
+ #
512
+ # DJANGO-CORS-MIDDLEWARE
513
+ #
514
+ CORS_ALLOW_ALL_ORIGINS = config("CORS_ALLOW_ALL_ORIGINS", default=False)
515
+ CORS_ALLOWED_ORIGINS = config("CORS_ALLOWED_ORIGINS", split=True, default=[])
516
+ CORS_ALLOWED_ORIGIN_REGEXES = config(
517
+ "CORS_ALLOWED_ORIGIN_REGEXES", split=True, default=[]
518
+ )
519
+ # Authorization is included in default_cors_headers
520
+ CORS_ALLOW_HEADERS = (
521
+ list(default_cors_headers)
522
+ + [
523
+ "accept-crs",
524
+ "content-crs",
525
+ ]
526
+ + config("CORS_EXTRA_ALLOW_HEADERS", split=True, default=[])
527
+ )
528
+ CORS_EXPOSE_HEADERS = [
529
+ "content-crs",
530
+ ]
531
+ # Django's SESSION_COOKIE_SAMESITE = "Lax" prevents session cookies from being sent
532
+ # cross-domain. There is no need for these cookies to be sent, since the API itself
533
+ # uses Bearer Authentication.
534
+ # we can't easily derive this from django-cors-headers, see also
535
+ # https://pypi.org/project/django-cors-headers/#csrf-integration
536
+ #
537
+ # So we do a best effort attempt at re-using configuration parameters, with an escape
538
+ # hatch to override it.
539
+ CSRF_TRUSTED_ORIGINS = config(
540
+ "CSRF_TRUSTED_ORIGINS",
541
+ split=True,
542
+ default=[strip_protocol_from_origin(origin) for origin in CORS_ALLOWED_ORIGINS],
543
+ )
544
+ #
545
+ # DJANGO-PRIVATES -- safely serve files after authorization
546
+ #
547
+ PRIVATE_MEDIA_ROOT = Path(BASE_DIR) / "private-media"
548
+ PRIVATE_MEDIA_URL = "/private-media/"
549
+
550
+
551
+ #
552
+ # NOTIFICATIONS-API-COMMON
553
+ #
554
+ NOTIFICATIONS_DISABLED = config("NOTIFICATIONS_DISABLED", default=False)
555
+
556
+ #
557
+ # SENTRY - error monitoring
558
+ #
559
+
560
+
561
+ def init_sentry(before_send: Callable | None = None):
562
+ SENTRY_DSN = config("SENTRY_DSN", None)
563
+
564
+ if SENTRY_DSN:
565
+ SENTRY_CONFIG = {
566
+ "dsn": SENTRY_DSN,
567
+ "release": RELEASE or "RELEASE not set",
568
+ "environment": ENVIRONMENT,
569
+ }
570
+
571
+ # Allow projects to define their own before_send filters
572
+ if before_send:
573
+ SENTRY_CONFIG["before_send"] = before_send
574
+
575
+ sentry_sdk.init(
576
+ **SENTRY_CONFIG,
577
+ integrations=get_sentry_integrations(),
578
+ send_default_pii=True,
579
+ )
580
+
581
+
582
+ #
583
+ # CELERY
584
+ #
585
+ CELERY_BROKER_URL = config("CELERY_RESULT_BACKEND", "redis://localhost:6379/1")
586
+ CELERY_RESULT_BACKEND = config("CELERY_RESULT_BACKEND", "redis://localhost:6379/1")
587
+
588
+
589
+ #
590
+ # DJANGO-ADMIN-INDEX
591
+ #
592
+ ADMIN_INDEX_SHOW_REMAINING_APPS_TO_SUPERUSERS = False
593
+ ADMIN_INDEX_AUTO_CREATE_APP_GROUP = False
594
+
595
+ #
596
+ # Mozilla Django OIDC DB settings
597
+ #
598
+ OIDC_AUTHENTICATE_CLASS = "mozilla_django_oidc_db.views.OIDCAuthenticationRequestView"
599
+ MOZILLA_DJANGO_OIDC_DB_CACHE = "oidc"
600
+ MOZILLA_DJANGO_OIDC_DB_CACHE_TIMEOUT = 5 * 60
601
+
602
+ #
603
+ # Elastic APM
604
+ #
605
+ ELASTIC_APM_SERVER_URL = config("ELASTIC_APM_SERVER_URL", None)
606
+ ELASTIC_APM = {
607
+ # FIXME this does change the default service name, because PROJECT_DIRNAME != PROJECT_NAME
608
+ "SERVICE_NAME": config(
609
+ "ELASTIC_APM_SERVICE_NAME", f"{PROJECT_DIRNAME} - {ENVIRONMENT}"
610
+ ),
611
+ "SECRET_TOKEN": config("ELASTIC_APM_SECRET_TOKEN", "default"),
612
+ "SERVER_URL": ELASTIC_APM_SERVER_URL,
613
+ "TRANSACTION_SAMPLE_RATE": config("ELASTIC_APM_TRANSACTION_SAMPLE_RATE", 0.1),
614
+ }
615
+ if not ELASTIC_APM_SERVER_URL:
616
+ ELASTIC_APM["ENABLED"] = False
617
+ ELASTIC_APM["SERVER_URL"] = "http://localhost:8200"
618
+ else:
619
+ MIDDLEWARE = ["elasticapm.contrib.django.middleware.TracingMiddleware"] + MIDDLEWARE
620
+ INSTALLED_APPS = INSTALLED_APPS + [
621
+ "elasticapm.contrib.django",
622
+ ]
623
+
624
+
625
+ #
626
+ # MAYKIN-2FA
627
+ # Uses django-two-factor-auth under the hood, so relevant upstream package settings
628
+ # apply too.
629
+ #
630
+
631
+ # we run the admin site monkeypatch instead.
632
+ TWO_FACTOR_PATCH_ADMIN = False
633
+ # add entries from AUTHENTICATION_BACKENDS that already enforce their own two-factor
634
+ # auth, avoiding having some set up MFA again in the project.
635
+ MAYKIN_2FA_ALLOW_MFA_BYPASS_BACKENDS = [
636
+ "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend",
637
+ ]
638
+
639
+ if config("DISABLE_2FA", default=False): # pragma: no cover
640
+ MAYKIN_2FA_ALLOW_MFA_BYPASS_BACKENDS = AUTHENTICATION_BACKENDS
641
+
642
+
643
+ #
644
+ # LOG OUTGOING REQUESTS
645
+ #
646
+ LOG_OUTGOING_REQUESTS_EMIT_BODY = True
647
+ LOG_OUTGOING_REQUESTS_DB_SAVE = config("LOG_OUTGOING_REQUESTS_DB_SAVE", default=False)
648
+ LOG_OUTGOING_REQUESTS_RESET_DB_SAVE_AFTER = None
649
+ LOG_OUTGOING_REQUESTS_MAX_AGE = config(
650
+ "LOG_OUTGOING_REQUESTS_MAX_AGE", default=7
651
+ ) # number of days
@@ -0,0 +1,63 @@
1
+ import sys
2
+ from pathlib import Path
3
+ from typing import Any
4
+ from urllib.parse import urlparse
5
+
6
+ from decouple import Csv, config as _config, undefined
7
+ from sentry_sdk.integrations import DidNotEnable, django, redis
8
+
9
+
10
+ def config(option: str, default: Any = undefined, *args, **kwargs):
11
+ """
12
+ Pull a config parameter from the environment.
13
+
14
+ Read the config variable ``option``. If it's optional, use the ``default`` value.
15
+ Input is automatically cast to the correct type, where the type is derived from the
16
+ default value if possible.
17
+
18
+ Pass ``split=True`` to split the comma-separated input into a list.
19
+ """
20
+ if "split" in kwargs:
21
+ kwargs.pop("split")
22
+ kwargs["cast"] = Csv()
23
+ if isinstance(default, list):
24
+ default = ",".join(default)
25
+
26
+ if default is not undefined and default is not None:
27
+ kwargs.setdefault("cast", type(default))
28
+ return _config(option, default=default, *args, **kwargs)
29
+
30
+
31
+ def get_sentry_integrations() -> list:
32
+ """
33
+ Determine which Sentry SDK integrations to enable.
34
+ """
35
+ default = [
36
+ django.DjangoIntegration(),
37
+ redis.RedisIntegration(),
38
+ ]
39
+ extra = []
40
+
41
+ try:
42
+ from sentry_sdk.integrations import celery
43
+ except DidNotEnable: # happens if the celery import fails by the integration
44
+ pass
45
+ else:
46
+ extra.append(celery.CeleryIntegration())
47
+
48
+ return [*default, *extra]
49
+
50
+
51
+ def strip_protocol_from_origin(origin: str) -> str:
52
+ parsed = urlparse(origin)
53
+ return parsed.netloc
54
+
55
+
56
+ def get_project_dirname() -> str:
57
+ return config("DJANGO_SETTINGS_MODULE").split(".")[0]
58
+
59
+
60
+ def get_django_project_dir() -> str:
61
+ # Get the path of the importing module
62
+ base_dirname = get_project_dirname()
63
+ return Path(sys.modules[base_dirname].__file__).parent
File without changes
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: open_api_framework
3
- Version: 0.2.0
4
- Summary: TODO
3
+ Version: 0.4.0
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 2024 Maykin Media
7
7
 
@@ -15,7 +15,7 @@ Project-URL: Homepage, https://github.com/maykinmedia/open_api_framework
15
15
  Project-URL: Documentation, http://open_api_framework.readthedocs.io/en/latest/
16
16
  Project-URL: Bug Tracker, https://github.com/maykinmedia/open_api_framework/issues
17
17
  Project-URL: Source Code, https://github.com/maykinmedia/open_api_framework
18
- Keywords: TODO
18
+ Keywords: metapackage,dependencies,settings
19
19
  Classifier: Development Status :: 3 - Alpha
20
20
  Classifier: Framework :: Django
21
21
  Classifier: Framework :: Django :: 4.2
@@ -35,6 +35,7 @@ Requires-Dist: django-axes>=6.3.0
35
35
  Requires-Dist: django-cors-headers>=4.3.1
36
36
  Requires-Dist: django-markup>=1.8.1
37
37
  Requires-Dist: django-jsonform>=2.21.4
38
+ Requires-Dist: django-log-outgoing-requests>=0.6.1
38
39
  Requires-Dist: django-admin-index>=3.1.0
39
40
  Requires-Dist: django-redis>=5.4.0
40
41
  Requires-Dist: djangorestframework>=3.12.4
@@ -56,6 +57,7 @@ Requires-Dist: elastic-apm>=6.20.0
56
57
  Requires-Dist: celery>=5.2.7
57
58
  Requires-Dist: flower>=2.0.1
58
59
  Requires-Dist: maykin-2fa>=1.0.0
60
+ Requires-Dist: django-setup-configuration>=0.1.0
59
61
  Provides-Extra: tests
60
62
  Requires-Dist: pytest; extra == "tests"
61
63
  Requires-Dist: pytest-django; extra == "tests"
@@ -77,7 +79,7 @@ Requires-Dist: twine; extra == "release"
77
79
  Welcome to open-api-framework's documentation!
78
80
  =================================================
79
81
 
80
- :Version: 0.2.0
82
+ :Version: 0.4.0
81
83
  :Source: https://github.com/maykinmedia/open-api-framework
82
84
  :Keywords: metapackage, dependencies
83
85
 
@@ -116,7 +118,7 @@ Install
116
118
  Local development
117
119
  =================
118
120
 
119
- To install and develop the library locally, use::
121
+ To install and develop the library locally, use:
120
122
 
121
123
  .. code-block:: bash
122
124
 
@@ -1,3 +1,4 @@
1
+ CHANGELOG.rst
1
2
  LICENSE
2
3
  MANIFEST.in
3
4
  README.rst
@@ -11,4 +12,7 @@ open_api_framework.egg-info/SOURCES.txt
11
12
  open_api_framework.egg-info/dependency_links.txt
12
13
  open_api_framework.egg-info/requires.txt
13
14
  open_api_framework.egg-info/top_level.txt
14
- tests/test_dummy.py
15
+ open_api_framework/conf/__init__.py
16
+ open_api_framework/conf/base.py
17
+ open_api_framework/conf/utils.py
18
+ tests/test_config_helpers.py
@@ -3,6 +3,7 @@ django-axes>=6.3.0
3
3
  django-cors-headers>=4.3.1
4
4
  django-markup>=1.8.1
5
5
  django-jsonform>=2.21.4
6
+ django-log-outgoing-requests>=0.6.1
6
7
  django-admin-index>=3.1.0
7
8
  django-redis>=5.4.0
8
9
  djangorestframework>=3.12.4
@@ -24,6 +25,7 @@ elastic-apm>=6.20.0
24
25
  celery>=5.2.7
25
26
  flower>=2.0.1
26
27
  maykin-2fa>=1.0.0
28
+ django-setup-configuration>=0.1.0
27
29
 
28
30
  [coverage]
29
31
  pytest-cov
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "open_api_framework"
7
- version = "0.2.0"
8
- description = "TODO"
7
+ version = "0.4.0"
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"}
11
11
  ]
12
12
  readme = "README.rst"
13
13
  license = {file = "LICENSE"}
14
- keywords = ["TODO"]
14
+ keywords = ["metapackage", "dependencies", "settings"]
15
15
  classifiers = [
16
16
  "Development Status :: 3 - Alpha",
17
17
  "Framework :: Django",
@@ -32,6 +32,7 @@ dependencies = [
32
32
  "django-cors-headers>=4.3.1",
33
33
  "django-markup>=1.8.1",
34
34
  "django-jsonform>=2.21.4",
35
+ "django-log-outgoing-requests>=0.6.1",
35
36
  "django-admin-index>=3.1.0",
36
37
  "django-redis>=5.4.0",
37
38
  "djangorestframework>=3.12.4",
@@ -53,6 +54,7 @@ dependencies = [
53
54
  "celery>=5.2.7",
54
55
  "flower>=2.0.1",
55
56
  "maykin-2fa>=1.0.0",
57
+ "django-setup-configuration>=0.1.0",
56
58
  ]
57
59
 
58
60
  [project.urls]
@@ -98,7 +100,7 @@ testpaths = ["tests"]
98
100
  DJANGO_SETTINGS_MODULE = "testapp.settings"
99
101
 
100
102
  [tool.bumpversion]
101
- current_version = "0.2.0"
103
+ current_version = "0.4.0"
102
104
  files = [
103
105
  {filename = "pyproject.toml"},
104
106
  {filename = "README.rst"},
@@ -1,5 +1,5 @@
1
1
  [flake8]
2
- max-line-length = 88
2
+ max-line-length = 119
3
3
  exclude = env,.tox,doc
4
4
 
5
5
  [egg_info]
@@ -0,0 +1,13 @@
1
+ from open_api_framework.conf.utils import config
2
+
3
+
4
+ def test_empty_list_as_default():
5
+ value = config("SOME_TEST_ENVVAR", split=True, default=[])
6
+
7
+ assert value == []
8
+
9
+
10
+ def test_non_empty_list_as_default():
11
+ value = config("SOME_TEST_ENVVAR", split=True, default=["foo"])
12
+
13
+ assert value == ["foo"]
@@ -1,2 +0,0 @@
1
- def test_dummy():
2
- assert True