flagsmith-common 3.3.0__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.3.0 → flagsmith_common-3.4.0}/PKG-INFO +21 -1
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/README.md +20 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/pyproject.toml +3 -1
- flagsmith_common-3.4.0/src/common/lint_tests.py +279 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/flagsmith_schemas/dynamodb.py +7 -1
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/LICENSE +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/app.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/cli/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/cli/healthcheck.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/constants.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/logging.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/main.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/management/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/management/commands/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/management/commands/docgen.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/management/commands/start.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/management/commands/waitfordb.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/metrics.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/middleware.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/templates/docgen-metrics.md +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/urls.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/utils.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/views.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/environments/permissions.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/features/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/features/multivariate/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/features/multivariate/serializers.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/features/serializers.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/features/versioning/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/features/versioning/serializers.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/gunicorn/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/gunicorn/conf.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/gunicorn/constants.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/gunicorn/logging.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/gunicorn/metrics.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/gunicorn/metrics_server.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/gunicorn/middleware.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/gunicorn/utils.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/migrations/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/migrations/helpers/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/migrations/helpers/postgres_helpers.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/organisations/permissions.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/projects/permissions.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/prometheus/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/prometheus/utils.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/py.typed +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/test_tools/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/test_tools/plugin.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/test_tools/types.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/test_tools/utils.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/types.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/flagsmith_schemas/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/flagsmith_schemas/api.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/flagsmith_schemas/constants.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/flagsmith_schemas/py.typed +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/flagsmith_schemas/pydantic_types.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/flagsmith_schemas/types.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/flagsmith_schemas/utils.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/flagsmith_schemas/validators.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/admin.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/apps.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/decorators.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/exceptions.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/health.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/managers.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/metrics.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/0001_initial.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/0002_healthcheckmodel.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/0003_add_completed_to_task.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/0004_recreate_task_indexes.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/0005_update_conditional_index_conditions.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/0006_auto_20230221_0802.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/0007_add_is_locked.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/0008_add_get_task_to_process_function.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/0010_task_priority.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/0012_add_locked_at_and_timeout.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/0013_add_last_picked_at.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/sql/0008_get_tasks_to_process.sql +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/sql/0011_get_tasks_to_process.sql +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/migrations/sql/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/models.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/monitoring.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/processor.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/py.typed +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/routers.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/serializers.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/task_registry.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/task_run_method.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/tasks.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/threads.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/types.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/urls.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/task_processor/utils.py +0 -0
- {flagsmith_common-3.3.0 → 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
|
|
@@ -81,6 +81,26 @@ This enables the `route` label for Prometheus HTTP metrics.
|
|
|
81
81
|
|
|
82
82
|
5. To enable the `/metrics` endpoint, set the `PROMETHEUS_ENABLED` setting to `True`.
|
|
83
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
|
+
|
|
84
104
|
### Test tools
|
|
85
105
|
|
|
86
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 = []
|
|
@@ -60,12 +60,14 @@ Repository = "https://github.com/flagsmith/flagsmith-common"
|
|
|
60
60
|
|
|
61
61
|
[project.scripts]
|
|
62
62
|
flagsmith = "common.core.main:main"
|
|
63
|
+
flagsmith-lint-tests = "common.lint_tests:main"
|
|
63
64
|
|
|
64
65
|
[project.entry-points.pytest11]
|
|
65
66
|
flagsmith-test-tools = "common.test_tools.plugin"
|
|
66
67
|
|
|
67
68
|
[dependency-groups]
|
|
68
69
|
dev = [
|
|
70
|
+
"diff-cover>=10.2.0",
|
|
69
71
|
"dj-database-url (>=2.3.0, <3.0.0)",
|
|
70
72
|
"django-stubs (>=5.1.3, <6.0.0)",
|
|
71
73
|
"djangorestframework-stubs (>=3.15.3, <4.0.0)",
|
|
@@ -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())
|
|
@@ -210,7 +210,10 @@ class _EnvironmentBaseFields(TypedDict):
|
|
|
210
210
|
"""Last updated timestamp. If not set, current timestamp should be assumed."""
|
|
211
211
|
|
|
212
212
|
allow_client_traits: NotRequired[bool]
|
|
213
|
-
"""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
|
+
"""
|
|
214
217
|
hide_sensitive_data: NotRequired[bool]
|
|
215
218
|
"""Whether the SDK API should hide sensitive data for this environment. Defaults to `False`."""
|
|
216
219
|
hide_disabled_flags: NotRequired[bool | None]
|
|
@@ -401,3 +404,6 @@ class EnvironmentV2IdentityOverride(TypedDict):
|
|
|
401
404
|
"""The UUID for this identity, used by `edge-identities` APIs in Core. **INDEXED**."""
|
|
402
405
|
feature_state: FeatureState
|
|
403
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
|
+
"""
|
|
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.3.0 → flagsmith_common-3.4.0}/src/common/core/management/commands/__init__.py
RENAMED
|
File without changes
|
{flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/management/commands/docgen.py
RENAMED
|
File without changes
|
{flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/management/commands/start.py
RENAMED
|
File without changes
|
{flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/management/commands/waitfordb.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/core/templates/docgen-metrics.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/features/multivariate/__init__.py
RENAMED
|
File without changes
|
{flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/features/multivariate/serializers.py
RENAMED
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.3.0 → flagsmith_common-3.4.0}/src/common/features/versioning/__init__.py
RENAMED
|
File without changes
|
{flagsmith_common-3.3.0 → 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
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.3.0 → 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
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.3.0 → 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.3.0 → 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
|