flagsmith-common 3.2.1__tar.gz → 3.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.
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/PKG-INFO +22 -1
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/README.md +20 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/pyproject.toml +4 -1
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/templates/docgen-metrics.md +3 -1
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/gunicorn/conf.py +11 -1
- flagsmith_common-3.4.0/src/common/gunicorn/metrics_server.py +65 -0
- flagsmith_common-3.4.0/src/common/lint_tests.py +279 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/flagsmith_schemas/dynamodb.py +86 -22
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/flagsmith_schemas/types.py +51 -2
- flagsmith_common-3.4.0/src/flagsmith_schemas/utils.py +15 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/LICENSE +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/app.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/cli/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/cli/healthcheck.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/constants.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/logging.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/main.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/management/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/management/commands/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/management/commands/docgen.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/management/commands/start.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/management/commands/waitfordb.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/metrics.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/middleware.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/urls.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/utils.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/views.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/environments/permissions.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/features/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/features/multivariate/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/features/multivariate/serializers.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/features/serializers.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/features/versioning/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/features/versioning/serializers.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/gunicorn/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/gunicorn/constants.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/gunicorn/logging.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/gunicorn/metrics.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/gunicorn/middleware.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/gunicorn/utils.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/migrations/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/migrations/helpers/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/migrations/helpers/postgres_helpers.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/organisations/permissions.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/projects/permissions.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/prometheus/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/prometheus/utils.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/py.typed +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/test_tools/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/test_tools/plugin.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/test_tools/types.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/test_tools/utils.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/types.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/flagsmith_schemas/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/flagsmith_schemas/api.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/flagsmith_schemas/constants.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/flagsmith_schemas/py.typed +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/flagsmith_schemas/pydantic_types.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/flagsmith_schemas/validators.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/admin.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/apps.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/decorators.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/exceptions.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/health.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/managers.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/metrics.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/0001_initial.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/0002_healthcheckmodel.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/0003_add_completed_to_task.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/0004_recreate_task_indexes.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/0005_update_conditional_index_conditions.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/0006_auto_20230221_0802.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/0007_add_is_locked.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/0008_add_get_task_to_process_function.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/0010_task_priority.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/0012_add_locked_at_and_timeout.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/0013_add_last_picked_at.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/sql/0008_get_tasks_to_process.sql +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/sql/0011_get_tasks_to_process.sql +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/sql/__init__.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/models.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/monitoring.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/processor.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/py.typed +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/routers.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/serializers.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/task_registry.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/task_run_method.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/tasks.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/threads.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/types.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/urls.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/utils.py +0 -0
- {flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flagsmith-common
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: Flagsmith's common library
|
|
5
5
|
Author: Matthew Elwell, Gagan Trivedi, Kim Gustyr, Zach Aysan, Francesco Lo Franco, Rodrigo López Dato, Evandro Myller, Wadii Zaim
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -21,6 +21,7 @@ Requires-Dist: prometheus-client>=0.0.16 ; extra == 'common-core'
|
|
|
21
21
|
Requires-Dist: psycopg2-binary>=2.9,<3 ; extra == 'common-core'
|
|
22
22
|
Requires-Dist: requests ; extra == 'common-core'
|
|
23
23
|
Requires-Dist: simplejson>=3,<4 ; extra == 'common-core'
|
|
24
|
+
Requires-Dist: simplejson ; extra == 'flagsmith-schemas'
|
|
24
25
|
Requires-Dist: typing-extensions ; extra == 'flagsmith-schemas'
|
|
25
26
|
Requires-Dist: flagsmith-flag-engine>6 ; extra == 'flagsmith-schemas'
|
|
26
27
|
Requires-Dist: backoff>=2.2.1,<3.0.0 ; extra == 'task-processor'
|
|
@@ -80,6 +81,26 @@ This enables the `route` label for Prometheus HTTP metrics.
|
|
|
80
81
|
|
|
81
82
|
5. To enable the `/metrics` endpoint, set the `PROMETHEUS_ENABLED` setting to `True`.
|
|
82
83
|
|
|
84
|
+
### Pre-commit hooks
|
|
85
|
+
|
|
86
|
+
This repo provides a [`flagsmith-lint-tests`](.pre-commit-hooks.yaml) hook that enforces test conventions:
|
|
87
|
+
|
|
88
|
+
- **FT001**: No module-level `Test*` classes — use function-based tests
|
|
89
|
+
- **FT002**: No `import unittest` / `from unittest import TestCase` — use pytest (`unittest.mock` is fine)
|
|
90
|
+
- **FT003**: Test names must follow `test_{subject}__{condition}__{expected}`
|
|
91
|
+
- **FT004**: Test bodies must contain `# Given`, `# When`, and `# Then` comments
|
|
92
|
+
|
|
93
|
+
To use in your repo, add to `.pre-commit-config.yaml`:
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
- repo: https://github.com/Flagsmith/flagsmith-common
|
|
97
|
+
rev: main
|
|
98
|
+
hooks:
|
|
99
|
+
- id: flagsmith-lint-tests
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Use `# noqa: FT003` (or any code) inline to suppress individual violations.
|
|
103
|
+
|
|
83
104
|
### Test tools
|
|
84
105
|
|
|
85
106
|
#### Fixtures
|
|
@@ -35,6 +35,26 @@ This enables the `route` label for Prometheus HTTP metrics.
|
|
|
35
35
|
|
|
36
36
|
5. To enable the `/metrics` endpoint, set the `PROMETHEUS_ENABLED` setting to `True`.
|
|
37
37
|
|
|
38
|
+
### Pre-commit hooks
|
|
39
|
+
|
|
40
|
+
This repo provides a [`flagsmith-lint-tests`](.pre-commit-hooks.yaml) hook that enforces test conventions:
|
|
41
|
+
|
|
42
|
+
- **FT001**: No module-level `Test*` classes — use function-based tests
|
|
43
|
+
- **FT002**: No `import unittest` / `from unittest import TestCase` — use pytest (`unittest.mock` is fine)
|
|
44
|
+
- **FT003**: Test names must follow `test_{subject}__{condition}__{expected}`
|
|
45
|
+
- **FT004**: Test bodies must contain `# Given`, `# When`, and `# Then` comments
|
|
46
|
+
|
|
47
|
+
To use in your repo, add to `.pre-commit-config.yaml`:
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
- repo: https://github.com/Flagsmith/flagsmith-common
|
|
51
|
+
rev: main
|
|
52
|
+
hooks:
|
|
53
|
+
- id: flagsmith-lint-tests
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use `# noqa: FT003` (or any code) inline to suppress individual violations.
|
|
57
|
+
|
|
38
58
|
### Test tools
|
|
39
59
|
|
|
40
60
|
#### Fixtures
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flagsmith-common"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.4.0"
|
|
4
4
|
description = "Flagsmith's common library"
|
|
5
5
|
requires-python = ">=3.11,<4.0"
|
|
6
6
|
dependencies = []
|
|
@@ -26,6 +26,7 @@ optional-dependencies = { test-tools = [
|
|
|
26
26
|
"django-health-check",
|
|
27
27
|
"prometheus-client (>=0.0.16)",
|
|
28
28
|
], flagsmith-schemas = [
|
|
29
|
+
"simplejson",
|
|
29
30
|
"typing_extensions",
|
|
30
31
|
"flagsmith-flag-engine>6",
|
|
31
32
|
] }
|
|
@@ -59,12 +60,14 @@ Repository = "https://github.com/flagsmith/flagsmith-common"
|
|
|
59
60
|
|
|
60
61
|
[project.scripts]
|
|
61
62
|
flagsmith = "common.core.main:main"
|
|
63
|
+
flagsmith-lint-tests = "common.lint_tests:main"
|
|
62
64
|
|
|
63
65
|
[project.entry-points.pytest11]
|
|
64
66
|
flagsmith-test-tools = "common.test_tools.plugin"
|
|
65
67
|
|
|
66
68
|
[dependency-groups]
|
|
67
69
|
dev = [
|
|
70
|
+
"diff-cover>=10.2.0",
|
|
68
71
|
"dj-database-url (>=2.3.0, <3.0.0)",
|
|
69
72
|
"django-stubs (>=5.1.3, <6.0.0)",
|
|
70
73
|
"djangorestframework-stubs (>=3.15.3, <4.0.0)",
|
{flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/templates/docgen-metrics.md
RENAMED
|
@@ -6,7 +6,9 @@ sidebar_position: 20
|
|
|
6
6
|
|
|
7
7
|
## Prometheus
|
|
8
8
|
|
|
9
|
-
To enable the Prometheus `/metrics` endpoint, set the `PROMETHEUS_ENABLED` environment variable to `true`.
|
|
9
|
+
To enable the Prometheus `/metrics` endpoint, set the `PROMETHEUS_ENABLED` environment variable to `true`.
|
|
10
|
+
|
|
11
|
+
When enabled, Flagsmith serves the `/metrics` endpoint on port 9100.
|
|
10
12
|
|
|
11
13
|
The metrics provided by Flagsmith are described below.
|
|
12
14
|
|
|
@@ -4,6 +4,7 @@ This module is used as a default configuration file for Gunicorn.
|
|
|
4
4
|
It is used to correctly support Prometheus metrics in a multi-process environment.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import os
|
|
7
8
|
import typing
|
|
8
9
|
|
|
9
10
|
from prometheus_client.multiprocess import mark_process_dead
|
|
@@ -13,6 +14,15 @@ if typing.TYPE_CHECKING: # pragma: no cover
|
|
|
13
14
|
from gunicorn.workers.base import Worker # type: ignore[import-untyped]
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
def
|
|
17
|
+
def when_ready(server: "Arbiter") -> None:
|
|
18
|
+
"""Start the standalone Prometheus metrics server after Gunicorn is ready."""
|
|
19
|
+
prometheus_enabled = os.getenv("PROMETHEUS_ENABLED", "")
|
|
20
|
+
if prometheus_enabled.lower() == "true": # Django settings are not available
|
|
21
|
+
from common.gunicorn.metrics_server import start_metrics_server
|
|
22
|
+
|
|
23
|
+
start_metrics_server()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def child_exit(server: "Arbiter", worker: "Worker") -> None:
|
|
17
27
|
"""Detach the process Prometheus metrics collector when a worker exits."""
|
|
18
28
|
mark_process_dead(worker.pid) # type: ignore[no-untyped-call]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Standalone Prometheus metrics HTTP server.
|
|
3
|
+
|
|
4
|
+
This module provides a separate HTTP server for Prometheus metrics,
|
|
5
|
+
independent of the main Gunicorn application server. This improves
|
|
6
|
+
metrics reliability under high API load.
|
|
7
|
+
|
|
8
|
+
The server runs in a daemon thread and serves metrics from the shared
|
|
9
|
+
PROMETHEUS_MULTIPROC_DIR directory.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import threading
|
|
15
|
+
|
|
16
|
+
from prometheus_client import CollectorRegistry, start_http_server
|
|
17
|
+
from prometheus_client.multiprocess import MultiProcessCollector
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
METRICS_SERVER_PORT = 9100
|
|
22
|
+
|
|
23
|
+
_server_started = False
|
|
24
|
+
_server_lock = threading.Lock()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_multiprocess_registry() -> CollectorRegistry:
|
|
28
|
+
"""Create a registry configured for multiprocess metric collection."""
|
|
29
|
+
registry = CollectorRegistry()
|
|
30
|
+
MultiProcessCollector(registry) # type: ignore[no-untyped-call]
|
|
31
|
+
return registry
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def start_metrics_server(
|
|
35
|
+
port: int = METRICS_SERVER_PORT,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Start the standalone Prometheus metrics HTTP server.
|
|
39
|
+
|
|
40
|
+
This function is idempotent - calling it multiple times will only
|
|
41
|
+
start one server. The server runs in a daemon thread.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
port: The port to serve metrics on. Defaults to 9100.
|
|
45
|
+
"""
|
|
46
|
+
global _server_started
|
|
47
|
+
|
|
48
|
+
with _server_lock:
|
|
49
|
+
if _server_started:
|
|
50
|
+
logger.debug("Metrics server already started")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
prometheus_multiproc_dir = os.environ.get("PROMETHEUS_MULTIPROC_DIR")
|
|
54
|
+
if not prometheus_multiproc_dir:
|
|
55
|
+
logger.warning("PROMETHEUS_MULTIPROC_DIR not set, skipping metrics server")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
registry = get_multiprocess_registry()
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
start_http_server(port=port, registry=registry)
|
|
62
|
+
_server_started = True
|
|
63
|
+
logger.info("Prometheus metrics server started on port %d", port)
|
|
64
|
+
except OSError as e:
|
|
65
|
+
logger.error("Failed to start metrics server on port %d: %s", port, e)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Linter for Flagsmith test conventions.
|
|
2
|
+
|
|
3
|
+
Enforces:
|
|
4
|
+
- FT001: No module-level class Test* (function-only tests)
|
|
5
|
+
- FT002: No `import unittest` / `from unittest import TestCase` (unittest.mock is fine)
|
|
6
|
+
- FT003: Test name must have exactly 2 `__` separators: test_{subject}__{condition}__{expected}
|
|
7
|
+
- FT004: Test body must contain # Given, # When, and # Then comments
|
|
8
|
+
|
|
9
|
+
Output format matches ruff/flake8/mypy: {file}:{line}:{col}: {code} {message}
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import ast
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import NamedTuple
|
|
20
|
+
|
|
21
|
+
UNITTEST_BANNED_IMPORTS = frozenset(
|
|
22
|
+
{"TestCase", "TestSuite", "TestLoader", "TextTestRunner"}
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Violation(NamedTuple):
|
|
27
|
+
file: str
|
|
28
|
+
line: int
|
|
29
|
+
col: int
|
|
30
|
+
code: str
|
|
31
|
+
message: str
|
|
32
|
+
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
return f"{self.file}:{self.line}:{self.col}: {self.code} {self.message}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _has_fixture_decorator(node: ast.FunctionDef) -> bool:
|
|
38
|
+
for decorator in node.decorator_list:
|
|
39
|
+
if isinstance(decorator, ast.Attribute) and decorator.attr == "fixture":
|
|
40
|
+
return True
|
|
41
|
+
if isinstance(decorator, ast.Name) and decorator.id == "fixture":
|
|
42
|
+
return True
|
|
43
|
+
# Handle @pytest.fixture(...)
|
|
44
|
+
if (
|
|
45
|
+
isinstance(decorator, ast.Call)
|
|
46
|
+
and isinstance(decorator.func, ast.Attribute)
|
|
47
|
+
and decorator.func.attr == "fixture"
|
|
48
|
+
):
|
|
49
|
+
return True
|
|
50
|
+
if (
|
|
51
|
+
isinstance(decorator, ast.Call)
|
|
52
|
+
and isinstance(decorator.func, ast.Name)
|
|
53
|
+
and decorator.func.id == "fixture"
|
|
54
|
+
):
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_COMMENT_RE = re.compile(r"#(.*)$")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _extract_comments(source: str) -> dict[int, str]:
|
|
63
|
+
"""Return a mapping of line number (1-based) -> comment text."""
|
|
64
|
+
comments: dict[int, str] = {}
|
|
65
|
+
for lineno, line in enumerate(source.splitlines(), start=1):
|
|
66
|
+
match = _COMMENT_RE.search(line)
|
|
67
|
+
if match:
|
|
68
|
+
comments[lineno] = "#" + match.group(1)
|
|
69
|
+
return comments
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
_NOQA_RE = re.compile(r"#\s*noqa\b(?::\s*(?P<codes>[A-Z0-9,\s]+))?")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _is_noqa_suppressed(comment: str, code: str) -> bool:
|
|
76
|
+
"""Check if a comment contains a noqa directive that suppresses the given code."""
|
|
77
|
+
match = _NOQA_RE.search(comment)
|
|
78
|
+
if not match:
|
|
79
|
+
return False
|
|
80
|
+
codes_str = match.group("codes")
|
|
81
|
+
# Bare noqa (without specific codes) suppresses everything
|
|
82
|
+
if codes_str is None:
|
|
83
|
+
return True
|
|
84
|
+
codes = {c.strip() for c in codes_str.split(",")}
|
|
85
|
+
return code in codes
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def check_ft001(tree: ast.Module, filepath: str) -> list[Violation]:
|
|
89
|
+
"""FT001: Module-level class Test* detected."""
|
|
90
|
+
violations = []
|
|
91
|
+
for node in ast.iter_child_nodes(tree):
|
|
92
|
+
if isinstance(node, ast.ClassDef) and node.name.startswith("Test"):
|
|
93
|
+
violations.append(
|
|
94
|
+
Violation(
|
|
95
|
+
file=filepath,
|
|
96
|
+
line=node.lineno,
|
|
97
|
+
col=node.col_offset + 1,
|
|
98
|
+
code="FT001",
|
|
99
|
+
message=f"Module-level test class `{node.name}` detected; use function-based tests",
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
return violations
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def check_ft002(tree: ast.Module, filepath: str) -> list[Violation]:
|
|
106
|
+
"""FT002: import unittest / from unittest import TestCase etc. (NOT unittest.mock)."""
|
|
107
|
+
violations = []
|
|
108
|
+
for node in ast.walk(tree):
|
|
109
|
+
if isinstance(node, ast.Import):
|
|
110
|
+
for alias in node.names:
|
|
111
|
+
# Flag `import unittest` but not `import unittest.mock`
|
|
112
|
+
if alias.name == "unittest":
|
|
113
|
+
violations.append(
|
|
114
|
+
Violation(
|
|
115
|
+
file=filepath,
|
|
116
|
+
line=node.lineno,
|
|
117
|
+
col=node.col_offset + 1,
|
|
118
|
+
code="FT002",
|
|
119
|
+
message="`import unittest` is not allowed; use pytest instead",
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
elif isinstance(node, ast.ImportFrom):
|
|
123
|
+
if node.module == "unittest":
|
|
124
|
+
for alias in node.names:
|
|
125
|
+
if alias.name in UNITTEST_BANNED_IMPORTS:
|
|
126
|
+
violations.append(
|
|
127
|
+
Violation(
|
|
128
|
+
file=filepath,
|
|
129
|
+
line=node.lineno,
|
|
130
|
+
col=node.col_offset + 1,
|
|
131
|
+
code="FT002",
|
|
132
|
+
message=f"`from unittest import {alias.name}` is not allowed; use pytest instead",
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
return violations
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def check_ft003(tree: ast.Module, filepath: str) -> list[Violation]:
|
|
139
|
+
"""FT003: Test name doesn't follow test_{subject}__{condition}__{expected} convention."""
|
|
140
|
+
violations = []
|
|
141
|
+
for node in ast.iter_child_nodes(tree):
|
|
142
|
+
if (
|
|
143
|
+
isinstance(node, ast.FunctionDef)
|
|
144
|
+
and node.name.startswith("test_")
|
|
145
|
+
and not _has_fixture_decorator(node)
|
|
146
|
+
):
|
|
147
|
+
# Strip `test_` prefix and count `__` separators
|
|
148
|
+
after_prefix = node.name[5:]
|
|
149
|
+
parts = after_prefix.split("__")
|
|
150
|
+
if len(parts) != 3:
|
|
151
|
+
violations.append(
|
|
152
|
+
Violation(
|
|
153
|
+
file=filepath,
|
|
154
|
+
line=node.lineno,
|
|
155
|
+
col=node.col_offset + 1,
|
|
156
|
+
code="FT003",
|
|
157
|
+
message=f"Test name `{node.name}` doesn't match `test_{{subject}}__{{condition}}__{{expected}}` (found {len(parts)} parts, expected 3)",
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
return violations
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _find_missing_gwt(func_comments: list[str]) -> list[str]:
|
|
164
|
+
"""Return list of missing Given/When/Then keywords from comments."""
|
|
165
|
+
has_given = False
|
|
166
|
+
has_when = False
|
|
167
|
+
has_then = False
|
|
168
|
+
for text in func_comments:
|
|
169
|
+
normalized = text.lstrip("#").strip().lower()
|
|
170
|
+
if normalized.startswith("given"):
|
|
171
|
+
has_given = True
|
|
172
|
+
# "Given / When" satisfies both
|
|
173
|
+
if "when" in normalized:
|
|
174
|
+
has_when = True
|
|
175
|
+
if normalized.startswith("when"):
|
|
176
|
+
has_when = True
|
|
177
|
+
# "When / Then" satisfies both
|
|
178
|
+
if "then" in normalized:
|
|
179
|
+
has_then = True
|
|
180
|
+
if normalized.startswith("then"):
|
|
181
|
+
has_then = True
|
|
182
|
+
|
|
183
|
+
missing = []
|
|
184
|
+
if not has_given:
|
|
185
|
+
missing.append("Given")
|
|
186
|
+
if not has_when:
|
|
187
|
+
missing.append("When")
|
|
188
|
+
if not has_then:
|
|
189
|
+
missing.append("Then")
|
|
190
|
+
return missing
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def check_ft004(
|
|
194
|
+
tree: ast.Module, filepath: str, comments: dict[int, str]
|
|
195
|
+
) -> list[Violation]:
|
|
196
|
+
"""FT004: Missing # Given, # When, or # Then comments in test body."""
|
|
197
|
+
violations = []
|
|
198
|
+
for node in ast.iter_child_nodes(tree):
|
|
199
|
+
if (
|
|
200
|
+
isinstance(node, ast.FunctionDef)
|
|
201
|
+
and node.name.startswith("test_")
|
|
202
|
+
and not _has_fixture_decorator(node)
|
|
203
|
+
):
|
|
204
|
+
func_comments = [
|
|
205
|
+
text
|
|
206
|
+
for line_no, text in comments.items()
|
|
207
|
+
if node.lineno <= line_no <= (node.end_lineno or node.lineno)
|
|
208
|
+
]
|
|
209
|
+
missing = _find_missing_gwt(func_comments)
|
|
210
|
+
if missing:
|
|
211
|
+
violations.append(
|
|
212
|
+
Violation(
|
|
213
|
+
file=filepath,
|
|
214
|
+
line=node.lineno,
|
|
215
|
+
col=node.col_offset + 1,
|
|
216
|
+
code="FT004",
|
|
217
|
+
message=f"Test `{node.name}` is missing GWT comments: {', '.join(missing)}",
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
return violations
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def lint_file(filepath: str) -> list[Violation]:
|
|
224
|
+
"""Run all checks on a single file."""
|
|
225
|
+
path = Path(filepath)
|
|
226
|
+
|
|
227
|
+
# Only check test_*.py files
|
|
228
|
+
if not (path.name.startswith("test_") and path.suffix == ".py"):
|
|
229
|
+
return []
|
|
230
|
+
|
|
231
|
+
source = path.read_text(encoding="utf-8")
|
|
232
|
+
try:
|
|
233
|
+
tree = ast.parse(source, filename=filepath)
|
|
234
|
+
except SyntaxError:
|
|
235
|
+
return [
|
|
236
|
+
Violation(
|
|
237
|
+
file=filepath,
|
|
238
|
+
line=1,
|
|
239
|
+
col=1,
|
|
240
|
+
code="FT000",
|
|
241
|
+
message="Could not parse file (SyntaxError)",
|
|
242
|
+
)
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
comments = _extract_comments(source)
|
|
246
|
+
|
|
247
|
+
violations = []
|
|
248
|
+
violations.extend(check_ft001(tree, filepath))
|
|
249
|
+
violations.extend(check_ft002(tree, filepath))
|
|
250
|
+
violations.extend(check_ft003(tree, filepath))
|
|
251
|
+
violations.extend(check_ft004(tree, filepath, comments))
|
|
252
|
+
|
|
253
|
+
# Filter out violations suppressed by noqa comments
|
|
254
|
+
return [
|
|
255
|
+
v
|
|
256
|
+
for v in violations
|
|
257
|
+
if v.line not in comments or not _is_noqa_suppressed(comments[v.line], v.code)
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def main(argv: list[str] | None = None) -> int:
|
|
262
|
+
parser = argparse.ArgumentParser(
|
|
263
|
+
description="Lint Flagsmith test conventions",
|
|
264
|
+
)
|
|
265
|
+
parser.add_argument("files", nargs="*", help="Files to check")
|
|
266
|
+
args = parser.parse_args(argv)
|
|
267
|
+
|
|
268
|
+
has_errors = False
|
|
269
|
+
for filepath in args.files:
|
|
270
|
+
violations = lint_file(filepath)
|
|
271
|
+
for v in violations:
|
|
272
|
+
has_errors = True
|
|
273
|
+
print(v)
|
|
274
|
+
|
|
275
|
+
return 1 if has_errors else 0
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__": # pragma: no cover
|
|
279
|
+
sys.exit(main())
|
|
@@ -20,6 +20,7 @@ from flagsmith_schemas.types import (
|
|
|
20
20
|
DynamoFloat,
|
|
21
21
|
DynamoInt,
|
|
22
22
|
FeatureType,
|
|
23
|
+
JsonGzipped,
|
|
23
24
|
UUIDStr,
|
|
24
25
|
)
|
|
25
26
|
|
|
@@ -200,7 +201,7 @@ class Webhook(TypedDict):
|
|
|
200
201
|
"""Secret used to sign webhook payloads."""
|
|
201
202
|
|
|
202
203
|
|
|
203
|
-
class
|
|
204
|
+
class _EnvironmentBaseFields(TypedDict):
|
|
204
205
|
"""Common fields for Environment documents."""
|
|
205
206
|
|
|
206
207
|
name: NotRequired[str]
|
|
@@ -208,13 +209,11 @@ class _EnvironmentFields(TypedDict):
|
|
|
208
209
|
updated_at: NotRequired[DateTimeStr | None]
|
|
209
210
|
"""Last updated timestamp. If not set, current timestamp should be assumed."""
|
|
210
211
|
|
|
211
|
-
project: Project
|
|
212
|
-
"""Project-specific data for this environment."""
|
|
213
|
-
feature_states: list[FeatureState]
|
|
214
|
-
"""List of feature states representing the environment defaults."""
|
|
215
|
-
|
|
216
212
|
allow_client_traits: NotRequired[bool]
|
|
217
|
-
"""Whether the SDK API should allow clients to set traits for this environment.
|
|
213
|
+
"""Whether the SDK API should allow clients to set traits for this environment.
|
|
214
|
+
If set to `False`, assumes only persisted traits and traits from server-side SDKs are used in evaluation, and traits incoming from client-side SDKs are ignored.
|
|
215
|
+
Defaults to `True`.
|
|
216
|
+
"""
|
|
218
217
|
hide_sensitive_data: NotRequired[bool]
|
|
219
218
|
"""Whether the SDK API should hide sensitive data for this environment. Defaults to `False`."""
|
|
220
219
|
hide_disabled_flags: NotRequired[bool | None]
|
|
@@ -240,7 +239,52 @@ class _EnvironmentFields(TypedDict):
|
|
|
240
239
|
"""Webhook configuration."""
|
|
241
240
|
|
|
242
241
|
|
|
243
|
-
|
|
242
|
+
class _EnvironmentV1Fields(TypedDict):
|
|
243
|
+
"""Common fields for environment documents in `flagsmith_environments`."""
|
|
244
|
+
|
|
245
|
+
api_key: str
|
|
246
|
+
"""Public client-side API key for the environment. **INDEXED**."""
|
|
247
|
+
id: DynamoInt
|
|
248
|
+
"""Unique identifier for the environment in Core."""
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class _EnvironmentV2MetaFields(TypedDict):
|
|
252
|
+
"""Common fields for environment documents in `flagsmith_environments_v2`."""
|
|
253
|
+
|
|
254
|
+
environment_id: str
|
|
255
|
+
"""Unique identifier for the environment in Core. Same as `Environment.id`, but string-typed to reduce coupling with Core's type definitions **INDEXED**."""
|
|
256
|
+
environment_api_key: str
|
|
257
|
+
"""Public client-side API key for the environment. **INDEXED**."""
|
|
258
|
+
document_key: Literal["_META"]
|
|
259
|
+
"""The fixed document key for the environment v2 document. Always `"_META"`. **INDEXED**."""
|
|
260
|
+
|
|
261
|
+
id: DynamoInt
|
|
262
|
+
"""Unique identifier for the environment in Core. Exists for compatibility with the API environment document schema."""
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class _EnvironmentBaseFieldsUncompressed(TypedDict):
|
|
266
|
+
"""Common fields for uncompressed environment documents."""
|
|
267
|
+
|
|
268
|
+
project: Project
|
|
269
|
+
"""Project-specific data for this environment."""
|
|
270
|
+
feature_states: list[FeatureState]
|
|
271
|
+
"""List of feature states representing the environment defaults."""
|
|
272
|
+
compressed: NotRequired[Literal[False]]
|
|
273
|
+
"""Either `False` or absent to indicate the data is uncompressed."""
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class _EnvironmentBaseFieldsCompressed(TypedDict):
|
|
277
|
+
"""Common fields for compressed environment documents."""
|
|
278
|
+
|
|
279
|
+
project: JsonGzipped[Project]
|
|
280
|
+
"""Project-specific data for this environment. **COMPRESSED**."""
|
|
281
|
+
feature_states: JsonGzipped[list[FeatureState]]
|
|
282
|
+
"""List of feature states representing the environment defaults. **COMPRESSED**."""
|
|
283
|
+
compressed: Literal[True]
|
|
284
|
+
"""Always `True` to indicate the data is compressed."""
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
### Root document schemas below. Indexed fields are marked as **INDEXED** in the docstrings. Compressed fields are marked as **COMPRESSED**. ###
|
|
244
288
|
|
|
245
289
|
|
|
246
290
|
class EnvironmentAPIKey(TypedDict):
|
|
@@ -295,33 +339,50 @@ class Identity(TypedDict):
|
|
|
295
339
|
"""Unique identifier for the identity in Core. If identity created via Core's `edge-identities` API, this can be missing or `None`."""
|
|
296
340
|
|
|
297
341
|
|
|
298
|
-
class Environment(
|
|
342
|
+
class Environment(
|
|
343
|
+
_EnvironmentBaseFieldsUncompressed,
|
|
344
|
+
_EnvironmentV1Fields,
|
|
345
|
+
_EnvironmentBaseFields,
|
|
346
|
+
):
|
|
299
347
|
"""Represents a Flagsmith environment. Carries all necessary data for flag evaluation within the environment.
|
|
300
348
|
|
|
301
349
|
**DynamoDB table**: `flagsmith_environments`
|
|
302
350
|
"""
|
|
303
351
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
352
|
+
|
|
353
|
+
class EnvironmentCompressed(
|
|
354
|
+
_EnvironmentBaseFieldsCompressed,
|
|
355
|
+
_EnvironmentV1Fields,
|
|
356
|
+
_EnvironmentBaseFields,
|
|
357
|
+
):
|
|
358
|
+
"""Represents a Flagsmith environment. Carries all necessary data for flag evaluation within the environment.
|
|
359
|
+
Has compressed fields.
|
|
360
|
+
|
|
361
|
+
**DynamoDB table**: `flagsmith_environments`
|
|
362
|
+
"""
|
|
308
363
|
|
|
309
364
|
|
|
310
|
-
class EnvironmentV2Meta(
|
|
365
|
+
class EnvironmentV2Meta(
|
|
366
|
+
_EnvironmentBaseFieldsUncompressed,
|
|
367
|
+
_EnvironmentV2MetaFields,
|
|
368
|
+
_EnvironmentBaseFields,
|
|
369
|
+
):
|
|
311
370
|
"""Represents a Flagsmith environment. Carries all necessary data for flag evaluation within the environment.
|
|
312
371
|
|
|
313
372
|
**DynamoDB table**: `flagsmith_environments_v2`
|
|
314
373
|
"""
|
|
315
374
|
|
|
316
|
-
environment_id: str
|
|
317
|
-
"""Unique identifier for the environment in Core. Same as `Environment.id`, but string-typed to reduce coupling with Core's type definitions **INDEXED**."""
|
|
318
|
-
environment_api_key: str
|
|
319
|
-
"""Public client-side API key for the environment. **INDEXED**."""
|
|
320
|
-
document_key: Literal["_META"]
|
|
321
|
-
"""The fixed document key for the environment v2 document. Always `"_META"`. **INDEXED**."""
|
|
322
375
|
|
|
323
|
-
|
|
324
|
-
|
|
376
|
+
class EnvironmentV2MetaCompressed(
|
|
377
|
+
_EnvironmentBaseFieldsCompressed,
|
|
378
|
+
_EnvironmentV2MetaFields,
|
|
379
|
+
_EnvironmentBaseFields,
|
|
380
|
+
):
|
|
381
|
+
"""Represents a Flagsmith environment. Carries all necessary data for flag evaluation within the environment.
|
|
382
|
+
Has compressed fields.
|
|
383
|
+
|
|
384
|
+
**DynamoDB table**: `flagsmith_environments_v2`
|
|
385
|
+
"""
|
|
325
386
|
|
|
326
387
|
|
|
327
388
|
class EnvironmentV2IdentityOverride(TypedDict):
|
|
@@ -343,3 +404,6 @@ class EnvironmentV2IdentityOverride(TypedDict):
|
|
|
343
404
|
"""The UUID for this identity, used by `edge-identities` APIs in Core. **INDEXED**."""
|
|
344
405
|
feature_state: FeatureState
|
|
345
406
|
"""The feature state override for this identity."""
|
|
407
|
+
created_date: NotRequired[DateTimeStr]
|
|
408
|
+
"""ISO 8601 creation timestamp. Note: might change between updates due to how it's written by Core.
|
|
409
|
+
"""
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import (
|
|
3
|
+
TYPE_CHECKING,
|
|
4
|
+
Annotated,
|
|
5
|
+
Any,
|
|
6
|
+
Generic,
|
|
7
|
+
Literal,
|
|
8
|
+
SupportsBytes,
|
|
9
|
+
TypeAlias,
|
|
10
|
+
TypeVar,
|
|
11
|
+
get_args,
|
|
12
|
+
)
|
|
3
13
|
|
|
4
14
|
from flagsmith_schemas.constants import PYDANTIC_INSTALLED
|
|
5
15
|
|
|
6
16
|
if PYDANTIC_INSTALLED:
|
|
7
|
-
from pydantic import
|
|
17
|
+
from pydantic import (
|
|
18
|
+
GetCoreSchemaHandler,
|
|
19
|
+
TypeAdapter,
|
|
20
|
+
WithJsonSchema,
|
|
21
|
+
)
|
|
22
|
+
from pydantic_core import core_schema
|
|
8
23
|
|
|
9
24
|
from flagsmith_schemas.pydantic_types import (
|
|
10
25
|
ValidateDecimalAsFloat,
|
|
@@ -13,6 +28,7 @@ if PYDANTIC_INSTALLED:
|
|
|
13
28
|
ValidateStrAsISODateTime,
|
|
14
29
|
ValidateStrAsUUID,
|
|
15
30
|
)
|
|
31
|
+
from flagsmith_schemas.utils import json_gzip
|
|
16
32
|
elif not TYPE_CHECKING:
|
|
17
33
|
# This code runs at runtime when Pydantic is not installed.
|
|
18
34
|
# We could use PEP 649 strings with `Annotated`, but Pydantic is inconsistent in how it parses them.
|
|
@@ -26,6 +42,39 @@ elif not TYPE_CHECKING:
|
|
|
26
42
|
ValidateStrAsISODateTime = ...
|
|
27
43
|
ValidateStrAsUUID = ...
|
|
28
44
|
|
|
45
|
+
T = TypeVar("T")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DynamoBinary(SupportsBytes):
|
|
49
|
+
"""boto3's wrapper type for bytes stored in DynamoDB."""
|
|
50
|
+
|
|
51
|
+
value: bytes | bytearray
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class JsonGzipped(DynamoBinary, Generic[T]):
|
|
55
|
+
"""A gzipped JSON blob representing a value of type `T`."""
|
|
56
|
+
|
|
57
|
+
if PYDANTIC_INSTALLED:
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def __get_pydantic_core_schema__(
|
|
61
|
+
cls,
|
|
62
|
+
source_type: "type[JsonGzipped[T]]",
|
|
63
|
+
handler: GetCoreSchemaHandler,
|
|
64
|
+
) -> core_schema.CoreSchema:
|
|
65
|
+
_adapter: TypeAdapter[T] = TypeAdapter(get_args(source_type)[0])
|
|
66
|
+
|
|
67
|
+
def _validate_json_gzipped(data: Any) -> bytes:
|
|
68
|
+
return json_gzip(_adapter.validate_python(data))
|
|
69
|
+
|
|
70
|
+
# We're returning bytes here for two reasons:
|
|
71
|
+
# 1. boto3.dynamodb seems to expect bytes as input for Binary columns.
|
|
72
|
+
# 2. We want to avoid having boto3 as a dependency.
|
|
73
|
+
return core_schema.no_info_before_validator_function(
|
|
74
|
+
_validate_json_gzipped,
|
|
75
|
+
core_schema.bytes_schema(strict=False),
|
|
76
|
+
)
|
|
77
|
+
|
|
29
78
|
|
|
30
79
|
DynamoInt: TypeAlias = Annotated[Decimal, ValidateDecimalAsInt]
|
|
31
80
|
"""An integer value stored in DynamoDB.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/management/commands/__init__.py
RENAMED
|
File without changes
|
{flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/management/commands/docgen.py
RENAMED
|
File without changes
|
{flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/management/commands/start.py
RENAMED
|
File without changes
|
{flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/core/management/commands/waitfordb.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/features/multivariate/__init__.py
RENAMED
|
File without changes
|
{flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/features/multivariate/serializers.py
RENAMED
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/features/versioning/__init__.py
RENAMED
|
File without changes
|
{flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/features/versioning/serializers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/common/migrations/helpers/postgres_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.2.1 → flagsmith_common-3.4.0}/src/task_processor/migrations/sql/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|