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.
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/PKG-INFO +9 -10
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/README.md +2 -8
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/pyproject.toml +14 -4
- flagsmith_common-1.6.0/src/common/core/main.py +80 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/management/commands/start.py +23 -3
- flagsmith_common-1.6.0/src/common/core/management/commands/waitfordb.py +87 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/gunicorn/middleware.py +4 -1
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/gunicorn/utils.py +17 -0
- flagsmith_common-1.6.0/src/common/migrations/helpers/__init__.py +9 -0
- flagsmith_common-1.6.0/src/common/migrations/helpers/postgres_helpers.py +41 -0
- flagsmith_common-1.6.0/src/common/py.typed +0 -0
- flagsmith_common-1.6.0/src/task_processor/__init__.py +0 -0
- flagsmith_common-1.6.0/src/task_processor/admin.py +38 -0
- flagsmith_common-1.6.0/src/task_processor/apps.py +18 -0
- flagsmith_common-1.6.0/src/task_processor/decorators.py +206 -0
- flagsmith_common-1.6.0/src/task_processor/exceptions.py +10 -0
- flagsmith_common-1.6.0/src/task_processor/health.py +44 -0
- flagsmith_common-1.6.0/src/task_processor/managers.py +18 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/0001_initial.py +44 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/0002_healthcheckmodel.py +21 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/0003_add_completed_to_task.py +22 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/0004_recreate_task_indexes.py +43 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/0005_update_conditional_index_conditions.py +45 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/0006_auto_20230221_0802.py +45 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/0007_add_is_locked.py +23 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/0008_add_get_task_to_process_function.py +31 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +18 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/0010_task_priority.py +27 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +27 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/0012_add_locked_at_and_timeout.py +40 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/0013_add_last_picked_at.py +34 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/__init__.py +0 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +30 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/sql/0008_get_tasks_to_process.sql +30 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/sql/0011_get_tasks_to_process.sql +30 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +33 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +33 -0
- flagsmith_common-1.6.0/src/task_processor/migrations/sql/__init__.py +0 -0
- flagsmith_common-1.6.0/src/task_processor/models.py +246 -0
- flagsmith_common-1.6.0/src/task_processor/monitoring.py +12 -0
- flagsmith_common-1.6.0/src/task_processor/processor.py +138 -0
- flagsmith_common-1.6.0/src/task_processor/py.typed +0 -0
- flagsmith_common-1.6.0/src/task_processor/serializers.py +7 -0
- flagsmith_common-1.6.0/src/task_processor/task_registry.py +82 -0
- flagsmith_common-1.6.0/src/task_processor/task_run_method.py +7 -0
- flagsmith_common-1.6.0/src/task_processor/tasks.py +71 -0
- flagsmith_common-1.6.0/src/task_processor/threads.py +112 -0
- flagsmith_common-1.6.0/src/task_processor/types.py +18 -0
- flagsmith_common-1.6.0/src/task_processor/urls.py +5 -0
- flagsmith_common-1.6.0/src/task_processor/utils.py +71 -0
- flagsmith_common-1.6.0/src/task_processor/views.py +20 -0
- flagsmith_common-1.5.2/src/common/core/main.py +0 -40
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/LICENSE +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/__init__.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/__init__.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/app.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/logging.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/management/__init__.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/management/commands/__init__.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/metrics.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/urls.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/utils.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/views.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/environments/permissions.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/features/__init__.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/features/multivariate/__init__.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/features/multivariate/serializers.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/features/serializers.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/features/versioning/__init__.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/features/versioning/serializers.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/gunicorn/__init__.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/gunicorn/conf.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/gunicorn/constants.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/gunicorn/logging.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/gunicorn/metrics.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/metadata/serializers.py +0 -0
- flagsmith_common-1.5.2/src/common/py.typed → flagsmith_common-1.6.0/src/common/migrations/__init__.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/organisations/permissions.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/projects/permissions.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/prometheus/__init__.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/prometheus/utils.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/segments/serializers.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/test_tools/__init__.py +0 -0
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/test_tools/plugin.py +5 -5
- {flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/test_tools/types.py +0 -0
- {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.
|
|
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:
|
|
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`, `
|
|
103
|
-
- `http_server_requests_total`: Counter labeled with `method`, `
|
|
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`, `
|
|
71
|
-
- `http_server_requests_total`: Counter labeled with `method`, `
|
|
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.
|
|
3
|
+
version = "1.6.0"
|
|
4
4
|
description = "Flagsmith's common library"
|
|
5
5
|
requires-python = ">=3.11,<4.0"
|
|
6
6
|
dependencies = [
|
|
7
|
-
"
|
|
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
|
-
"
|
|
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 = [
|
|
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)
|
{flagsmith_common-1.5.2 → flagsmith_common-1.6.0}/src/common/core/management/commands/start.py
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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] =
|
|
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)
|