flagsmith-common 2.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. common/__init__.py +0 -0
  2. common/core/__init__.py +6 -0
  3. common/core/app.py +6 -0
  4. common/core/cli/__init__.py +0 -0
  5. common/core/cli/healthcheck.py +120 -0
  6. common/core/logging.py +24 -0
  7. common/core/main.py +105 -0
  8. common/core/management/__init__.py +0 -0
  9. common/core/management/commands/__init__.py +0 -0
  10. common/core/management/commands/docgen.py +63 -0
  11. common/core/management/commands/start.py +61 -0
  12. common/core/management/commands/waitfordb.py +87 -0
  13. common/core/metrics.py +25 -0
  14. common/core/middleware.py +22 -0
  15. common/core/templates/docgen-metrics.md +22 -0
  16. common/core/urls.py +17 -0
  17. common/core/utils.py +239 -0
  18. common/core/views.py +27 -0
  19. common/environments/permissions.py +15 -0
  20. common/features/__init__.py +0 -0
  21. common/features/multivariate/__init__.py +0 -0
  22. common/features/multivariate/serializers.py +19 -0
  23. common/features/serializers.py +68 -0
  24. common/features/versioning/__init__.py +0 -0
  25. common/features/versioning/serializers.py +13 -0
  26. common/gunicorn/__init__.py +0 -0
  27. common/gunicorn/conf.py +18 -0
  28. common/gunicorn/constants.py +23 -0
  29. common/gunicorn/logging.py +120 -0
  30. common/gunicorn/metrics.py +26 -0
  31. common/gunicorn/middleware.py +30 -0
  32. common/gunicorn/utils.py +104 -0
  33. common/migrations/__init__.py +0 -0
  34. common/migrations/helpers/__init__.py +9 -0
  35. common/migrations/helpers/postgres_helpers.py +41 -0
  36. common/organisations/permissions.py +10 -0
  37. common/projects/permissions.py +40 -0
  38. common/prometheus/__init__.py +3 -0
  39. common/prometheus/utils.py +38 -0
  40. common/py.typed +0 -0
  41. common/test_tools/__init__.py +11 -0
  42. common/test_tools/plugin.py +139 -0
  43. common/test_tools/types.py +56 -0
  44. common/test_tools/utils.py +11 -0
  45. common/types.py +45 -0
  46. flagsmith_common-2.2.4.dist-info/METADATA +196 -0
  47. flagsmith_common-2.2.4.dist-info/RECORD +92 -0
  48. flagsmith_common-2.2.4.dist-info/WHEEL +4 -0
  49. flagsmith_common-2.2.4.dist-info/entry_points.txt +6 -0
  50. flagsmith_common-2.2.4.dist-info/licenses/LICENSE +28 -0
  51. task_processor/__init__.py +0 -0
  52. task_processor/admin.py +38 -0
  53. task_processor/apps.py +47 -0
  54. task_processor/decorators.py +209 -0
  55. task_processor/exceptions.py +28 -0
  56. task_processor/health.py +44 -0
  57. task_processor/managers.py +18 -0
  58. task_processor/metrics.py +22 -0
  59. task_processor/migrations/0001_initial.py +44 -0
  60. task_processor/migrations/0002_healthcheckmodel.py +21 -0
  61. task_processor/migrations/0003_add_completed_to_task.py +22 -0
  62. task_processor/migrations/0004_recreate_task_indexes.py +43 -0
  63. task_processor/migrations/0005_update_conditional_index_conditions.py +45 -0
  64. task_processor/migrations/0006_auto_20230221_0802.py +45 -0
  65. task_processor/migrations/0007_add_is_locked.py +23 -0
  66. task_processor/migrations/0008_add_get_task_to_process_function.py +31 -0
  67. task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +18 -0
  68. task_processor/migrations/0010_task_priority.py +27 -0
  69. task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +27 -0
  70. task_processor/migrations/0012_add_locked_at_and_timeout.py +40 -0
  71. task_processor/migrations/0013_add_last_picked_at.py +34 -0
  72. task_processor/migrations/__init__.py +0 -0
  73. task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +30 -0
  74. task_processor/migrations/sql/0008_get_tasks_to_process.sql +30 -0
  75. task_processor/migrations/sql/0011_get_tasks_to_process.sql +30 -0
  76. task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +33 -0
  77. task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +33 -0
  78. task_processor/migrations/sql/__init__.py +0 -0
  79. task_processor/models.py +237 -0
  80. task_processor/monitoring.py +12 -0
  81. task_processor/processor.py +202 -0
  82. task_processor/py.typed +0 -0
  83. task_processor/routers.py +55 -0
  84. task_processor/serializers.py +7 -0
  85. task_processor/task_registry.py +90 -0
  86. task_processor/task_run_method.py +7 -0
  87. task_processor/tasks.py +71 -0
  88. task_processor/threads.py +128 -0
  89. task_processor/types.py +18 -0
  90. task_processor/urls.py +5 -0
  91. task_processor/utils.py +71 -0
  92. task_processor/views.py +20 -0
