flagsmith-common 2.0.0__tar.gz → 2.2.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.
Files changed (92) hide show
  1. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/PKG-INFO +1 -1
  2. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/pyproject.toml +1 -1
  3. flagsmith_common-2.2.0/src/common/core/__init__.py +6 -0
  4. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/core/utils.py +74 -2
  5. flagsmith_common-2.0.0/src/task_processor/migrations/sql/__init__.py +0 -0
  6. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/LICENSE +0 -0
  7. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/README.md +0 -0
  8. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/__init__.py +0 -0
  9. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/core/app.py +0 -0
  10. {flagsmith_common-2.0.0/src/common/core → flagsmith_common-2.2.0/src/common/core/cli}/__init__.py +0 -0
  11. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/core/cli/healthcheck.py +0 -0
  12. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/core/logging.py +0 -0
  13. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/core/main.py +0 -0
  14. {flagsmith_common-2.0.0/src/common/core/cli → flagsmith_common-2.2.0/src/common/core/management}/__init__.py +0 -0
  15. {flagsmith_common-2.0.0/src/common/core/management → flagsmith_common-2.2.0/src/common/core/management/commands}/__init__.py +0 -0
  16. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/core/management/commands/docgen.py +0 -0
  17. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/core/management/commands/start.py +0 -0
  18. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/core/management/commands/waitfordb.py +0 -0
  19. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/core/metrics.py +0 -0
  20. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/core/middleware.py +0 -0
  21. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/core/templates/docgen-metrics.md +0 -0
  22. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/core/urls.py +0 -0
  23. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/core/views.py +0 -0
  24. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/environments/permissions.py +0 -0
  25. {flagsmith_common-2.0.0/src/common/core/management/commands → flagsmith_common-2.2.0/src/common/features}/__init__.py +0 -0
  26. {flagsmith_common-2.0.0/src/common/features → flagsmith_common-2.2.0/src/common/features/multivariate}/__init__.py +0 -0
  27. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/features/multivariate/serializers.py +0 -0
  28. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/features/serializers.py +0 -0
  29. {flagsmith_common-2.0.0/src/common/features/multivariate → flagsmith_common-2.2.0/src/common/features/versioning}/__init__.py +0 -0
  30. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/features/versioning/serializers.py +0 -0
  31. {flagsmith_common-2.0.0/src/common/features/versioning → flagsmith_common-2.2.0/src/common/gunicorn}/__init__.py +0 -0
  32. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/gunicorn/conf.py +0 -0
  33. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/gunicorn/constants.py +0 -0
  34. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/gunicorn/logging.py +0 -0
  35. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/gunicorn/metrics.py +0 -0
  36. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/gunicorn/middleware.py +0 -0
  37. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/gunicorn/utils.py +0 -0
  38. {flagsmith_common-2.0.0/src/common/gunicorn → flagsmith_common-2.2.0/src/common/migrations}/__init__.py +0 -0
  39. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/migrations/helpers/__init__.py +0 -0
  40. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/migrations/helpers/postgres_helpers.py +0 -0
  41. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/organisations/permissions.py +0 -0
  42. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/projects/permissions.py +0 -0
  43. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/prometheus/__init__.py +0 -0
  44. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/prometheus/utils.py +0 -0
  45. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/py.typed +0 -0
  46. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/test_tools/__init__.py +0 -0
  47. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/test_tools/plugin.py +0 -0
  48. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/test_tools/types.py +0 -0
  49. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/test_tools/utils.py +0 -0
  50. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/common/types.py +0 -0
  51. {flagsmith_common-2.0.0/src/common/migrations → flagsmith_common-2.2.0/src/task_processor}/__init__.py +0 -0
  52. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/admin.py +0 -0
  53. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/apps.py +0 -0
  54. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/decorators.py +0 -0
  55. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/exceptions.py +0 -0
  56. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/health.py +0 -0
  57. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/managers.py +0 -0
  58. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/metrics.py +0 -0
  59. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/0001_initial.py +0 -0
  60. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/0002_healthcheckmodel.py +0 -0
  61. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/0003_add_completed_to_task.py +0 -0
  62. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/0004_recreate_task_indexes.py +0 -0
  63. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/0005_update_conditional_index_conditions.py +0 -0
  64. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/0006_auto_20230221_0802.py +0 -0
  65. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/0007_add_is_locked.py +0 -0
  66. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/0008_add_get_task_to_process_function.py +0 -0
  67. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +0 -0
  68. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/0010_task_priority.py +0 -0
  69. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +0 -0
  70. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/0012_add_locked_at_and_timeout.py +0 -0
  71. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/0013_add_last_picked_at.py +0 -0
  72. {flagsmith_common-2.0.0/src/task_processor → flagsmith_common-2.2.0/src/task_processor/migrations}/__init__.py +0 -0
  73. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +0 -0
  74. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/sql/0008_get_tasks_to_process.sql +0 -0
  75. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/sql/0011_get_tasks_to_process.sql +0 -0
  76. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +0 -0
  77. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +0 -0
  78. {flagsmith_common-2.0.0/src/task_processor/migrations → flagsmith_common-2.2.0/src/task_processor/migrations/sql}/__init__.py +0 -0
  79. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/models.py +0 -0
  80. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/monitoring.py +0 -0
  81. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/processor.py +0 -0
  82. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/py.typed +0 -0
  83. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/routers.py +0 -0
  84. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/serializers.py +0 -0
  85. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/task_registry.py +0 -0
  86. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/task_run_method.py +0 -0
  87. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/tasks.py +0 -0
  88. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/threads.py +0 -0
  89. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/types.py +0 -0
  90. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/urls.py +0 -0
  91. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/utils.py +0 -0
  92. {flagsmith_common-2.0.0 → flagsmith_common-2.2.0}/src/task_processor/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flagsmith-common
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: Flagsmith's common library
5
5
  License: BSD-3-Clause
