flagsmith-common 3.7.0__tar.gz → 3.8.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.7.0 → flagsmith_common-3.8.0}/PKG-INFO +1 -1
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/pyproject.toml +1 -1
- flagsmith_common-3.8.0/src/common/core/docgen/events.py +420 -0
- flagsmith_common-3.8.0/src/common/core/management/commands/docgen.py +140 -0
- flagsmith_common-3.8.0/src/common/core/templates/docgen-events.md +20 -0
- flagsmith_common-3.8.0/src/task_processor/py.typed +0 -0
- flagsmith_common-3.7.0/src/common/core/management/commands/docgen.py +0 -63
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/LICENSE +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/README.md +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/app.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/cli/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/cli/healthcheck.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/constants.py +0 -0
- {flagsmith_common-3.7.0/src/common/core/management → flagsmith_common-3.8.0/src/common/core/docgen}/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/logging.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/main.py +0 -0
- {flagsmith_common-3.7.0/src/common/core/management/commands → flagsmith_common-3.8.0/src/common/core/management}/__init__.py +0 -0
- {flagsmith_common-3.7.0/src/common/features → flagsmith_common-3.8.0/src/common/core/management/commands}/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/management/commands/start.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/management/commands/waitfordb.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/metrics.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/middleware.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/otel.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/sentry.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/templates/docgen-metrics.md +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/urls.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/utils.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/views.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/environments/permissions.py +0 -0
- {flagsmith_common-3.7.0/src/common/features/multivariate → flagsmith_common-3.8.0/src/common/features}/__init__.py +0 -0
- {flagsmith_common-3.7.0/src/common/features/versioning → flagsmith_common-3.8.0/src/common/features/multivariate}/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/features/multivariate/serializers.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/features/serializers.py +0 -0
- {flagsmith_common-3.7.0/src/common/gunicorn → flagsmith_common-3.8.0/src/common/features/versioning}/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/features/versioning/serializers.py +0 -0
- {flagsmith_common-3.7.0/src/common/migrations → flagsmith_common-3.8.0/src/common/gunicorn}/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/gunicorn/conf.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/gunicorn/constants.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/gunicorn/logging.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/gunicorn/metrics.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/gunicorn/metrics_server.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/gunicorn/middleware.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/gunicorn/processors.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/gunicorn/utils.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/lint_tests.py +0 -0
- {flagsmith_common-3.7.0/src/flagsmith_schemas → flagsmith_common-3.8.0/src/common/migrations}/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/migrations/helpers/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/migrations/helpers/postgres_helpers.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/organisations/permissions.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/projects/permissions.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/prometheus/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/prometheus/utils.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/py.typed +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/test_tools/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/test_tools/plugin.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/test_tools/types.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/test_tools/utils.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/types.py +0 -0
- {flagsmith_common-3.7.0/src/task_processor → flagsmith_common-3.8.0/src/flagsmith_schemas}/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/flagsmith_schemas/api.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/flagsmith_schemas/constants.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/flagsmith_schemas/dynamodb.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/flagsmith_schemas/py.typed +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/flagsmith_schemas/pydantic_types.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/flagsmith_schemas/types.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/flagsmith_schemas/utils.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/flagsmith_schemas/validators.py +0 -0
- {flagsmith_common-3.7.0/src/task_processor/migrations → flagsmith_common-3.8.0/src/task_processor}/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/admin.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/apps.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/decorators.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/exceptions.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/health.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/managers.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/metrics.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/0001_initial.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/0002_healthcheckmodel.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/0003_add_completed_to_task.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/0004_recreate_task_indexes.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/0005_update_conditional_index_conditions.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/0006_auto_20230221_0802.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/0007_add_is_locked.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/0008_add_get_task_to_process_function.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/0010_task_priority.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/0012_add_locked_at_and_timeout.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/0013_add_last_picked_at.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/0014_add_trace_context.py +0 -0
- {flagsmith_common-3.7.0/src/task_processor/migrations/sql → flagsmith_common-3.8.0/src/task_processor/migrations}/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/sql/0008_get_tasks_to_process.sql +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/sql/0011_get_tasks_to_process.sql +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +0 -0
- /flagsmith_common-3.7.0/src/task_processor/py.typed → /flagsmith_common-3.8.0/src/task_processor/migrations/sql/__init__.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/models.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/monitoring.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/processor.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/routers.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/serializers.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/task_registry.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/task_run_method.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/tasks.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/threads.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/types.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/urls.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/task_processor/utils.py +0 -0
- {flagsmith_common-3.7.0 → flagsmith_common-3.8.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.8.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
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import warnings
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Iterable, Iterator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DocgenEventsWarning(UserWarning):
|
|
9
|
+
"""Raised by the events scanner when a call site can't be resolved."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Emission methods exposed by `structlog.stdlib.BoundLogger` whose first
|
|
13
|
+
# positional argument is the event name. `log` is excluded because its
|
|
14
|
+
# first argument is the level, not the event; `bind`/`unbind`/`new` are
|
|
15
|
+
# not emissions.
|
|
16
|
+
EMIT_METHOD_NAMES = frozenset(
|
|
17
|
+
{
|
|
18
|
+
"debug",
|
|
19
|
+
"info",
|
|
20
|
+
"warning",
|
|
21
|
+
"warn",
|
|
22
|
+
"error",
|
|
23
|
+
"critical",
|
|
24
|
+
"fatal",
|
|
25
|
+
"exception",
|
|
26
|
+
"msg",
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class SourceLocation:
|
|
33
|
+
path: Path
|
|
34
|
+
line: int
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class EventEntry:
|
|
39
|
+
name: str
|
|
40
|
+
level: str
|
|
41
|
+
attributes: frozenset[str]
|
|
42
|
+
locations: list[SourceLocation] = field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class _LoggerScope:
|
|
47
|
+
domain: str
|
|
48
|
+
bound_attrs: frozenset[str]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_EXCLUDED_DIR_NAMES = frozenset({"migrations", "tests"})
|
|
52
|
+
_EXCLUDED_MANAGEMENT_DIR = ("management", "commands")
|
|
53
|
+
_TASK_PROCESSOR_APP_LABEL = "task_processor"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_event_entries_from_tree(
|
|
57
|
+
root: Path,
|
|
58
|
+
*,
|
|
59
|
+
app_label: str,
|
|
60
|
+
module_prefix: str,
|
|
61
|
+
) -> Iterator[EventEntry]:
|
|
62
|
+
"""Walk every `*.py` under `root` and yield its scanned event entries.
|
|
63
|
+
|
|
64
|
+
Skips `migrations/`, `tests/`, `conftest.py`, and `test_*.py`. Also skips
|
|
65
|
+
`management/commands/` unless `app_label == "task_processor"`, where the
|
|
66
|
+
runner loop's events are operationally important.
|
|
67
|
+
"""
|
|
68
|
+
for file_path in sorted(root.rglob("*.py")):
|
|
69
|
+
if _should_skip(file_path.relative_to(root), app_label=app_label):
|
|
70
|
+
continue
|
|
71
|
+
rel_parts = file_path.relative_to(root).with_suffix("").parts
|
|
72
|
+
module_dotted = ".".join((module_prefix, *rel_parts))
|
|
73
|
+
yield from get_event_entries_from_source(
|
|
74
|
+
file_path.read_text(),
|
|
75
|
+
module_dotted=module_dotted,
|
|
76
|
+
path=file_path,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _should_skip(relative: Path, *, app_label: str) -> bool:
|
|
81
|
+
parts = relative.parts
|
|
82
|
+
if any(part in _EXCLUDED_DIR_NAMES for part in parts[:-1]):
|
|
83
|
+
return True
|
|
84
|
+
filename = parts[-1]
|
|
85
|
+
if filename == "conftest.py" or filename.startswith("test_"):
|
|
86
|
+
return True
|
|
87
|
+
if (
|
|
88
|
+
len(parts) >= 3
|
|
89
|
+
and parts[0] == _EXCLUDED_MANAGEMENT_DIR[0]
|
|
90
|
+
and parts[1] == _EXCLUDED_MANAGEMENT_DIR[1]
|
|
91
|
+
and app_label != _TASK_PROCESSOR_APP_LABEL
|
|
92
|
+
):
|
|
93
|
+
return True
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def merge_event_entries(entries: Iterable[EventEntry]) -> list[EventEntry]:
|
|
98
|
+
"""Collapse entries sharing an event name: union attributes and locations.
|
|
99
|
+
|
|
100
|
+
Diverging log levels trigger a `DocgenEventsWarning`; the first-seen level
|
|
101
|
+
wins. Output is sorted alphabetically by event name.
|
|
102
|
+
"""
|
|
103
|
+
merged: dict[str, EventEntry] = {}
|
|
104
|
+
for entry in entries:
|
|
105
|
+
if existing := merged.get(entry.name):
|
|
106
|
+
if entry.level != existing.level:
|
|
107
|
+
original_location = existing.locations[0]
|
|
108
|
+
new_location = entry.locations[0]
|
|
109
|
+
warnings.warn(
|
|
110
|
+
f"`{entry.name}` is emitted at diverging log levels:"
|
|
111
|
+
f" `{existing.level}` at {original_location.path}:{original_location.line},"
|
|
112
|
+
f" `{entry.level}` at {new_location.path}:{new_location.line}."
|
|
113
|
+
f" Keeping first-seen level `{existing.level}`; reconcile"
|
|
114
|
+
" the emission sites to silence this warning.",
|
|
115
|
+
DocgenEventsWarning,
|
|
116
|
+
stacklevel=2,
|
|
117
|
+
)
|
|
118
|
+
existing.attributes = existing.attributes | entry.attributes
|
|
119
|
+
existing_locations = set(existing.locations)
|
|
120
|
+
for location in entry.locations:
|
|
121
|
+
if location not in existing_locations:
|
|
122
|
+
existing.locations.append(location)
|
|
123
|
+
existing_locations.add(location)
|
|
124
|
+
else:
|
|
125
|
+
merged[entry.name] = EventEntry(
|
|
126
|
+
name=entry.name,
|
|
127
|
+
level=entry.level,
|
|
128
|
+
attributes=entry.attributes,
|
|
129
|
+
locations=list(entry.locations),
|
|
130
|
+
)
|
|
131
|
+
return sorted(merged.values(), key=lambda e: e.name)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_event_entries_from_source(
|
|
135
|
+
source: str,
|
|
136
|
+
*,
|
|
137
|
+
module_dotted: str,
|
|
138
|
+
path: Path,
|
|
139
|
+
) -> Iterator[EventEntry]:
|
|
140
|
+
tree = ast.parse(source)
|
|
141
|
+
visitor = _ScopeVisitor(module_dotted=module_dotted, path=path)
|
|
142
|
+
visitor.visit(tree)
|
|
143
|
+
yield from visitor.entries
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class _ScopeVisitor(ast.NodeVisitor):
|
|
147
|
+
"""Walks the AST in source order with a stack of logger scopes.
|
|
148
|
+
|
|
149
|
+
Entering a function body pushes a copy of the enclosing scope so
|
|
150
|
+
binds that happen inside the function don't leak to sibling scopes.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(self, *, module_dotted: str, path: Path) -> None:
|
|
154
|
+
self.module_dotted = module_dotted
|
|
155
|
+
self.path = path
|
|
156
|
+
self._scope_stack: list[dict[str, _LoggerScope]] = [{}]
|
|
157
|
+
self._class_stack: list[dict[str, _LoggerScope]] = []
|
|
158
|
+
self._module_classes: dict[str, dict[str, _LoggerScope]] = {}
|
|
159
|
+
self.entries: list[EventEntry] = []
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def _scope(self) -> dict[str, _LoggerScope]:
|
|
163
|
+
return self._scope_stack[-1]
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def _class_scope(self) -> dict[str, _LoggerScope] | None:
|
|
167
|
+
return self._class_stack[-1] if self._class_stack else None
|
|
168
|
+
|
|
169
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
170
|
+
class_scope: dict[str, _LoggerScope] = {}
|
|
171
|
+
# Own methods take precedence — register them first.
|
|
172
|
+
for stmt in node.body:
|
|
173
|
+
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
174
|
+
if accessor := _resolve_method_accessor(stmt, outer_scopes=self._scope):
|
|
175
|
+
class_scope[stmt.name] = accessor
|
|
176
|
+
# Inherit from same-file parents declared earlier (Name-typed bases only).
|
|
177
|
+
for base in node.bases:
|
|
178
|
+
if isinstance(base, ast.Name) and base.id in self._module_classes:
|
|
179
|
+
for method_name, method_scope in self._module_classes[base.id].items():
|
|
180
|
+
class_scope.setdefault(method_name, method_scope)
|
|
181
|
+
self._module_classes[node.name] = class_scope
|
|
182
|
+
self._class_stack.append(class_scope)
|
|
183
|
+
self.generic_visit(node)
|
|
184
|
+
self._class_stack.pop()
|
|
185
|
+
|
|
186
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
187
|
+
self._scope_stack.append(dict(self._scope))
|
|
188
|
+
self.generic_visit(node)
|
|
189
|
+
self._scope_stack.pop()
|
|
190
|
+
|
|
191
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
192
|
+
self._scope_stack.append(dict(self._scope))
|
|
193
|
+
self.generic_visit(node)
|
|
194
|
+
self._scope_stack.pop()
|
|
195
|
+
|
|
196
|
+
def visit_Assign(self, node: ast.Assign) -> None:
|
|
197
|
+
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
|
198
|
+
target_id = node.targets[0].id
|
|
199
|
+
if scope := _resolve_seed(
|
|
200
|
+
node,
|
|
201
|
+
module_dotted=self.module_dotted,
|
|
202
|
+
path=self.path,
|
|
203
|
+
):
|
|
204
|
+
self._scope[target_id] = scope
|
|
205
|
+
elif scope := _resolve_bind(node, logger_scopes=self._scope):
|
|
206
|
+
self._scope[target_id] = scope
|
|
207
|
+
self.generic_visit(node)
|
|
208
|
+
|
|
209
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
210
|
+
if entry := _build_entry_from_emit_call(
|
|
211
|
+
node, self._scope, self.path, class_scope=self._class_scope
|
|
212
|
+
):
|
|
213
|
+
self.entries.append(entry)
|
|
214
|
+
self.generic_visit(node)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _resolve_seed(
|
|
218
|
+
node: ast.Assign,
|
|
219
|
+
*,
|
|
220
|
+
module_dotted: str,
|
|
221
|
+
path: Path,
|
|
222
|
+
) -> _LoggerScope | None:
|
|
223
|
+
call = node.value
|
|
224
|
+
if not isinstance(call, ast.Call):
|
|
225
|
+
return None
|
|
226
|
+
func = call.func
|
|
227
|
+
if not isinstance(func, ast.Attribute) or func.attr != "get_logger":
|
|
228
|
+
return None
|
|
229
|
+
if not isinstance(func.value, ast.Name) or func.value.id != "structlog":
|
|
230
|
+
return None
|
|
231
|
+
if not call.args:
|
|
232
|
+
return _LoggerScope(domain="", bound_attrs=frozenset())
|
|
233
|
+
target = node.targets[0]
|
|
234
|
+
assert isinstance(target, ast.Name)
|
|
235
|
+
domain = _resolve_domain(call.args[0], module_dotted=module_dotted)
|
|
236
|
+
if domain is None:
|
|
237
|
+
warnings.warn(
|
|
238
|
+
f"{path}:{node.lineno}: cannot statically resolve logger domain"
|
|
239
|
+
f" for `{target.id}`; skipping its events.",
|
|
240
|
+
DocgenEventsWarning,
|
|
241
|
+
stacklevel=2,
|
|
242
|
+
)
|
|
243
|
+
return None
|
|
244
|
+
return _LoggerScope(domain=domain, bound_attrs=frozenset())
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _resolve_bind(
|
|
248
|
+
node: ast.Assign,
|
|
249
|
+
*,
|
|
250
|
+
logger_scopes: dict[str, _LoggerScope],
|
|
251
|
+
) -> _LoggerScope | None:
|
|
252
|
+
call = node.value
|
|
253
|
+
if not isinstance(call, ast.Call):
|
|
254
|
+
return None
|
|
255
|
+
func = call.func
|
|
256
|
+
if not isinstance(func, ast.Attribute) or func.attr != "bind":
|
|
257
|
+
return None
|
|
258
|
+
if not isinstance(func.value, ast.Name) or func.value.id not in logger_scopes:
|
|
259
|
+
return None
|
|
260
|
+
parent = logger_scopes[func.value.id]
|
|
261
|
+
new_attrs = _kwargs_as_attributes(call.keywords)
|
|
262
|
+
return _LoggerScope(
|
|
263
|
+
domain=parent.domain,
|
|
264
|
+
bound_attrs=parent.bound_attrs | new_attrs,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _resolve_domain(node: ast.expr, *, module_dotted: str) -> str | None:
|
|
269
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
270
|
+
return node.value
|
|
271
|
+
if isinstance(node, ast.Name) and node.id == "__name__":
|
|
272
|
+
return module_dotted
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _kwargs_as_attributes(keywords: list[ast.keyword]) -> frozenset[str]:
|
|
277
|
+
return frozenset(kw.arg.replace("__", ".") for kw in keywords if kw.arg is not None)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _build_entry_from_emit_call(
|
|
281
|
+
node: ast.Call,
|
|
282
|
+
logger_scopes: dict[str, _LoggerScope],
|
|
283
|
+
path: Path,
|
|
284
|
+
*,
|
|
285
|
+
class_scope: dict[str, _LoggerScope] | None = None,
|
|
286
|
+
) -> EventEntry | None:
|
|
287
|
+
func = node.func
|
|
288
|
+
if not isinstance(func, ast.Attribute):
|
|
289
|
+
return None
|
|
290
|
+
if func.attr not in EMIT_METHOD_NAMES:
|
|
291
|
+
return None
|
|
292
|
+
scope = _scope_for_emit_target(func.value, logger_scopes, class_scope=class_scope)
|
|
293
|
+
if scope is None:
|
|
294
|
+
if accessor_name := _self_cls_accessor_name(func.value):
|
|
295
|
+
warnings.warn(
|
|
296
|
+
f"{path}:{node.lineno}: cannot resolve"
|
|
297
|
+
f" `{_describe_emit_target(func.value)}.{func.attr}(...)`:"
|
|
298
|
+
f" `{accessor_name}` isn't a tracked accessor on this class"
|
|
299
|
+
" or any same-file parent. Consider inlining the bind at the"
|
|
300
|
+
" call site or moving the accessor into this file.",
|
|
301
|
+
DocgenEventsWarning,
|
|
302
|
+
stacklevel=2,
|
|
303
|
+
)
|
|
304
|
+
return None
|
|
305
|
+
if not node.args:
|
|
306
|
+
return None
|
|
307
|
+
event_arg = node.args[0]
|
|
308
|
+
if not (isinstance(event_arg, ast.Constant) and isinstance(event_arg.value, str)):
|
|
309
|
+
warnings.warn(
|
|
310
|
+
f"{path}:{node.lineno}: cannot statically resolve event name"
|
|
311
|
+
f" for `{_describe_emit_target(func.value)}.{func.attr}(...)`;"
|
|
312
|
+
" skipping. Consider annotating the call site with a"
|
|
313
|
+
" `# docgen: event=<name>` comment so the catalogue can still"
|
|
314
|
+
" pick it up.",
|
|
315
|
+
DocgenEventsWarning,
|
|
316
|
+
stacklevel=2,
|
|
317
|
+
)
|
|
318
|
+
return None
|
|
319
|
+
attributes = scope.bound_attrs | _kwargs_as_attributes(node.keywords)
|
|
320
|
+
name = f"{scope.domain}.{event_arg.value}" if scope.domain else event_arg.value
|
|
321
|
+
return EventEntry(
|
|
322
|
+
name=name,
|
|
323
|
+
level=func.attr,
|
|
324
|
+
attributes=attributes,
|
|
325
|
+
locations=[SourceLocation(path=path, line=node.lineno)],
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _scope_for_emit_target(
|
|
330
|
+
target: ast.expr,
|
|
331
|
+
logger_scopes: dict[str, _LoggerScope],
|
|
332
|
+
*,
|
|
333
|
+
class_scope: dict[str, _LoggerScope] | None = None,
|
|
334
|
+
) -> _LoggerScope | None:
|
|
335
|
+
if isinstance(target, ast.Name):
|
|
336
|
+
return logger_scopes.get(target.id)
|
|
337
|
+
if isinstance(target, ast.Attribute):
|
|
338
|
+
# `self.<name>` — method/property accessor on the enclosing class.
|
|
339
|
+
if (
|
|
340
|
+
class_scope is not None
|
|
341
|
+
and isinstance(target.value, ast.Name)
|
|
342
|
+
and target.value.id in _SELF_OR_CLS
|
|
343
|
+
and target.attr in class_scope
|
|
344
|
+
):
|
|
345
|
+
return class_scope[target.attr]
|
|
346
|
+
return None
|
|
347
|
+
if isinstance(target, ast.Call):
|
|
348
|
+
func = target.func
|
|
349
|
+
if not isinstance(func, ast.Attribute):
|
|
350
|
+
return None
|
|
351
|
+
# `self.<name>(...)` — method accessor invocation.
|
|
352
|
+
if (
|
|
353
|
+
class_scope is not None
|
|
354
|
+
and isinstance(func.value, ast.Name)
|
|
355
|
+
and func.value.id in _SELF_OR_CLS
|
|
356
|
+
and func.attr in class_scope
|
|
357
|
+
):
|
|
358
|
+
return class_scope[func.attr]
|
|
359
|
+
if func.attr != "bind":
|
|
360
|
+
return None
|
|
361
|
+
parent = _scope_for_emit_target(
|
|
362
|
+
func.value, logger_scopes, class_scope=class_scope
|
|
363
|
+
)
|
|
364
|
+
if parent is None:
|
|
365
|
+
return None
|
|
366
|
+
return _LoggerScope(
|
|
367
|
+
domain=parent.domain,
|
|
368
|
+
bound_attrs=parent.bound_attrs | _kwargs_as_attributes(target.keywords),
|
|
369
|
+
)
|
|
370
|
+
return None
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
_SELF_OR_CLS = frozenset({"self", "cls"})
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _self_cls_accessor_name(target: ast.expr) -> str | None:
|
|
377
|
+
"""Name of the accessor in a `self.<X>` / `cls.<X>(...)` emit shape, else None."""
|
|
378
|
+
if isinstance(target, ast.Attribute):
|
|
379
|
+
if isinstance(target.value, ast.Name) and target.value.id in _SELF_OR_CLS:
|
|
380
|
+
return target.attr
|
|
381
|
+
if isinstance(target, ast.Call):
|
|
382
|
+
func = target.func
|
|
383
|
+
if isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name):
|
|
384
|
+
if func.value.id in _SELF_OR_CLS:
|
|
385
|
+
return func.attr
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _resolve_method_accessor(
|
|
390
|
+
func_def: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
391
|
+
*,
|
|
392
|
+
outer_scopes: dict[str, _LoggerScope],
|
|
393
|
+
) -> _LoggerScope | None:
|
|
394
|
+
"""Return a scope for a method that just returns a bound logger."""
|
|
395
|
+
body = list(func_def.body)
|
|
396
|
+
# Allow a leading docstring.
|
|
397
|
+
if (
|
|
398
|
+
body
|
|
399
|
+
and isinstance(body[0], ast.Expr)
|
|
400
|
+
and isinstance(body[0].value, ast.Constant)
|
|
401
|
+
and isinstance(body[0].value.value, str)
|
|
402
|
+
):
|
|
403
|
+
body = body[1:]
|
|
404
|
+
if len(body) != 1:
|
|
405
|
+
return None
|
|
406
|
+
stmt = body[0]
|
|
407
|
+
if not isinstance(stmt, ast.Return) or stmt.value is None:
|
|
408
|
+
return None
|
|
409
|
+
return _scope_for_emit_target(stmt.value, outer_scopes)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _describe_emit_target(target: ast.expr) -> str:
|
|
413
|
+
if isinstance(target, ast.Name):
|
|
414
|
+
return target.id
|
|
415
|
+
if isinstance(target, ast.Attribute):
|
|
416
|
+
return f"{_describe_emit_target(target.value)}.{target.attr}"
|
|
417
|
+
assert isinstance(target, ast.Call)
|
|
418
|
+
func = target.func
|
|
419
|
+
assert isinstance(func, ast.Attribute)
|
|
420
|
+
return f"{_describe_emit_target(func.value)}.{func.attr}(...)"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from operator import itemgetter
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
import prometheus_client
|
|
7
|
+
from django.apps import apps
|
|
8
|
+
from django.core.management import BaseCommand, CommandParser
|
|
9
|
+
from django.template.loader import get_template
|
|
10
|
+
from django.utils.module_loading import autodiscover_modules
|
|
11
|
+
from prometheus_client.metrics import MetricWrapperBase
|
|
12
|
+
|
|
13
|
+
from common.core.docgen.events import (
|
|
14
|
+
EventEntry,
|
|
15
|
+
get_event_entries_from_tree,
|
|
16
|
+
merge_event_entries,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Command(BaseCommand):
|
|
21
|
+
help = "Generate documentation for the Flagsmith codebase."
|
|
22
|
+
|
|
23
|
+
def add_arguments(self, parser: CommandParser) -> None:
|
|
24
|
+
subparsers = parser.add_subparsers(
|
|
25
|
+
title="sub-commands",
|
|
26
|
+
required=True,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
metric_parser = subparsers.add_parser(
|
|
30
|
+
"metrics",
|
|
31
|
+
help="Generate metrics documentation.",
|
|
32
|
+
)
|
|
33
|
+
metric_parser.set_defaults(handle_method=self.handle_metrics)
|
|
34
|
+
|
|
35
|
+
events_parser = subparsers.add_parser(
|
|
36
|
+
"events",
|
|
37
|
+
help="Generate structlog events documentation.",
|
|
38
|
+
)
|
|
39
|
+
events_parser.set_defaults(handle_method=self.handle_events)
|
|
40
|
+
|
|
41
|
+
def initialise(self) -> None:
|
|
42
|
+
from common.gunicorn import metrics # noqa: F401
|
|
43
|
+
|
|
44
|
+
autodiscover_modules(
|
|
45
|
+
"metrics",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def handle(
|
|
49
|
+
self,
|
|
50
|
+
*args: Any,
|
|
51
|
+
handle_method: Callable[..., None],
|
|
52
|
+
**options: Any,
|
|
53
|
+
) -> None:
|
|
54
|
+
self.initialise()
|
|
55
|
+
handle_method(*args, **options)
|
|
56
|
+
|
|
57
|
+
def handle_metrics(self, *args: Any, **options: Any) -> None:
|
|
58
|
+
template = get_template("docgen-metrics.md")
|
|
59
|
+
|
|
60
|
+
flagsmith_metrics = sorted(
|
|
61
|
+
(
|
|
62
|
+
{
|
|
63
|
+
"name": collector._name,
|
|
64
|
+
"documentation": collector._documentation,
|
|
65
|
+
"labels": collector._labelnames,
|
|
66
|
+
"type": collector._type,
|
|
67
|
+
}
|
|
68
|
+
for collector in prometheus_client.REGISTRY._collector_to_names
|
|
69
|
+
if isinstance(collector, MetricWrapperBase)
|
|
70
|
+
),
|
|
71
|
+
key=itemgetter("name"),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
self.stdout.write(
|
|
75
|
+
template.render(
|
|
76
|
+
context={"flagsmith_metrics": flagsmith_metrics},
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def handle_events(self, *args: Any, **options: Any) -> None:
|
|
81
|
+
template = get_template("docgen-events.md")
|
|
82
|
+
|
|
83
|
+
repo_root = _get_repo_root()
|
|
84
|
+
entries: list[EventEntry] = []
|
|
85
|
+
for app_config in apps.get_app_configs():
|
|
86
|
+
entries.extend(
|
|
87
|
+
get_event_entries_from_tree(
|
|
88
|
+
Path(app_config.path),
|
|
89
|
+
app_label=app_config.label,
|
|
90
|
+
module_prefix=app_config.name,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
merged = merge_event_entries(entries)
|
|
94
|
+
|
|
95
|
+
flagsmith_events = [
|
|
96
|
+
{
|
|
97
|
+
"name": entry.name,
|
|
98
|
+
"level": entry.level,
|
|
99
|
+
"locations": [
|
|
100
|
+
{
|
|
101
|
+
"path": _relative_if_under(location.path, repo_root),
|
|
102
|
+
"line": location.line,
|
|
103
|
+
}
|
|
104
|
+
for location in entry.locations
|
|
105
|
+
],
|
|
106
|
+
"attributes": sorted(entry.attributes),
|
|
107
|
+
}
|
|
108
|
+
for entry in merged
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
self.stdout.write(
|
|
112
|
+
template.render(
|
|
113
|
+
context={"flagsmith_events": flagsmith_events},
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _get_repo_root() -> Path:
|
|
119
|
+
"""Resolve the git repo root for emitted source paths.
|
|
120
|
+
|
|
121
|
+
Falls back to the current working directory when git isn't available or
|
|
122
|
+
the CWD isn't inside a repo.
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
result = subprocess.run(
|
|
126
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
127
|
+
capture_output=True,
|
|
128
|
+
text=True,
|
|
129
|
+
check=True,
|
|
130
|
+
)
|
|
131
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
132
|
+
return Path.cwd()
|
|
133
|
+
return Path(result.stdout.strip())
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _relative_if_under(path: Path, base: Path) -> Path:
|
|
137
|
+
try:
|
|
138
|
+
return path.relative_to(base)
|
|
139
|
+
except ValueError:
|
|
140
|
+
return path
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Events
|
|
3
|
+
sidebar_label: Events
|
|
4
|
+
sidebar_position: 30
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Flagsmith backend emits [OpenTelemetry events](https://opentelemetry.io/docs/specs/otel/logs/data-model/#events)
|
|
8
|
+
that can be ingested to downstream observability systems and/or a data warehouse of your choice via OTLP.
|
|
9
|
+
To learn how to configure this, see [OpenTelemetry](deployment-self-hosting/scaling-and-performance/opentelemetry).
|
|
10
|
+
|
|
11
|
+
## Event catalogue
|
|
12
|
+
{% for event in flagsmith_events %}
|
|
13
|
+
### `{{ event.name }}`
|
|
14
|
+
|
|
15
|
+
Logged at `{{ event.level }}` from:
|
|
16
|
+
{% for location in event.locations %} - `{{ location.path }}:{{ location.line }}`
|
|
17
|
+
{% endfor %}
|
|
18
|
+
Attributes:
|
|
19
|
+
{% for attr in event.attributes %} - `{{ attr }}`
|
|
20
|
+
{% endfor %}{% endfor %}
|
|
File without changes
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
from operator import itemgetter
|
|
2
|
-
from typing import Any, Callable
|
|
3
|
-
|
|
4
|
-
import prometheus_client
|
|
5
|
-
from django.core.management import BaseCommand, CommandParser
|
|
6
|
-
from django.template.loader import get_template
|
|
7
|
-
from django.utils.module_loading import autodiscover_modules
|
|
8
|
-
from prometheus_client.metrics import MetricWrapperBase
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Command(BaseCommand):
|
|
12
|
-
help = "Generate documentation for the Flagsmith codebase."
|
|
13
|
-
|
|
14
|
-
def add_arguments(self, parser: CommandParser) -> None:
|
|
15
|
-
subparsers = parser.add_subparsers(
|
|
16
|
-
title="sub-commands",
|
|
17
|
-
required=True,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
metric_parser = subparsers.add_parser(
|
|
21
|
-
"metrics",
|
|
22
|
-
help="Generate metrics documentation.",
|
|
23
|
-
)
|
|
24
|
-
metric_parser.set_defaults(handle_method=self.handle_metrics)
|
|
25
|
-
|
|
26
|
-
def initialise(self) -> None:
|
|
27
|
-
from common.gunicorn import metrics # noqa: F401
|
|
28
|
-
|
|
29
|
-
autodiscover_modules(
|
|
30
|
-
"metrics",
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
def handle(
|
|
34
|
-
self,
|
|
35
|
-
*args: Any,
|
|
36
|
-
handle_method: Callable[..., None],
|
|
37
|
-
**options: Any,
|
|
38
|
-
) -> None:
|
|
39
|
-
self.initialise()
|
|
40
|
-
handle_method(*args, **options)
|
|
41
|
-
|
|
42
|
-
def handle_metrics(self, *args: Any, **options: Any) -> None:
|
|
43
|
-
template = get_template("docgen-metrics.md")
|
|
44
|
-
|
|
45
|
-
flagsmith_metrics = sorted(
|
|
46
|
-
(
|
|
47
|
-
{
|
|
48
|
-
"name": collector._name,
|
|
49
|
-
"documentation": collector._documentation,
|
|
50
|
-
"labels": collector._labelnames,
|
|
51
|
-
"type": collector._type,
|
|
52
|
-
}
|
|
53
|
-
for collector in prometheus_client.REGISTRY._collector_to_names
|
|
54
|
-
if isinstance(collector, MetricWrapperBase)
|
|
55
|
-
),
|
|
56
|
-
key=itemgetter("name"),
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
self.stdout.write(
|
|
60
|
-
template.render(
|
|
61
|
-
context={"flagsmith_metrics": flagsmith_metrics},
|
|
62
|
-
)
|
|
63
|
-
)
|
|
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.7.0 → flagsmith_common-3.8.0}/src/common/core/management/commands/start.py
RENAMED
|
File without changes
|
{flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/core/management/commands/waitfordb.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.7.0 → flagsmith_common-3.8.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
|
|
File without changes
|
{flagsmith_common-3.7.0 → flagsmith_common-3.8.0}/src/common/features/multivariate/serializers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.7.0 → flagsmith_common-3.8.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
|
|
File without changes
|
|
File without changes
|
{flagsmith_common-3.7.0 → flagsmith_common-3.8.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
|
|
File without changes
|
{flagsmith_common-3.7.0 → flagsmith_common-3.8.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
|
|
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
|