@@ -0,0 +1,104 @@
1
+ import argparse
2
+ import os
3
+ from functools import lru_cache
4
+ from typing import Any
5
+
6
+ from django.core.handlers.wsgi import WSGIHandler
7
+ from django.core.wsgi import get_wsgi_application
8
+ from django.http import HttpRequest
9
+ from drf_yasg.generators import EndpointEnumerator # type: ignore[import-untyped]
10
+ from environs import Env
11
+ from gunicorn.app.wsgiapp import ( # type: ignore[import-untyped]
12
+ WSGIApplication as GunicornWSGIApplication,
13
+ )
14
+ from gunicorn.config import Config # type: ignore[import-untyped]
15
+
16
+ from common.gunicorn.constants import WSGI_EXTRA_PREFIX
17
+
18
+ env = Env()
19
+
20
+ DEFAULT_ACCESS_LOG_FORMAT = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %({origin}i)s %({access-control-allow-origin}o)s'
21
+ GUNICORN_FLAGSMITH_DEFAULTS = {
22
+ "access_log_format": env.str("ACCESS_LOG_FORMAT", DEFAULT_ACCESS_LOG_FORMAT),
23
+ "accesslog": env.str("ACCESS_LOG_LOCATION", os.devnull),
24
+ "bind": "0.0.0.0:8000",
25
+ "config": "python:common.gunicorn.conf",
26
+ "logger_class": "common.gunicorn.logging.GunicornJsonCapableLogger",
27
+ "statsd_prefix": "flagsmith.api",
28
+ "threads": env.int("GUNICORN_THREADS", 1),
29
+ "timeout": env.int("GUNICORN_TIMEOUT", 30),
30
+ "worker_class": "sync",
31
+ "workers": env.int("GUNICORN_WORKERS", 1),
32
+ }
33
+
34
+
35
+ class DjangoWSGIApplication(GunicornWSGIApplication): # type: ignore[misc]
36
+ def __init__(self, options: dict[str, Any] | None) -> None:
37
+ self.options = {
38
+ key: value for key, value in (options or {}).items() if value is not None
39
+ }
40
+ super().__init__()
41
+
42
+ def load_config(self) -> None:
43
+ cfg_settings = self.cfg.settings
44
+ options_items = (
45
+ (key, value)
46
+ for key, value in {**GUNICORN_FLAGSMITH_DEFAULTS, **self.options}.items()
47
+ if key in cfg_settings
48
+ )
49
+ for key, value in options_items:
50
+ self.cfg.set(key.lower(), value)
51
+ self.load_config_from_module_name_or_filename(self.cfg.config)
52
+
53
+ def load_wsgiapp(self) -> WSGIHandler:
54
+ return get_wsgi_application()
55
+
56
+
57
+ def add_arguments(parser: argparse.ArgumentParser) -> None:
58
+ gunicorn_group = parser.add_argument_group("gunicorn")
59
+ _config = Config()
60
+ keys = sorted(_config.settings, key=_config.settings.__getitem__)
61
+ for key in keys:
62
+ _config.settings[key].add_option(gunicorn_group)
63
+
64
+
65
+ def run_server(options: dict[str, Any] | None = None) -> None:
66
+ DjangoWSGIApplication(options).run()
67
+
68
+
69
+ @lru_cache
70
+ def get_route_template(route: str) -> str:
71
+ """
72
+ Convert a Django regex route to a template string that can be
73
+ searched for in the API documentation.
74
+
75
+ e.g.,
76
+
77
+ `"^api/v1/environments/(?P<environment_api_key>[^/.]+)/api-keys/$"` ->
78
+ `"/api/v1/environments/{environment_api_key}/api-keys/"`
79
+ """
80
+ route_template: str = EndpointEnumerator().get_path_from_regex(route)
81
+ return route_template
82
+
83
+
84
+ def log_extra(
85
+ request: HttpRequest,
86
+ key: str,
87
+ value: Any,
88
+ ) -> None:
89
+ """
90
+ Store a value in the WSGI request `environ` using a prefixed key.
91
+
92
+ https://peps.python.org/pep-3333/#specification-details
93
+ "...the application is allowed to modify the dictionary in any way it desires"
94
+ """
95
+ meta_key = f"{WSGI_EXTRA_PREFIX}{key}"
96
+ request.META[meta_key] = value
97
+
98
+
99
+ def get_extra(environ: dict[str, Any], key: str) -> Any:
100
+ """
101
+ Retrieve a value from the WSGI request `environ` using a prefixed key.
102
+ """
103
+ meta_key = f"{WSGI_EXTRA_PREFIX}{key}"
104
+ return environ.get(meta_key)
File without changes
@@ -0,0 +1,9 @@
1
+ """
2
+ Note: django doesn't support adding submodules to the migrations module directory
3
+ that don't include a Migration class. As such, I've defined this helpers submodule
4
+ and simplified the imports by defining the __all__ attribute.
5
+ """
6
+
7
+ from common.migrations.helpers.postgres_helpers import PostgresOnlyRunSQL
8
+
9
+ __all__ = ["PostgresOnlyRunSQL"]
@@ -0,0 +1,41 @@
1
+ from contextlib import suppress
2
+
3
+ from django.db import migrations
4
+ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
5
+ from django.db.migrations.state import ProjectState
6
+
7
+
8
+ class PostgresOnlyRunSQL(migrations.RunSQL):
9
+ @classmethod
10
+ def from_sql_file(
11
+ cls,
12
+ file_path: str,
13
+ reverse_sql: str = "",
14
+ ) -> "PostgresOnlyRunSQL":
15
+ with open(file_path) as forward_sql:
16
+ with suppress(FileNotFoundError):
17
+ with open(reverse_sql) as reverse_sql_file:
18
+ reverse_sql = reverse_sql_file.read()
19
+ return cls(forward_sql.read(), reverse_sql=reverse_sql)
20
+
21
+ def database_forwards(
22
+ self,
23
+ app_label: str,
24
+ schema_editor: BaseDatabaseSchemaEditor,
25
+ from_state: ProjectState,
26
+ to_state: ProjectState,
27
+ ) -> None:
28
+ if schema_editor.connection.vendor != "postgresql":
29
+ return
30
+ super().database_forwards(app_label, schema_editor, from_state, to_state)
31
+
32
+ def database_backwards(
33
+ self,
34
+ app_label: str,
35
+ schema_editor: BaseDatabaseSchemaEditor,
36
+ from_state: ProjectState,
37
+ to_state: ProjectState,
38
+ ) -> None:
39
+ if schema_editor.connection.vendor != "postgresql":
40
+ return
41
+ super().database_backwards(app_label, schema_editor, from_state, to_state)
@@ -0,0 +1,10 @@
1
+ CREATE_PROJECT = "CREATE_PROJECT"
2
+ MANAGE_USER_GROUPS = "MANAGE_USER_GROUPS"
3
+
4
+ ORGANISATION_PERMISSIONS = (
5
+ (CREATE_PROJECT, "Allows the user to create projects in this organisation."),
6
+ (
7
+ MANAGE_USER_GROUPS,
8
+ "Allows the user to manage the groups in the organisation and their members.",
9
+ ),
10
+ )
@@ -0,0 +1,40 @@
1
+ VIEW_AUDIT_LOG = "VIEW_AUDIT_LOG"
2
+
3
+ # Maintain a list of permissions here
4
+ VIEW_PROJECT = "VIEW_PROJECT"
5
+ CREATE_ENVIRONMENT = "CREATE_ENVIRONMENT"
6
+ DELETE_FEATURE = "DELETE_FEATURE"
7
+ CREATE_FEATURE = "CREATE_FEATURE"
8
+ EDIT_FEATURE = "EDIT_FEATURE"
9
+ MANAGE_SEGMENTS = "MANAGE_SEGMENTS"
10
+ MANAGE_TAGS = "MANAGE_TAGS"
11
+
12
+ # Note that this does not impact change requests in an environment
13
+ MANAGE_PROJECT_LEVEL_CHANGE_REQUESTS = "MANAGE_PROJECT_LEVEL_CHANGE_REQUESTS"
14
+ APPROVE_PROJECT_LEVEL_CHANGE_REQUESTS = "APPROVE_PROJECT_LEVEL_CHANGE_REQUESTS"
15
+ CREATE_PROJECT_LEVEL_CHANGE_REQUESTS = "CREATE_PROJECT_LEVEL_CHANGE_REQUESTS"
16
+
17
+ TAG_SUPPORTED_PERMISSIONS = [DELETE_FEATURE]
18
+
19
+ PROJECT_PERMISSIONS = [
20
+ (VIEW_PROJECT, "View permission for the given project."),
21
+ (CREATE_ENVIRONMENT, "Ability to create an environment in the given project."),
22
+ (DELETE_FEATURE, "Ability to delete features in the given project."),
23
+ (CREATE_FEATURE, "Ability to create features in the given project."),
24
+ (EDIT_FEATURE, "Ability to edit features in the given project."),
25
+ (MANAGE_SEGMENTS, "Ability to manage segments in the given project."),
26
+ (VIEW_AUDIT_LOG, "Allows the user to view the audit logs for this organisation."),
27
+ (
28
+ MANAGE_PROJECT_LEVEL_CHANGE_REQUESTS,
29
+ "Ability to create, delete, and publish change requests associated with a project.",
30
+ ),
31
+ (
32
+ APPROVE_PROJECT_LEVEL_CHANGE_REQUESTS,
33
+ "Ability to approve project level change requests.",
34
+ ),
35
+ (
36
+ CREATE_PROJECT_LEVEL_CHANGE_REQUESTS,
37
+ "Ability to create project level change requests.",
38
+ ),
39
+ (MANAGE_TAGS, "Allows the user to manage tags in the given project."),
40
+ ]
@@ -0,0 +1,3 @@
1
+ from common.prometheus.utils import Histogram
2
+
3
+ __all__ = ("Histogram",)
@@ -0,0 +1,38 @@
1
+ import importlib
2
+
3
+ import prometheus_client
4
+ from django.conf import settings
5
+ from prometheus_client.metrics import MetricWrapperBase
6
+ from prometheus_client.multiprocess import MultiProcessCollector
7
+
8
+
9
+ class Histogram(prometheus_client.Histogram):
10
+ DEFAULT_BUCKETS = settings.PROMETHEUS_HISTOGRAM_BUCKETS
11
+
12
+
13
+ def get_registry() -> prometheus_client.CollectorRegistry:
14
+ registry = prometheus_client.CollectorRegistry()
15
+ MultiProcessCollector(registry) # type: ignore[no-untyped-call]
16
+ return registry
17
+
18
+
19
+ def reload_metrics(*metric_module_names: str) -> None:
20
+ """
21
+ Clear the registry of all collectors from the given modules
22
+ and reload the modules to register the collectors again.
23
+
24
+ Used in tests to reset the state of the metrics module
25
+ when needed.
26
+ """
27
+
28
+ registry = prometheus_client.REGISTRY
29
+
30
+ for module_name in metric_module_names:
31
+ metrics_module = importlib.import_module(module_name)
32
+
33
+ for module_attr in vars(metrics_module).values():
34
+ if isinstance(module_attr, MetricWrapperBase):
35
+ # Unregister the collector from the registry
36
+ registry.unregister(module_attr)
37
+
38
+ importlib.reload(metrics_module)
common/py.typed ADDED
File without changes
@@ -0,0 +1,11 @@
1
+ from common.test_tools.types import (
2
+ AssertMetricFixture,
3
+ RunTasksFixture,
4
+ SnapshotFixture,
5
+ )
6
+
7
+ __all__ = (
8
+ "AssertMetricFixture",
9
+ "RunTasksFixture",
10
+ "SnapshotFixture",
11
+ )
@@ -0,0 +1,139 @@
1
+ from functools import partial
2
+ from typing import Generator
3
+
4
+ import prometheus_client
5
+ import pytest
6
+ from prometheus_client.metrics import MetricWrapperBase
7
+ from pyfakefs.fake_filesystem import FakeFilesystem
8
+ from pytest_django.fixtures import SettingsWrapper
9
+
10
+ from common.test_tools.types import (
11
+ AssertMetricFixture,
12
+ RunTasksFixture,
13
+ Snapshot,
14
+ SnapshotFixture,
15
+ )
16
+ from task_processor.task_run_method import TaskRunMethod
17
+
18
+
19
+ def pytest_addoption(parser: pytest.Parser) -> None:
20
+ group = parser.getgroup("snapshot")
21
+ group.addoption(
22
+ "--snapshot-update",
23
+ action="store_true",
24
+ help="Update snapshot files instead of testing against them.",
25
+ )
26
+
27
+
28
+ def assert_metric_impl() -> Generator[AssertMetricFixture, None, None]:
29
+ registry = prometheus_client.REGISTRY
30
+ collectors = [*registry._collector_to_names]
31
+
32
+ # Reset registry state
33
+ for collector in collectors:
34
+ if isinstance(collector, MetricWrapperBase):
35
+ collector.clear()
36
+
37
+ def _assert_metric(
38
+ *,
39
+ name: str,
40
+ labels: dict[str, str],
41
+ value: float | int,
42
+ ) -> None:
43
+ metric_value = registry.get_sample_value(name, labels)
44
+ assert metric_value == value, (
45
+ f"Metric {name} not found in registry:\n"
46
+ f"{prometheus_client.generate_latest(registry).decode()}"
47
+ )
48
+
49
+ yield _assert_metric
50
+
51
+
52
+ assert_metric = pytest.fixture(assert_metric_impl)
53
+
54
+
55
+ @pytest.fixture()
56
+ def saas_mode(fs: FakeFilesystem) -> Generator[None, None, None]:
57
+ from common.core.utils import is_saas
58
+
59
+ is_saas.cache_clear()
60
+ fs.create_file("./SAAS_DEPLOYMENT")
61
+
62
+ yield
63
+
64
+ is_saas.cache_clear()
65
+
66
+
67
+ @pytest.fixture()
68
+ def enterprise_mode(fs: FakeFilesystem) -> Generator[None, None, None]:
69
+ from common.core.utils import is_enterprise
70
+
71
+ is_enterprise.cache_clear()
72
+ fs.create_file("./ENTERPRISE_VERSION")
73
+
74
+ yield
75
+
76
+ is_enterprise.cache_clear()
77
+
78
+
79
+ @pytest.fixture()
80
+ def task_processor_mode(settings: SettingsWrapper) -> None:
81
+ settings.TASK_PROCESSOR_MODE = True
82
+ # The setting is supposed to be set before the metrics module is imported,
83
+ # so reload it
84
+ from common.prometheus.utils import reload_metrics
85
+
86
+ reload_metrics("task_processor.metrics")
87
+
88
+
89
+ @pytest.fixture(autouse=True)
90
+ def flagsmith_markers_marked(
91
+ request: pytest.FixtureRequest,
92
+ ) -> None:
93
+ for marker in request.node.iter_markers():
94
+ if marker.name == "saas_mode":
95
+ request.getfixturevalue("saas_mode")
96
+ if marker.name == "enterprise_mode":
97
+ request.getfixturevalue("enterprise_mode")
98
+ if marker.name == "task_processor_mode":
99
+ request.getfixturevalue("task_processor_mode")
100
+
101
+
102
+ @pytest.fixture(name="run_tasks")
103
+ def run_tasks_impl(
104
+ settings: SettingsWrapper,
105
+ transactional_db: None,
106
+ task_processor_mode: None,
107
+ ) -> RunTasksFixture:
108
+ settings.TASK_RUN_METHOD = TaskRunMethod.TASK_PROCESSOR
109
+
110
+ from task_processor.processor import run_tasks
111
+
112
+ return partial(run_tasks, database="default")
113
+
114
+
115
+ @pytest.fixture
116
+ def snapshot(request: pytest.FixtureRequest) -> SnapshotFixture:
117
+ """
118
+ Retrieve a `Snapshot` object getter for the current test.
119
+ The snapshot is stored in the `snapshots` directory next to the test file.
120
+
121
+ Snapshot files are named after the test function name (+ ".txt") by default.
122
+ If a name is provided to the getter, the snapshot will be stored in a file with that name.
123
+ The name is relative to the `snapshots` directory.
124
+
125
+ When `--snapshot-update` is provided to `pytest`:
126
+ - The snapshot will be created if it does not exist.
127
+ - If the comparison is false, the snapshot will be updated with the string it's being compared to in the test,
128
+ and the test will be marked as expected to fail.
129
+ """
130
+ for_update = request.config.getoption("--snapshot-update")
131
+ snapshot_dir = request.path.parent / "snapshots"
132
+ snapshot_dir.mkdir(exist_ok=True)
133
+
134
+ def _get_snapshot(name: str = "") -> Snapshot:
135
+ snapshot_name = name or f"{request.node.name}.txt"
136
+ snapshot_path = snapshot_dir / snapshot_name
137
+ return Snapshot(snapshot_path, for_update=for_update)
138
+
139
+ return _get_snapshot
@@ -0,0 +1,56 @@
1
+ from pathlib import Path
2
+ from typing import TYPE_CHECKING, Protocol
3
+
4
+ import pytest
5
+
6
+ if TYPE_CHECKING:
7
+ from task_processor.models import TaskRun
8
+
9
+
10
+ class AssertMetricFixture(Protocol):
11
+ def __call__(
12
+ self,
13
+ *,
14
+ name: str,
15
+ labels: dict[str, str],
16
+ value: float | int,
17
+ ) -> None: ...
18
+
19
+
20
+ class RunTasksFixture(Protocol):
21
+ def __call__(
22
+ self,
23
+ num_tasks: int,
24
+ ) -> "list[TaskRun]": ...
25
+
26
+
27
+ class SnapshotFixture(Protocol):
28
+ def __call__(self, name: str = "") -> "Snapshot": ...
29
+
30
+
31
+ class Snapshot:
32
+ """
33
+ Read contents of `path` and make them available for comparison via the `==` operator.
34
+ If the contents are different, and `Snapshot` initialised in update mode,
35
+ (e.g. by running `pytest` with `--snapshot-update`), write the new contents to `path`.
36
+ """
37
+
38
+ def __init__(self, path: Path, for_update: bool) -> None:
39
+ self.path = path
40
+ mode = "r" if not for_update else "w+"
41
+ self.content: str = open(path, encoding="utf-8", mode=mode).read()
42
+ self.for_update = for_update
43
+
44
+ def __eq__(self, other: object) -> bool:
45
+ if self.content == other:
46
+ return True
47
+ if self.for_update and isinstance(other, str):
48
+ with open(self.path, "w", encoding="utf-8") as f:
49
+ f.write(other)
50
+ pytest.xfail(reason=f"Snapshot updated: {self.path}")
51
+ return False
52
+
53
+ def __str__(self) -> str:
54
+ return self.content
55
+
56
+ __repr__ = __str__
@@ -0,0 +1,11 @@
1
+ from typing import Literal
2
+
3
+ from common.core.utils import is_enterprise, is_saas
4
+
5
+
6
+ def edition_printer() -> Literal["saas!", "enterprise!", "oss!"]:
7
+ if is_saas():
8
+ return "saas!"
9
+ if is_enterprise():
10
+ return "enterprise!"
11
+ return "oss!"
common/types.py ADDED
@@ -0,0 +1,45 @@
1
+ import typing
2
+
3
+ if typing.TYPE_CHECKING:
4
+ from django.contrib.contenttypes.models import ContentType
5
+ from django.db import models
6
+
7
+ FeatureStateValue: typing.TypeAlias = models.Model
8
+ FeatureSegment: typing.TypeAlias = models.Model
9
+ Condition: typing.TypeAlias = models.Model
10
+ MultivariateFeatureStateValue: typing.TypeAlias = models.Model
11
+ Metadata: typing.TypeAlias = models.Model
12
+ Organisation: typing.TypeAlias = models.Model
13
+
14
+ class SoftDeleteExportableModel(models.Model):
15
+ def hard_delete(self) -> None: ...
16
+
17
+ class Segment(SoftDeleteExportableModel):
18
+ id: int
19
+ version: int | None
20
+ rules = models.ForeignKey("Rule", on_delete=models.CASCADE)
21
+
22
+ def deep_clone(self) -> "Segment": ...
23
+
24
+ class Rule(models.Model):
25
+ def get_segment(self) -> Segment: ...
26
+
27
+ class SegmentRule(Rule):
28
+ pass
29
+
30
+ class Project(models.Model):
31
+ organisation: "Organisation"
32
+ max_segments_allowed: int
33
+
34
+ class MetadataField(models.Model):
35
+ name: str
36
+ organisation: "Organisation"
37
+
38
+ class MetadataModelField(models.Model):
39
+ field: "MetadataField"
40
+ content_type: ContentType
41
+
42
+ class MetadataModelFieldRequirement(models.Model):
43
+ model_field: "MetadataModelField"
44
+ object_id: int
45
+ content_type: ContentType