flagsmith-common 1.5.2__tar.gz → 1.6.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 (86) hide show
  1. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/PKG-INFO +9 -10
  2. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/README.md +2 -8
  3. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/pyproject.toml +14 -4
  4. flagsmith_common-1.6.0/src/common/core/main.py +80 -0
  5. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/management/commands/start.py +23 -3
  6. flagsmith_common-1.6.0/src/common/core/management/commands/waitfordb.py +87 -0
  7. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/gunicorn/middleware.py +4 -1
  8. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/gunicorn/utils.py +17 -0
  9. flagsmith_common-1.6.0/src/common/migrations/helpers/__init__.py +9 -0
  10. flagsmith_common-1.6.0/src/common/migrations/helpers/postgres_helpers.py +41 -0
  11. flagsmith_common-1.6.0/src/common/py.typed +0 -0
  12. flagsmith_common-1.6.0/src/task_processor/__init__.py +0 -0
  13. flagsmith_common-1.6.0/src/task_processor/admin.py +38 -0
  14. flagsmith_common-1.6.0/src/task_processor/apps.py +18 -0
  15. flagsmith_common-1.6.0/src/task_processor/decorators.py +206 -0
  16. flagsmith_common-1.6.0/src/task_processor/exceptions.py +10 -0
  17. flagsmith_common-1.6.0/src/task_processor/health.py +44 -0
  18. flagsmith_common-1.6.0/src/task_processor/managers.py +18 -0
  19. flagsmith_common-1.6.0/src/task_processor/migrations/0001_initial.py +44 -0
  20. flagsmith_common-1.6.0/src/task_processor/migrations/0002_healthcheckmodel.py +21 -0
  21. flagsmith_common-1.6.0/src/task_processor/migrations/0003_add_completed_to_task.py +22 -0
  22. flagsmith_common-1.6.0/src/task_processor/migrations/0004_recreate_task_indexes.py +43 -0
  23. flagsmith_common-1.6.0/src/task_processor/migrations/0005_update_conditional_index_conditions.py +45 -0
  24. flagsmith_common-1.6.0/src/task_processor/migrations/0006_auto_20230221_0802.py +45 -0
  25. flagsmith_common-1.6.0/src/task_processor/migrations/0007_add_is_locked.py +23 -0
  26. flagsmith_common-1.6.0/src/task_processor/migrations/0008_add_get_task_to_process_function.py +31 -0
  27. flagsmith_common-1.6.0/src/task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +18 -0
  28. flagsmith_common-1.6.0/src/task_processor/migrations/0010_task_priority.py +27 -0
  29. flagsmith_common-1.6.0/src/task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +27 -0
  30. flagsmith_common-1.6.0/src/task_processor/migrations/0012_add_locked_at_and_timeout.py +40 -0
  31. flagsmith_common-1.6.0/src/task_processor/migrations/0013_add_last_picked_at.py +34 -0
  32. flagsmith_common-1.6.0/src/task_processor/migrations/__init__.py +0 -0
  33. flagsmith_common-1.6.0/src/task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +30 -0
  34. flagsmith_common-1.6.0/src/task_processor/migrations/sql/0008_get_tasks_to_process.sql +30 -0
  35. flagsmith_common-1.6.0/src/task_processor/migrations/sql/0011_get_tasks_to_process.sql +30 -0
  36. flagsmith_common-1.6.0/src/task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +33 -0
  37. flagsmith_common-1.6.0/src/task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +33 -0
  38. flagsmith_common-1.6.0/src/task_processor/migrations/sql/__init__.py +0 -0
  39. flagsmith_common-1.6.0/src/task_processor/models.py +246 -0
  40. flagsmith_common-1.6.0/src/task_processor/monitoring.py +12 -0
  41. flagsmith_common-1.6.0/src/task_processor/processor.py +138 -0
  42. flagsmith_common-1.6.0/src/task_processor/py.typed +0 -0
  43. flagsmith_common-1.6.0/src/task_processor/serializers.py +7 -0
  44. flagsmith_common-1.6.0/src/task_processor/task_registry.py +82 -0
  45. flagsmith_common-1.6.0/src/task_processor/task_run_method.py +7 -0
  46. flagsmith_common-1.6.0/src/task_processor/tasks.py +71 -0
  47. flagsmith_common-1.6.0/src/task_processor/threads.py +112 -0
  48. flagsmith_common-1.6.0/src/task_processor/types.py +18 -0
  49. flagsmith_common-1.6.0/src/task_processor/urls.py +5 -0
  50. flagsmith_common-1.6.0/src/task_processor/utils.py +71 -0
  51. flagsmith_common-1.6.0/src/task_processor/views.py +20 -0
  52. flagsmith_common-1.5.2/src/common/core/main.py +0 -40
  53. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/LICENSE +0 -0
  54. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/__init__.py +0 -0
  55. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/__init__.py +0 -0
  56. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/app.py +0 -0
  57. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/logging.py +0 -0
  58. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/management/__init__.py +0 -0
  59. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/management/commands/__init__.py +0 -0
  60. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/metrics.py +0 -0
  61. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/urls.py +0 -0
  62. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/utils.py +0 -0
  63. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/views.py +0 -0
  64. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/environments/permissions.py +0 -0
  65. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/features/__init__.py +0 -0
  66. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/features/multivariate/__init__.py +0 -0
  67. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/features/multivariate/serializers.py +0 -0
  68. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/features/serializers.py +0 -0
  69. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/features/versioning/__init__.py +0 -0
  70. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/features/versioning/serializers.py +0 -0
  71. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/gunicorn/__init__.py +0 -0
  72. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/gunicorn/conf.py +0 -0
  73. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/gunicorn/constants.py +0 -0
  74. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/gunicorn/logging.py +0 -0
  75. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/gunicorn/metrics.py +0 -0
  76. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/metadata/serializers.py +0 -0
  77. flagsmith_common-1.5.2/src/common/py.typed → flagsmith_common-1.6.0/src/common/migrations/__init__.py +0 -0
  78. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/organisations/permissions.py +0 -0
  79. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/projects/permissions.py +0 -0
  80. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/prometheus/__init__.py +0 -0
  81. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/prometheus/utils.py +0 -0
  82. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/segments/serializers.py +0 -0
  83. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/test_tools/__init__.py +0 -0
  84. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/test_tools/plugin.py +5 -5
  85. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/test_tools/types.py +0 -0
  86. {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flagsmith-common
3
- Version: 1.5.2
3
+ Version: 1.6.0
4
4
  Summary: Flagsmith's common library
5
5
  License: BSD-3-Clause
6
6
  Author: Matthew Elwell
@@ -15,15 +15,20 @@ Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
- Requires-Dist: django (<5)
18
+ Requires-Dist: backoff (>=2.2.1,<3.0.0)
19
+ Requires-Dist: django (>4,<5)
19
20
  Requires-Dist: django-health-check
20
21
  Requires-Dist: djangorestframework
21
22
  Requires-Dist: djangorestframework-recursive
22
23
  Requires-Dist: drf-writable-nested
24
+ Requires-Dist: drf-yasg (>=1.21.10,<2.0.0)
23
25
  Requires-Dist: environs (<15)
24
26
  Requires-Dist: flagsmith-flag-engine
25
27
  Requires-Dist: gunicorn (>=19.1)
26
28
  Requires-Dist: prometheus-client (>=0.0.16)
29
+ Requires-Dist: psycopg2-binary (>=2.9,<3)
30
+ Requires-Dist: simplejson (>=3,<4)
31
+ Project-URL: Changelog, https://github.com/flagsmith/flagsmith-common/blob/main/CHANGELOG.md
27
32
  Project-URL: Download, https://github.com/flagsmith/flagsmith-common/releases
28
33
  Project-URL: Homepage, https://flagsmith.com
29
34
  Project-URL: Issues, https://github.com/flagsmith/flagsmith-common/issues
@@ -56,12 +61,6 @@ make install-poetry # Install Poetry
56
61
  make install-packages # Install project dependencies
57
62
  ```
58
63
 
59
- By default, Poetry version 2.0.1 will be installed. You can specify a different version:
60
-
61
- ```bash
62
- make install-poetry POETRY_VERSION=2.1.0
63
- ```
64
-
65
64
  #### Development
66
65
 
67
66
  Run linting checks using pre-commit:
@@ -99,8 +98,8 @@ Flagsmith uses Prometheus to track performance metrics.
99
98
  The following default metrics are exposed:
100
99
 
101
100
  - `flagsmith_build_info`: Has the labels `version` and `ci_commit_sha`.
102
- - `http_server_request_duration_seconds`: Histogram labeled with `method`, `path`, and `response_status`.
103
- - `http_server_requests_total`: Counter labeled with `method`, `path`, and `response_status`.
101
+ - `http_server_request_duration_seconds`: Histogram labeled with `method`, `route`, and `response_status`.
102
+ - `http_server_requests_total`: Counter labeled with `method`, `route`, and `response_status`.
104
103
 
105
104
  ##### Guidelines
106
105
 
@@ -24,12 +24,6 @@ make install-poetry # Install Poetry
24
24
  make install-packages # Install project dependencies
25
25
  ```
26
26
 
27
- By default, Poetry version 2.0.1 will be installed. You can specify a different version:
28
-
29
- ```bash
30
- make install-poetry POETRY_VERSION=2.1.0
31
- ```
32
-
33
27
  #### Development
34
28
 
35
29
  Run linting checks using pre-commit:
@@ -67,8 +61,8 @@ Flagsmith uses Prometheus to track performance metrics.
67
61
  The following default metrics are exposed:
68
62
 
69
63
  - `flagsmith_build_info`: Has the labels `version` and `ci_commit_sha`.
70
- - `http_server_request_duration_seconds`: Histogram labeled with `method`, `path`, and `response_status`.
71
- - `http_server_requests_total`: Counter labeled with `method`, `path`, and `response_status`.
64
+ - `http_server_request_duration_seconds`: Histogram labeled with `method`, `route`, and `response_status`.
65
+ - `http_server_requests_total`: Counter labeled with `method`, `route`, and `response_status`.
72
66
 
73
67
  ##### Guidelines
74
68
 
@@ -1,18 +1,22 @@
1
1
  [project]
2
2
  name = "flagsmith-common"
3
- version = "1.5.2"
3
+ version = "1.6.0"
4
4
  description = "Flagsmith's common library"
5
5
  requires-python = ">=3.11,<4.0"
6
6
  dependencies = [
7
- "django (<5)",
7
+ "backoff (>=2.2.1,<3.0.0)",
8
+ "django (>4,<5)",
8
9
  "django-health-check",
9
10
  "djangorestframework-recursive",
10
11
  "djangorestframework",
11
12
  "drf-writable-nested",
13
+ "drf-yasg (>=1.21.10,<2.0.0)",
14
+ "environs (<15)",
12
15
  "flagsmith-flag-engine",
13
16
  "gunicorn (>=19.1)",
14
17
  "prometheus-client (>=0.0.16)",
15
- "environs (<15)",
18
+ "simplejson (>=3,<4)",
19
+ "psycopg2-binary (>=2.9,<3)",
16
20
  ]
17
21
  authors = [
18
22
  { name = "Matthew Elwell" },
@@ -29,6 +33,7 @@ readme = "README.md"
29
33
  dynamic = ["classifiers"]
30
34
 
31
35
  [project.urls]
36
+ Changelog = "https://github.com/flagsmith/flagsmith-common/blob/main/CHANGELOG.md"
32
37
  Download = "https://github.com/flagsmith/flagsmith-common/releases"
33
38
  Homepage = "https://flagsmith.com"
34
39
  Issues = "https://github.com/flagsmith/flagsmith-common/issues"
@@ -48,9 +53,13 @@ classifiers = [
48
53
  "Intended Audience :: Developers",
49
54
  "License :: OSI Approved :: BSD License",
50
55
  ]
51
- packages = [{ include = "common", from = "src" }]
56
+ packages = [
57
+ { include = "common", from = "src" },
58
+ { include = "task_processor", from = "src" },
59
+ ]
52
60
 
53
61
  [tool.poetry.group.dev.dependencies]
62
+ dj-database-url = "^2.3.0"
54
63
  django-stubs = "^5.1.3"
55
64
  djangorestframework-stubs = "^3.15.3"
56
65
  mypy = "^1.15.0"
@@ -65,6 +74,7 @@ pytest-mock = "^3.14.0"
65
74
  requests = "^2.32.3"
66
75
  ruff = "*"
67
76
  setuptools = "^77.0.3"
77
+ types-simplejson = "^3.20.0.20250326"
68
78
 
69
79
  [build-system]
70
80
  requires = ["poetry-core"]
@@ -0,0 +1,80 @@
1
+ import contextlib
2
+ import logging
3
+ import os
4
+ import sys
5
+ import tempfile
6
+ import typing
7
+
8
+ from django.core.management import execute_from_command_line
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @contextlib.contextmanager
14
+ def ensure_cli_env() -> typing.Generator[None, None, None]:
15
+ """
16
+ Set up the environment for the main entry point of the application
17
+ and clean up after it's done.
18
+
19
+ Add environment-related code that needs to happen before and after Django is involved
20
+ to here.
21
+
22
+ Use as a context manager, e.g.:
23
+
24
+ ```python
25
+ with ensure_cli_env():
26
+ main()
27
+ ```
28
+ """
29
+ ctx = contextlib.ExitStack()
30
+
31
+ # TODO @khvn26 Move logging setup to here
32
+
33
+ # Currently we don't install Flagsmith modules as a package, so we need to add
34
+ # $CWD to the Python path to be able to import them
35
+ sys.path.append(os.getcwd())
36
+
37
+ # TODO @khvn26 We should find a better way to pre-set the Django settings module
38
+ # without resorting to it being set outside of the application
39
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.dev")
40
+
41
+ # Set up Prometheus' multiprocess mode
42
+ if "PROMETHEUS_MULTIPROC_DIR" not in os.environ:
43
+ prometheus_multiproc_dir_name = ctx.enter_context(
44
+ tempfile.TemporaryDirectory(
45
+ prefix="prometheus_multiproc",
46
+ )
47
+ )
48
+
49
+ logger.info(
50
+ "Created %s for Prometheus multi-process mode",
51
+ prometheus_multiproc_dir_name,
52
+ )
53
+ os.environ["PROMETHEUS_MULTIPROC_DIR"] = prometheus_multiproc_dir_name
54
+
55
+ if "task-processor" in sys.argv:
56
+ # A hacky way to signal we're not running the API
57
+ os.environ["RUN_BY_PROCESSOR"] = "true"
58
+
59
+ with ctx:
60
+ yield
61
+
62
+
63
+ def main() -> None:
64
+ """
65
+ The main entry point to the Flagsmith application.
66
+
67
+ An equivalent to Django's `manage.py` script, this module is used to run management commands.
68
+
69
+ It's installed as the `flagsmith` command.
70
+
71
+ Everything that needs to be run before Django is started should be done here.
72
+
73
+ The end goal is to eventually replace Core API's `run-docker.sh` with this.
74
+
75
+ Usage:
76
+ `flagsmith <command> [options]`
77
+ """
78
+ with ensure_cli_env():
79
+ # Run Django
80
+ execute_from_command_line(sys.argv)
@@ -3,7 +3,10 @@ from typing import Any, Callable
3
3
  from django.core.management import BaseCommand, CommandParser
4
4
  from django.utils.module_loading import autodiscover_modules
5
5
 
6
- from common.gunicorn.utils import add_arguments, run_server
6
+ from common.gunicorn.utils import add_arguments as add_gunicorn_arguments
7
+ from common.gunicorn.utils import run_server
8
+ from task_processor.utils import add_arguments as add_task_processor_arguments
9
+ from task_processor.utils import start_task_processor
7
10
 
8
11
 
9
12
  class Command(BaseCommand):
@@ -13,20 +16,31 @@ class Command(BaseCommand):
13
16
  return super().create_parser(*args, conflict_handler="resolve", **kwargs)
14
17
 
15
18
  def add_arguments(self, parser: CommandParser) -> None:
16
- add_arguments(parser)
19
+ add_gunicorn_arguments(parser)
17
20
 
18
21
  subparsers = parser.add_subparsers(
19
22
  title="sub-commands",
20
23
  required=True,
21
24
  )
25
+
22
26
  api_parser = subparsers.add_parser(
23
27
  "api",
24
28
  help="Start the Core API.",
25
29
  )
26
30
  api_parser.set_defaults(handle_method=self.handle_api)
27
31
 
32
+ task_processor_parser = subparsers.add_parser(
33
+ "task-processor",
34
+ help="Start the Task Processor.",
35
+ )
36
+ task_processor_parser.set_defaults(handle_method=self.handle_task_processor)
37
+ add_task_processor_arguments(task_processor_parser)
38
+
28
39
  def initialise(self) -> None:
29
- autodiscover_modules("metrics")
40
+ autodiscover_modules(
41
+ "metrics",
42
+ "tasks",
43
+ )
30
44
 
31
45
  def handle(
32
46
  self,
@@ -39,3 +53,9 @@ class Command(BaseCommand):
39
53
 
40
54
  def handle_api(self, *args: Any, **options: Any) -> None:
41
55
  run_server(options)
56
+
57
+ def handle_task_processor(self, *args: Any, **options: Any) -> None:
58
+ with start_task_processor(options):
59
+ # Delegate signal handling to Gunicorn.
60
+ # The task processor will finalise once Gunicorn is shut down.
61
+ run_server(options)
@@ -0,0 +1,87 @@
1
+ import logging
2
+ import time
3
+ from argparse import ArgumentParser
4
+ from typing import Any
5
+
6
+ from django.core.management import BaseCommand, CommandError
7
+ from django.db import OperationalError, connections
8
+ from django.db.migrations.executor import MigrationExecutor
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class Command(BaseCommand):
14
+ def add_arguments(self, parser: ArgumentParser) -> None:
15
+ parser.add_argument(
16
+ "--waitfor",
17
+ type=int,
18
+ dest="wait_for",
19
+ help="Number of seconds to wait for db to become available.",
20
+ default=5,
21
+ )
22
+ parser.add_argument(
23
+ "--migrations",
24
+ action="store_true",
25
+ dest="should_wait_for_migrations",
26
+ help="Whether to wait until all migrations are applied.",
27
+ default=False,
28
+ )
29
+ parser.add_argument(
30
+ "--database",
31
+ type=str,
32
+ dest="database",
33
+ help=(
34
+ 'The database to wait for ("default", "analytics").'
35
+ 'Defaults to the "default" database.'
36
+ ),
37
+ default="default",
38
+ )
39
+
40
+ def handle(
41
+ self,
42
+ *args: Any,
43
+ wait_for: int,
44
+ should_wait_for_migrations: bool,
45
+ database: str,
46
+ **options: Any,
47
+ ) -> None:
48
+ start = time.monotonic()
49
+ wait_between_checks = 0.25
50
+
51
+ logger.info("Checking if database is ready for connections.")
52
+
53
+ while True:
54
+ if time.monotonic() - start > wait_for:
55
+ msg = f"Failed to connect to DB within {wait_for} seconds."
56
+ logger.error(msg)
57
+ raise CommandError(msg)
58
+
59
+ conn = connections.create_connection(database)
60
+ try:
61
+ with conn.temporary_connection() as cursor:
62
+ cursor.execute("SELECT 1")
63
+ logger.info("Successfully connected to the database.")
64
+ break
65
+ except OperationalError as e:
66
+ logger.warning("Database not yet ready for connections.")
67
+ logger.warning("Error was: %s: %s", e.__class__.__name__, e)
68
+
69
+ time.sleep(wait_between_checks)
70
+
71
+ if should_wait_for_migrations:
72
+ logger.info("Checking for applied migrations.")
73
+
74
+ while True:
75
+ if time.monotonic() - start > wait_for:
76
+ msg = f"Didn't detect applied migrations for {wait_for} seconds."
77
+ logger.error(msg)
78
+ raise CommandError(msg)
79
+
80
+ conn = connections[database]
81
+ executor = MigrationExecutor(conn)
82
+ if not executor.migration_plan(executor.loader.graph.leaf_nodes()):
83
+ logger.info("No pending migrations detected, good to go.")
84
+ return
85
+
86
+ logger.warning("Migrations not yet applied.")
87
+ time.sleep(wait_between_checks)
@@ -3,6 +3,7 @@ from typing import Callable
3
3
  from django.http import HttpRequest, HttpResponse
4
4
 
5
5
  from common.gunicorn.constants import WSGI_DJANGO_ROUTE_ENVIRON_KEY
6
+ from common.gunicorn.utils import get_route_template
6
7
 
7
8
 
8
9
  class RouteLoggerMiddleware:
@@ -23,6 +24,8 @@ class RouteLoggerMiddleware:
23
24
  if resolver_match := request.resolver_match:
24
25
  # https://peps.python.org/pep-3333/#specification-details
25
26
  # "...the application is allowed to modify the dictionary in any way it desires"
26
- request.META[WSGI_DJANGO_ROUTE_ENVIRON_KEY] = resolver_match.route
27
+ request.META[WSGI_DJANGO_ROUTE_ENVIRON_KEY] = get_route_template(
28
+ resolver_match.route
29
+ )
27
30
 
28
31
  return response
@@ -1,9 +1,11 @@
1
1
  import argparse
2
2
  import os
3
+ from functools import lru_cache
3
4
  from typing import Any
4
5
 
5
6
  from django.core.handlers.wsgi import WSGIHandler
6
7
  from django.core.wsgi import get_wsgi_application
8
+ from drf_yasg.generators import EndpointEnumerator # type: ignore[import-untyped]
7
9
  from environs import Env
8
10
  from gunicorn.app.wsgiapp import ( # type: ignore[import-untyped]
9
11
  WSGIApplication as GunicornWSGIApplication,
@@ -59,3 +61,18 @@ def add_arguments(parser: argparse.ArgumentParser) -> None:
59
61
 
60
62
  def run_server(options: dict[str, Any] | None = None) -> None:
61
63
  DjangoWSGIApplication(options).run()
64
+
65
+
66
+ @lru_cache
67
+ def get_route_template(route: str) -> str:
68
+ """
69
+ Convert a Django regex route to a template string that can be
70
+ searched for in the API documentation.
71
+
72
+ e.g.,
73
+
74
+ `"^api/v1/environments/(?P<environment_api_key>[^/.]+)/api-keys/$"` ->
75
+ `"/api/v1/environments/{environment_api_key}/api-keys/"`
76
+ """
77
+ route_template: str = EndpointEnumerator().get_path_from_regex(route)
78
+ return route_template
@@ -0,0 +1,9 @@
1
+ """
2
+ Note: django doesn't support adding submodules to the migrations module directory
3
+ that don't include a Migration class. As such, I've defined this helpers submodule
4
+ and simplified the imports by defining the __all__ attribute.
5
+ """
6
+
7
+ from common.migrations.helpers.postgres_helpers import PostgresOnlyRunSQL
8
+
9
+ __all__ = ["PostgresOnlyRunSQL"]
@@ -0,0 +1,41 @@
1
+ from contextlib import suppress
2
+
3
+ from django.db import migrations
4
+ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
5
+ from django.db.migrations.state import ProjectState
6
+
7
+
8
+ class PostgresOnlyRunSQL(migrations.RunSQL):
9
+ @classmethod
10
+ def from_sql_file(
11
+ cls,
12
+ file_path: str,
13
+ reverse_sql: str = "",
14
+ ) -> "PostgresOnlyRunSQL":
15
+ with open(file_path) as forward_sql:
16
+ with suppress(FileNotFoundError):
17
+ with open(reverse_sql) as reverse_sql_file:
18
+ reverse_sql = reverse_sql_file.read()
19
+ return cls(forward_sql.read(), reverse_sql=reverse_sql)
20
+
21
+ def database_forwards(
22
+ self,
23
+ app_label: str,
24
+ schema_editor: BaseDatabaseSchemaEditor,
25
+ from_state: ProjectState,
26
+ to_state: ProjectState,
27
+ ) -> None:
28
+ if schema_editor.connection.vendor != "postgresql":
29
+ return
30
+ super().database_forwards(app_label, schema_editor, from_state, to_state)
31
+
32
+ def database_backwards(
33
+ self,
34
+ app_label: str,
35
+ schema_editor: BaseDatabaseSchemaEditor,
36
+ from_state: ProjectState,
37
+ to_state: ProjectState,
38
+ ) -> None:
39
+ if schema_editor.connection.vendor != "postgresql":
40
+ return
41
+ super().database_backwards(app_label, schema_editor, from_state, to_state)
File without changes
File without changes
@@ -0,0 +1,38 @@
1
+ from datetime import datetime
2
+
3
+ from django.contrib import admin
4
+ from django.db.models import QuerySet
5
+ from django.http import HttpRequest
6
+
7
+ from task_processor.models import RecurringTask
8
+
9
+
10
+ @admin.register(RecurringTask)
11
+ class RecurringTaskAdmin(admin.ModelAdmin[RecurringTask]):
12
+ list_display = (
13
+ "uuid",
14
+ "task_identifier",
15
+ "run_every",
16
+ "last_run_status",
17
+ "last_run_finished_at",
18
+ "is_locked",
19
+ )
20
+ readonly_fields = ("args", "kwargs")
21
+
22
+ def last_run_status(self, instance: RecurringTask) -> str | None:
23
+ if last_run := instance.task_runs.order_by("-started_at").first():
24
+ return last_run.result
25
+ return None
26
+
27
+ def last_run_finished_at(self, instance: RecurringTask) -> datetime | None:
28
+ if last_run := instance.task_runs.order_by("-started_at").first():
29
+ return last_run.finished_at
30
+ return None
31
+
32
+ @admin.action(description="Unlock selected tasks")
33
+ def unlock(
34
+ self,
35
+ request: HttpRequest,
36
+ queryset: QuerySet[RecurringTask],
37
+ ) -> None:
38
+ queryset.update(is_locked=False)
@@ -0,0 +1,18 @@
1
+ from django.apps import AppConfig
2
+ from django.conf import settings
3
+ from health_check.plugins import plugin_dir # type: ignore[import-untyped]
4
+
5
+ from task_processor.task_run_method import TaskRunMethod
6
+
7
+
8
+ class TaskProcessorAppConfig(AppConfig):
9
+ name = "task_processor"
10
+
11
+ def ready(self) -> None:
12
+ if (
13
+ settings.ENABLE_TASK_PROCESSOR_HEALTH_CHECK
14
+ and settings.TASK_RUN_METHOD == TaskRunMethod.TASK_PROCESSOR
15
+ ):
16
+ from .health import TaskProcessorHealthCheckBackend
17
+
18
+ plugin_dir.register(TaskProcessorHealthCheckBackend)