6
6
  Author: Matthew Elwell
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flagsmith-common"
3
- version = "2.0.0"
3
+ version = "2.2.0"
4
4
  description = "Flagsmith's common library"
5
5
  requires-python = ">=3.11,<4.0"
6
6
  dependencies = [
@@ -0,0 +1,6 @@
1
+ import enum
2
+
3
+
4
+ class ReplicaReadStrategy(enum.StrEnum):
5
+ DISTRIBUTED = enum.auto()
6
+ SEQUENTIAL = enum.auto()
@@ -1,16 +1,30 @@
1
1
  import json
2
+ import logging
2
3
  import pathlib
4
+ import random
3
5
  from functools import lru_cache
4
- from typing import NotRequired, TypedDict
6
+ from itertools import cycle
7
+ from typing import Iterator, Literal, NotRequired, TypedDict, TypeVar, get_args
5
8
 
6
9
  from django.conf import settings
7
10
  from django.contrib.auth import get_user_model
8
11
  from django.contrib.auth.models import AbstractBaseUser
9
- from django.db.models import Manager
12
+ from django.db import connections
13
+ from django.db.models import Manager, Model
14
+ from django.db.utils import OperationalError
15
+
16
+ from common.core import ReplicaReadStrategy
17
+
18
+ logger = logging.getLogger(__name__)
10
19
 
11
20
  UNKNOWN = "unknown"
12
21
  VERSIONS_INFO_FILE_LOCATION = ".versions.json"
13
22
 
23
+ ManagerType = TypeVar("ManagerType", bound=Manager[Model])
24
+
25
+ ReplicaNamePrefix = Literal["replica_", "cross_region_replica_"]
26
+ _replica_sequential_names_by_prefix: dict[ReplicaNamePrefix, Iterator[str]] = {}
27
+
14
28
 
15
29
  class SelfHostedData(TypedDict):
16
30
  has_users: bool
@@ -114,3 +128,61 @@ def get_file_contents(file_path: str) -> str | None:
114
128
  return f.read().replace("\n", "")
115
129
  except FileNotFoundError:
116
130
  return None
131
+
132
+
133
+ @lru_cache()
134
+ def is_database_replica_setup() -> bool:
135
+ """Checks if any database replica is set up"""
136
+ return any(
137
+ name for name in connections if name.startswith(get_args(ReplicaNamePrefix))
138
+ )
139
+
140
+
141
+ def using_database_replica(
142
+ manager: ManagerType,
143
+ replica_prefix: ReplicaNamePrefix = "replica_",
144
+ ) -> ManagerType:
145
+ """Attempts to bind a manager to a healthy database replica"""
146
+ local_replicas = [name for name in connections if name.startswith(replica_prefix)]
147
+
148
+ if not local_replicas:
149
+ logger.info("No replicas set up.")
150
+ return manager
151
+
152
+ chosen_replica = None
153
+
154
+ if settings.REPLICA_READ_STRATEGY == ReplicaReadStrategy.SEQUENTIAL:
155
+ sequence = _replica_sequential_names_by_prefix.setdefault(
156
+ replica_prefix, cycle(local_replicas)
157
+ )
158
+ for _ in range(len(local_replicas)):
159
+ attempted_replica = next(sequence)
160
+ try:
161
+ connections[attempted_replica].ensure_connection()
162
+ chosen_replica = attempted_replica
163
+ break
164
+ except OperationalError:
165
+ logger.exception(f"Replica '{attempted_replica}' is not available.")
166
+ continue
167
+
168
+ if settings.REPLICA_READ_STRATEGY == ReplicaReadStrategy.DISTRIBUTED:
169
+ for _ in range(len(local_replicas)):
170
+ attempted_replica = random.choice(local_replicas)
171
+ try:
172
+ connections[attempted_replica].ensure_connection()
173
+ chosen_replica = attempted_replica
174
+ break
175
+ except OperationalError:
176
+ logger.exception(f"Replica '{attempted_replica}' is not available.")
177
+ local_replicas.remove(attempted_replica)
178
+ continue
179
+
180
+ if not chosen_replica:
181
+ if replica_prefix == "replica_":
182
+ logger.warning("Falling back to cross-region replicas, if any.")
183
+ return using_database_replica(manager, "cross_region_replica_")
184
+
185
+ logger.warning("No replicas available.")
186
+ return manager
187
+
188
+ return manager.db_manager(chosen_replica)