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,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: flagsmith-common
3
+ Version: 2.2.4
4
+ Summary: Flagsmith's common library
5
+ License-Expression: BSD-3-Clause
6
+ License-File: LICENSE
7
+ Author: Matthew Elwell
8
+ Maintainer: Flagsmith Team
9
+ Maintainer-email: support@flagsmith.com
10
+ Requires-Python: >=3.11,<4.0
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Pytest
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: BSD License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Provides-Extra: test-tools
21
+ Requires-Dist: backoff (>=2.2.1,<3.0.0)
22
+ Requires-Dist: django (>4,<5)
23
+ Requires-Dist: django-health-check
24
+ Requires-Dist: djangorestframework
25
+ Requires-Dist: djangorestframework-recursive
26
+ Requires-Dist: drf-writable-nested
27
+ Requires-Dist: drf-yasg (>=1.21.10,<2.0.0)
28
+ Requires-Dist: environs (<15)
29
+ Requires-Dist: flagsmith-flag-engine
30
+ Requires-Dist: gunicorn (>=19.1)
31
+ Requires-Dist: prometheus-client (>=0.0.16)
32
+ Requires-Dist: psycopg2-binary (>=2.9,<3)
33
+ Requires-Dist: pyfakefs (>=5,<6) ; extra == "test-tools"
34
+ Requires-Dist: pytest-django (>=4,<5) ; extra == "test-tools"
35
+ Requires-Dist: requests
36
+ Requires-Dist: simplejson (>=3,<4)
37
+ Project-URL: Changelog, https://github.com/flagsmith/flagsmith-common/blob/main/CHANGELOG.md
38
+ Project-URL: Download, https://github.com/flagsmith/flagsmith-common/releases
39
+ Project-URL: Homepage, https://flagsmith.com
40
+ Project-URL: Issues, https://github.com/flagsmith/flagsmith-common/issues
41
+ Project-URL: Repository, https://github.com/flagsmith/flagsmith-common
42
+ Description-Content-Type: text/markdown
43
+
44
+ # flagsmith-common
45
+
46
+ [![Coverage](https://codecov.io/gh/Flagsmith/flagsmith-common/graph/badge.svg?token=L3OGOXH86K)](https://codecov.io/gh/Flagsmith/flagsmith-common)
47
+
48
+ Flagsmith's common library
49
+
50
+ ### Development Setup
51
+
52
+ This project uses [Poetry](https://python-poetry.org/) for dependency management and includes a Makefile to simplify common development tasks.
53
+
54
+ #### Prerequisites
55
+
56
+ - Python >= 3.11
57
+ - Make
58
+
59
+ #### Installation
60
+
61
+ You can set up your development environment using the provided Makefile:
62
+
63
+ ```bash
64
+ # Install everything (pip, poetry, and project dependencies)
65
+ make install
66
+
67
+ # Individual installation steps are also available
68
+ make install-pip # Upgrade pip
69
+ make install-poetry # Install Poetry
70
+ make install-packages # Install project dependencies
71
+ ```
72
+
73
+ #### Development
74
+
75
+ Run linting checks using pre-commit:
76
+
77
+ ```bash
78
+ make lint
79
+ ```
80
+
81
+ Additional options can be passed to the `install-packages` target:
82
+
83
+ ```bash
84
+ # Install with development dependencies
85
+ make install-packages opts="--with dev"
86
+
87
+ # Install with specific extras
88
+ make install-packages opts="--extras 'feature1 feature2'"
89
+ ```
90
+
91
+ ### Usage
92
+
93
+ #### Installation
94
+
95
+ 1. `poetry add flagsmith-common`
96
+
97
+ 2. `poetry add --G dev flagsmith-common[test-tools]` — this will enable the Pytest fixtures. Skipping this step will make Pytest collection fail due to missing dependencies.
98
+
99
+ 3. Make sure `"common.core"` is in the `INSTALLED_APPS` of your settings module.
100
+ This enables the `manage.py flagsmith` commands.
101
+
102
+ 4. Add `"common.gunicorn.middleware.RouteLoggerMiddleware"` to `MIDDLEWARE` in your settings module.
103
+ This enables the `route` label for Prometheus HTTP metrics.
104
+
105
+ 5. To enable the `/metrics` endpoint, set the `PROMETHEUS_ENABLED` setting to `True`.
106
+
107
+ #### Test tools
108
+
109
+ ##### Fixtures
110
+
111
+ ###### `assert_metric`
112
+
113
+ To test your metrics using the `assert_metric` fixture:
114
+
115
+ ```python
116
+ from common.test_tools import AssertMetricFixture
117
+
118
+ def test_my_code__expected_metrics(assert_metric: AssertMetricFixture) -> None:
119
+ # When
120
+ my_code()
121
+
122
+ # Then
123
+ assert_metric(
124
+ name="flagsmith_distance_from_earth_au_sum",
125
+ labels={"engine_type": "solar_sail"},
126
+ value=1.0,
127
+ )
128
+ ```
129
+
130
+ ###### `saas_mode`
131
+
132
+ The `saas_mode` fixture makes all `common.core.utils.is_saas` calls return `True`.
133
+
134
+ ###### `enterprise_mode`
135
+
136
+ The `enterprise_mode` fixture makes all `common.core.utils.is_enterprise` calls return `True`.
137
+
138
+ ##### Markers
139
+
140
+ ###### `pytest.mark.saas_mode`
141
+
142
+ Use this mark to auto-use the `saas_mode` fixture.
143
+
144
+ ###### `pytest.mark.enterprise_mode`
145
+
146
+ Use this mark to auto-use the `enterprise_mode` fixture.
147
+
148
+ #### Metrics
149
+
150
+ Flagsmith uses Prometheus to track performance metrics.
151
+
152
+ The following default metrics are exposed:
153
+
154
+ ##### Common metrics
155
+
156
+ - `flagsmith_build_info`: Has the labels `version` and `ci_commit_sha`.
157
+ - `flagsmith_http_server_request_duration_seconds`: Histogram labeled with `method`, `route`, and `response_status`.
158
+ - `flagsmith_http_server_requests_total`: Counter labeled with `method`, `route`, and `response_status`.
159
+ - `flagsmith_http_server_response_size_bytes`:Histogram labeled with `method`, `route`, and `response_status`.
160
+ - `flagsmith_task_processor_enqueued_tasks_total`: Counter labeled with `task_identifier`.
161
+
162
+ ##### Task Processor metrics
163
+
164
+ - `flagsmith_task_processor_finished_tasks_total`: Counter labeled with `task_identifier`, `task_type` (`"recurring"`, `"standard"`) and `result` (`"success"`, `"failure"`).
165
+ - `flagsmith_task_processor_task_duration_seconds`: Histogram labeled with `task_identifier`, `task_type` (`"recurring"`, `"standard"`) and `result` (`"success"`, `"failure"`).
166
+
167
+ ##### Guidelines
168
+
169
+ Try to come up with meaningful metrics to cover your feature with when developing it. Refer to [Prometheus best practices][1] when naming your metric and labels.
170
+
171
+ As a reasonable default, Flagsmith metrics are expected to be namespaced with the `"flagsmith_"` prefix.
172
+
173
+ Define your metrics in a `metrics.py` module of your Django application — see [example][2]. Contrary to Prometheus Python client examples and documentation, please name a metric variable exactly as your metric name.
174
+
175
+ It's generally a good idea to allow users to define histogram buckets of their own. Flagsmith accepts a `PROMETHEUS_HISTOGRAM_BUCKETS` setting so users can customise their buckets. To honour the setting, use the `common.prometheus.Histogram` class when defining your histograms. When using `prometheus_client.Histogram` directly, please expose a dedicated setting like so:
176
+
177
+ ```python
178
+ import prometheus_client
179
+ from django.conf import settings
180
+
181
+ flagsmith_distance_from_earth_au = prometheus_client.Histogram(
182
+ "flagsmith_distance_from_earth_au",
183
+ "Distance from Earth in astronomical units",
184
+ labels=["engine_type"],
185
+ buckets=settings.DISTANCE_FROM_EARTH_AU_HISTOGRAM_BUCKETS,
186
+ )
187
+ ```
188
+
189
+ For testing your metrics, refer to [`assert_metric` documentation][5].
190
+
191
+ [1]: https://prometheus.io/docs/practices/naming/
192
+ [2]: https://github.com/Flagsmith/flagsmith-common/blob/main/src/common/gunicorn/metrics.py
193
+ [3]: https://docs.gunicorn.org/en/stable/design.html#server-model
194
+ [4]: https://prometheus.github.io/client_python/multiprocess
195
+ [5]: #assert_metric
196
+
@@ -0,0 +1,92 @@
1
+ common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ common/core/__init__.py,sha256=y0vhhSyK_c2S5BwrxJOBKKP9hOQeI03WeZ_Xdp7CLuU,114
3
+ common/core/app.py,sha256=QRhmj2MPnbPwjjEje-QDlmgQuYWNq7cGoUiCf8Ky_GU,116
4
+ common/core/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ common/core/cli/healthcheck.py,sha256=UxiaqDu0v8GxbGT6oLdpI7WN6rdS7a3NjwNvcbSHTcE,3002
6
+ common/core/logging.py,sha256=r2Ig21YIfq-R4MhtF614N2UjhW6EH1_Zs9BhrVLSQFk,807
7
+ common/core/main.py,sha256=MOtTaWSjYptLFOR-E7__C4bb6cPn7lUtMiXI-F_SGws,3176
8
+ common/core/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ common/core/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ common/core/management/commands/docgen.py,sha256=TlNYJDtAo7xIfc4eLxjdXoXbriD0XYQdlhbUwFk70WA,1909
11
+ common/core/management/commands/start.py,sha256=74UsFvLZqfFb9O0gz32K005ooONmne1NlV2xEI-GVjw,2009
12
+ common/core/management/commands/waitfordb.py,sha256=Mkdjp07x_5vYhx0309peX8TzBcXls7b19zGv1f6Ncr0,2951
13
+ common/core/metrics.py,sha256=_7dwLCn4XkTjXG63BVEEvggqENgoVZs61dMrZeepE0A,630
14
+ common/core/middleware.py,sha256=l_Ps351lRkVbKuvJANtWQI3l7-iXTd1o0wvDmB3Qhdo,589
15
+ common/core/templates/docgen-metrics.md,sha256=3VJFvE-5DAjcIlS-ZUya-MkgV9HFDqSnqpSyOsjmE4Q,447
16
+ common/core/urls.py,sha256=2spNF4eeXDIh4auHWheUf-qqgvNBLm_xCPBpeew7suE,752
17
+ common/core/utils.py,sha256=Ys0scxweEXVM3yM5-QD_3eMJA3N2Z4IcV7gPdad_RZo,7321
18
+ common/core/views.py,sha256=4PNgC9gLoi6NVTmn84VVYxtDrtcDr9Waa3S1IY04U6g,704
19
+ common/environments/permissions.py,sha256=TkYOcRBYogasSJTMS1YxLffucO7ItsTklq3aRHalFhY,494
20
+ common/features/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ common/features/multivariate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ common/features/multivariate/serializers.py,sha256=lryUuwSMC7-fo60KhyQRY-GPCaTF6Fzwy4qFZtdXozw,522
23
+ common/features/serializers.py,sha256=qodyM6cGaCP8IHwyYuex_9CJMDBUDt9RUnHEo5zD7Q4,1995
24
+ common/features/versioning/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
+ common/features/versioning/serializers.py,sha256=iyMrjy7_JsjD7E6KOSAd3P8QAhReEnAVgHkpt6BCAaA,439
26
+ common/gunicorn/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ common/gunicorn/conf.py,sha256=rht6KqrnYpHRjP-3S8kwJEYIqjWkDX9Ggl-JAW_J4Kw,641
28
+ common/gunicorn/constants.py,sha256=kSV94KSf_a8C1OkXeq4Eaxm1QcideeKyRBTO79RJubk,532
29
+ common/gunicorn/logging.py,sha256=tFzv6Z-gm0gMATDiW2HBcdRaSggFUzWPxU5xDA_br-I,4465
30
+ common/gunicorn/metrics.py,sha256=IBXQNg--cfA2wscqMlTMzZFolODuauA8dPeHB2b2SiA,924
31
+ common/gunicorn/middleware.py,sha256=YBt_lXSaZd8__9SyNErDT8xmfgRrHWGdzyeo3VHwfDc,798
32
+ common/gunicorn/utils.py,sha256=W7xJF1-YBkgttFyD8nCpYkSArWXJIXvonAECYHOtUNA,3484
33
+ common/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
+ common/migrations/helpers/__init__.py,sha256=sOWChvIDGaxWSj_5ySH97zBSiHprYPDzl-pjbfWWOM8,344
35
+ common/migrations/helpers/postgres_helpers.py,sha256=BHvTYwShWfLdiZb6tYh91FnyO9atnbN4Q0kU74qbNcw,1365
36
+ common/organisations/permissions.py,sha256=zTtJHWGJU2JUV_GRb3RxsMYuWL01tyEiEbbAztmQ09s,318
37
+ common/projects/permissions.py,sha256=uQXxfYhTtXk3JGukxUmXHULp2xPH4HaM1E0uQ4SxGcE,1677
38
+ common/prometheus/__init__.py,sha256=iw9EDPtC-NnbuPequjwFLIwGKEq5vnWIsyX2hcX_7Bk,72
39
+ common/prometheus/utils.py,sha256=GjXOoY4RU11DaCe0ShI5u_20pFH8Y2XTITg14FgFnMk,1203
40
+ common/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
+ common/test_tools/__init__.py,sha256=E5hVRUrjQvzw2-c6508HtQQ7uy-aS232_qHNgXy34SM,195
42
+ common/test_tools/plugin.py,sha256=Bczq2eE1Kn6RP42UYTY8lYnsSG6hbR3G_IphGVIDZaY,4196
43
+ common/test_tools/types.py,sha256=msp8h3diICSHxwY3lQlzgCNKX5jHjtwYGDr8lA2EJO4,1504
44
+ common/test_tools/utils.py,sha256=UCMJw9pV6W7pZ5KDkXQuQMfDRpClON_ilqkEltMfWqw,261
45
+ common/types.py,sha256=sgj8cUAG75EylkUNLvvg4aAn8tVpsSe_QU9idM24gL0,1341
46
+ task_processor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
+ task_processor/admin.py,sha256=rO0iTK59UqCoCOJYYBLEQXURBPTEcgB-2_HgckCNOtA,1123
48
+ task_processor/apps.py,sha256=uwjp7wBGVQxCZ2qoZecqRC5F90ApX7L9b9AXFJ1uXs0,1665
49
+ task_processor/decorators.py,sha256=7FtjEme3Ach5iYvoM4FbdY2-w8-DTAIc8W3OFKWNYfQ,7349
50
+ task_processor/exceptions.py,sha256=97e18EAjxaAxPdS-zWQ4QRhyq6BrR0Z_HV1jdp2A_Lk,633
51
+ task_processor/health.py,sha256=5lnzFMFREf-XMQhRaPDgJrpzSzZrWC3s7XKnMHanrjU,1448
52
+ task_processor/managers.py,sha256=YZWBXqpVvctuzuzntfr7DztQlAPYmUvPbm_Lkd49RVQ,577
53
+ task_processor/metrics.py,sha256=iRwuZNJBvy2UjXR4kbiUiDOaBoajFI5Up4DrUgGDRic,999
54
+ task_processor/migrations/0001_initial.py,sha256=ujWGWQknRSvczNZb9ca-ArkDy5xjbisH6O4lxDHNaJo,1898
55
+ task_processor/migrations/0002_healthcheckmodel.py,sha256=-FrMadNmOZqe1BLXGwQPuzB4xTMnMuwYKwWhcFlSsXE,591
56
+ task_processor/migrations/0003_add_completed_to_task.py,sha256=CzjyKHS2OgckwvIdZpa6oZ5zmU7HysVh49yBvfFp9bQ,546
57
+ task_processor/migrations/0004_recreate_task_indexes.py,sha256=iX8HurYMFklC36HHWZ799M6Ahx7M2y6JGOVxRwTm3bY,1591
58
+ task_processor/migrations/0005_update_conditional_index_conditions.py,sha256=1uh4i4RthQgRyw4ZuGOoDFLVLzArrXIhlZXdIZ-kxHE,1618
59
+ task_processor/migrations/0006_auto_20230221_0802.py,sha256=8Hi80cNSgpaPxgdqIicg99Wnts4pftmAfvDPkXfVUFg,1991
60
+ task_processor/migrations/0007_add_is_locked.py,sha256=MTOkS3VR-oLjm5AeDZU3h-AwhfBT4TBbFWgyRiV_KdM,560
61
+ task_processor/migrations/0008_add_get_task_to_process_function.py,sha256=KIrmsmCVOVBOleFD--YL04n1UGYt3mvn8lG9qoK4T0M,876
62
+ task_processor/migrations/0009_add_recurring_task_run_first_run_at.py,sha256=V-yaW5nmXAWVFpCbKia0ZiQTyeRe0KLCa_Yuntd55jI,429
63
+ task_processor/migrations/0010_task_priority.py,sha256=X3ORgs-AptBdMPN0Y0pZwbztPlhUGjVpb7EE8FEG1vI,693
64
+ task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py,sha256=YVbouesDcdQ4tTtAqseG-j28n1lW0k3Ci_kAjLOqleo,672
65
+ task_processor/migrations/0012_add_locked_at_and_timeout.py,sha256=iDbHvhjZzkywn4noQ3OQw4LYZ5PVzJPbEcIX2-Ta35s,1167
66
+ task_processor/migrations/0013_add_last_picked_at.py,sha256=2m5gpgaGKgb4lbKXhPIarR8kEQC6ImEKICCtzaBOeW0,953
67
+ task_processor/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
+ task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql,sha256=D5tb4WrFF8LJwyIxjTNVXLNFTUuWAczHufy2MjO67Xw,1243
69
+ task_processor/migrations/sql/0008_get_tasks_to_process.sql,sha256=7aYALpu-ELWRWnMsQh0jcSM4L6YwOIAg3L6AVrOgvJw,1298
70
+ task_processor/migrations/sql/0011_get_tasks_to_process.sql,sha256=1CC_HBSgJ8zYBARmQDAjT3iTmUVfRRkS3dPkPodX7ho,1312
71
+ task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql,sha256=nIV0Mn69z-JHn8SYJAjPnkVXRMem4m1500y1C-O47Ek,1441
72
+ task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql,sha256=Fw976UOIwkSqav_OYoxyyodLgOVesIlfWaIRqZM3qVg,1489
73
+ task_processor/migrations/sql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
+ task_processor/models.py,sha256=5OhNCaw2X4boF1b5_N3ghEQucZjHs-p1M0zJS-irvHI,7640
75
+ task_processor/monitoring.py,sha256=ysNbPWLV5t5_a28LS0JqnSStNj7B5haZNo9rVNcuAkA,278
76
+ task_processor/processor.py,sha256=b_ZpVeG0RddmPoJ6Q8AZRXL-mmhFmdxoP4-9wFv9PAQ,7014
77
+ task_processor/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
+ task_processor/routers.py,sha256=7C1KC2A8-Pzp6jWGhOqMREg5hjF3NlVjZ--IGwLa624,1752
79
+ task_processor/serializers.py,sha256=0C9PY6YYq7sz3njqDgQosX7a8Fl296P-_7I1E7mB6Hg,213
80
+ task_processor/task_registry.py,sha256=ewg8CxuFSm4geT7vr4fZIaJbyTo7HQmRyrZ_MB40sEM,2392
81
+ task_processor/task_run_method.py,sha256=WZAbODZEMukDDBrSl5wf7vMZWg5CyJ6wISCVjgx8auk,165
82
+ task_processor/tasks.py,sha256=Bg4TQPItyEUQXHOg73vaPQAPV3snPudlVKvYL0u0ei8,2280
83
+ task_processor/threads.py,sha256=EeI1t8g-W6Ow8DG0GGPsltkNpmQa__gP-0q6p_8ZpgU,4380
84
+ task_processor/types.py,sha256=VMSphDb1KAVoEUNybhAgphHdY72QLyrcTpOJD4rZvcM,388
85
+ task_processor/urls.py,sha256=mHxdF12-f2bXAgB08snjze_0HgIJnlrrzNxqbFsg_5g,123
86
+ task_processor/utils.py,sha256=0uE1VrOaMudt2glG0t1YnGWH9QYIrHRizZD16m8so1k,1838
87
+ task_processor/views.py,sha256=FX3RjO3yPv8CPX16mS64U43Iufww1vEysWogZbxVRks,819
88
+ flagsmith_common-2.2.4.dist-info/METADATA,sha256=kzSfgA7Yk_Qdjs1iDDF-H9nkthyqIcXxwrbV2OO_fng,6898
89
+ flagsmith_common-2.2.4.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
90
+ flagsmith_common-2.2.4.dist-info/entry_points.txt,sha256=SsKB3kaA63LHPbpX4wXe7twAm956lPEt28m5GUquYeI,109
91
+ flagsmith_common-2.2.4.dist-info/licenses/LICENSE,sha256=54EKQkJZc4xBIMpkJNo9EdvDXGq5TefERvXaIFF8Dac,1496
92
+ flagsmith_common-2.2.4.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,6 @@
1
+ [console_scripts]
2
+ flagsmith=common.core.main:main
3
+
4
+ [pytest11]
5
+ flagsmith-test-tools=common.test_tools.plugin
6
+
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Flagsmith
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
File without changes
@@ -0,0 +1,38 @@
1
+ from datetime import datetime
2
+
3
+ from django.contrib import admin
4
+ from django.db.models import QuerySet
5
+ from django.http import HttpRequest
6
+
7
+ from task_processor.models import RecurringTask
8
+
9
+
10
+ @admin.register(RecurringTask)
11
+ class RecurringTaskAdmin(admin.ModelAdmin[RecurringTask]):
12
+ list_display = (
13
+ "uuid",
14
+ "task_identifier",
15
+ "run_every",
16
+ "last_run_status",
17
+ "last_run_finished_at",
18
+ "is_locked",
19
+ )
20
+ readonly_fields = ("args", "kwargs")
21
+
22
+ def last_run_status(self, instance: RecurringTask) -> str | None:
23
+ if last_run := instance.task_runs.order_by("-started_at").first():
24
+ return last_run.result
25
+ return None
26
+
27
+ def last_run_finished_at(self, instance: RecurringTask) -> datetime | None:
28
+ if last_run := instance.task_runs.order_by("-started_at").first():
29
+ return last_run.finished_at
30
+ return None
31
+
32
+ @admin.action(description="Unlock selected tasks")
33
+ def unlock(
34
+ self,
35
+ request: HttpRequest,
36
+ queryset: QuerySet[RecurringTask],
37
+ ) -> None:
38
+ queryset.update(is_locked=False)
task_processor/apps.py ADDED
@@ -0,0 +1,47 @@
1
+ from django.apps import AppConfig
2
+ from django.conf import settings
3
+ from django.core.exceptions import ImproperlyConfigured
4
+ from health_check.plugins import plugin_dir # type: ignore[import-untyped]
5
+
6
+ from task_processor.task_run_method import TaskRunMethod
7
+
8
+
9
+ class TaskProcessorAppConfig(AppConfig):
10
+ name = "task_processor"
11
+
12
+ def ready(self) -> None:
13
+ if settings.TASK_RUN_METHOD != TaskRunMethod.TASK_PROCESSOR:
14
+ return
15
+
16
+ self._validate_database_settings()
17
+ self._register_health_check()
18
+
19
+ def _register_health_check(self) -> None:
20
+ """
21
+ Register the health check for the task processor
22
+ """
23
+ if not settings.ENABLE_TASK_PROCESSOR_HEALTH_CHECK:
24
+ return
25
+
26
+ from .health import TaskProcessorHealthCheckBackend
27
+
28
+ plugin_dir.register(TaskProcessorHealthCheckBackend)
29
+
30
+ def _validate_database_settings(self) -> None:
31
+ """
32
+ Validate that multi-database is setup correctly
33
+ """
34
+ if "task_processor" not in settings.TASK_PROCESSOR_DATABASES:
35
+ return # Not using a separate database
36
+
37
+ if "task_processor" not in settings.DATABASES:
38
+ raise ImproperlyConfigured(
39
+ "DATABASES must include 'task_processor' when using a separate task processor database."
40
+ )
41
+
42
+ router_name = "task_processor.routers.TaskProcessorRouter"
43
+ if router_name not in settings.DATABASE_ROUTERS:
44
+ raise ImproperlyConfigured(
45
+ "DATABASE_ROUTERS must include 'task_processor.routers.TaskProcessorRouter' "
46
+ "when using a separate task processor database."
47
+ )
@@ -0,0 +1,209 @@
1
+ import logging
2
+ import typing
3
+ from datetime import datetime, time, timedelta
4
+ from threading import Thread
5
+
6
+ from django.conf import settings
7
+ from django.db.transaction import on_commit
8
+ from django.utils import timezone
9
+
10
+ from task_processor import metrics, task_registry
11
+ from task_processor.exceptions import InvalidArgumentsError, TaskQueueFullError
12
+ from task_processor.models import RecurringTask, Task, TaskPriority
13
+ from task_processor.task_run_method import TaskRunMethod
14
+ from task_processor.types import TaskCallable, TaskParameters
15
+ from task_processor.utils import get_task_identifier_from_function
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class TaskHandler(typing.Generic[TaskParameters]):
21
+ __slots__ = (
22
+ "unwrapped",
23
+ "queue_size",
24
+ "priority",
25
+ "transaction_on_commit",
26
+ "task_identifier",
27
+ "timeout",
28
+ )
29
+
30
+ unwrapped: TaskCallable[TaskParameters]
31
+
32
+ def __init__(
33
+ self,
34
+ f: TaskCallable[TaskParameters],
35
+ *,
36
+ task_name: str | None = None,
37
+ queue_size: int | None = None,
38
+ priority: TaskPriority = TaskPriority.NORMAL,
39
+ transaction_on_commit: bool = True,
40
+ timeout: timedelta | None = None,
41
+ ) -> None:
42
+ self.unwrapped = f
43
+ self.queue_size = queue_size
44
+ self.priority = priority
45
+ self.transaction_on_commit = transaction_on_commit
46
+ self.timeout = timeout
47
+
48
+ self.task_identifier = task_identifier = get_task_identifier_from_function(
49
+ f,
50
+ task_name,
51
+ )
52
+ task_registry.register_task(task_identifier, f)
53
+
54
+ def __call__(
55
+ self,
56
+ *args: TaskParameters.args,
57
+ **kwargs: TaskParameters.kwargs,
58
+ ) -> None:
59
+ _validate_inputs(*args, **kwargs)
60
+ return self.unwrapped(*args, **kwargs)
61
+
62
+ def delay(
63
+ self,
64
+ *,
65
+ delay_until: datetime | None = None,
66
+ # TODO @khvn26 consider typing `args` and `kwargs` with `ParamSpec`
67
+ # (will require a change to the signature)
68
+ args: tuple[typing.Any, ...] = (),
69
+ kwargs: dict[str, typing.Any] | None = None,
70
+ ) -> Task | None:
71
+ task_identifier = self.task_identifier
72
+ logger.debug("Request to run task '%s' asynchronously.", task_identifier)
73
+
74
+ kwargs = kwargs or {}
75
+
76
+ if delay_until and settings.TASK_RUN_METHOD != TaskRunMethod.TASK_PROCESSOR:
77
+ # TODO: consider not having this silently fail?
78
+ logger.warning(
79
+ "Cannot schedule tasks to run in the future without task processor."
80
+ )
81
+ return None
82
+
83
+ if settings.TASK_RUN_METHOD == TaskRunMethod.SYNCHRONOUSLY:
84
+ _validate_inputs(*args, **kwargs)
85
+ self.unwrapped(*args, **kwargs)
86
+ elif settings.TASK_RUN_METHOD == TaskRunMethod.SEPARATE_THREAD:
87
+ logger.debug("Running task '%s' in separate thread", task_identifier)
88
+ self.run_in_thread(args=args, kwargs=kwargs)
89
+ else:
90
+ logger.debug("Creating task for function '%s'...", task_identifier)
91
+ metrics.flagsmith_task_processor_enqueued_tasks_total.labels(
92
+ task_identifier=task_identifier
93
+ ).inc()
94
+ try:
95
+ task = Task.create(
96
+ task_identifier=task_identifier,
97
+ scheduled_for=delay_until or timezone.now(),
98
+ priority=self.priority,
99
+ queue_size=self.queue_size,
100
+ timeout=self.timeout,
101
+ args=args,
102
+ kwargs=kwargs,
103
+ )
104
+ except TaskQueueFullError as e:
105
+ logger.warning(e)
106
+ return None
107
+
108
+ task.save()
109
+ return task
110
+ return None
111
+
112
+ def run_in_thread(
113
+ self,
114
+ *,
115
+ args: tuple[typing.Any, ...] = (),
116
+ kwargs: dict[str, typing.Any] | None = None,
117
+ ) -> None:
118
+ kwargs = kwargs or {}
119
+ _validate_inputs(*args, **kwargs)
120
+ thread = Thread(target=self.unwrapped, args=args, kwargs=kwargs, daemon=True)
121
+
122
+ def _start() -> None:
123
+ logger.info(
124
+ "Running function %s in unmanaged thread.", self.unwrapped.__name__
125
+ )
126
+ thread.start()
127
+
128
+ if self.transaction_on_commit:
129
+ return on_commit(_start)
130
+ return _start()
131
+
132
+
133
+ def register_task_handler( # noqa: C901
134
+ *,
135
+ task_name: str | None = None,
136
+ queue_size: int | None = None,
137
+ priority: TaskPriority = TaskPriority.NORMAL,
138
+ transaction_on_commit: bool = True,
139
+ timeout: timedelta | None = timedelta(seconds=60),
140
+ ) -> typing.Callable[[TaskCallable[TaskParameters]], TaskHandler[TaskParameters]]:
141
+ """
142
+ Turn a function into an asynchronous task.
143
+
144
+ :param str task_name: task name. Defaults to function name.
145
+ :param int queue_size: (`TASK_PROCESSOR` task run method only)
146
+ max queue size for the task. Task runs exceeding the max size get dropped by
147
+ the task processor Defaults to `None` (infinite).
148
+ :param TaskPriority priority: task priority.
149
+ :param bool transaction_on_commit: (`SEPARATE_THREAD` task run method only)
150
+ Whether to wrap the task call in `transaction.on_commit`. Defaults to `True`.
151
+ We need this for the task to be able to access data committed with the current
152
+ transaction. If the task is invoked outside of a transaction, it will start
153
+ immediately.
154
+ Pass `False` if you want the task to start immediately regardless of current
155
+ transaction.
156
+ :rtype: TaskHandler
157
+ """
158
+
159
+ def wrapper(f: TaskCallable[TaskParameters]) -> TaskHandler[TaskParameters]:
160
+ return TaskHandler(
161
+ f,
162
+ task_name=task_name,
163
+ queue_size=queue_size,
164
+ priority=priority,
165
+ transaction_on_commit=transaction_on_commit,
166
+ timeout=timeout,
167
+ )
168
+
169
+ return wrapper
170
+
171
+
172
+ def register_recurring_task(
173
+ run_every: timedelta,
174
+ task_name: str | None = None,
175
+ args: tuple[typing.Any, ...] = (),
176
+ kwargs: dict[str, typing.Any] | None = None,
177
+ first_run_time: time | None = None,
178
+ timeout: timedelta | None = timedelta(minutes=30),
179
+ ) -> typing.Callable[[TaskCallable[TaskParameters]], TaskCallable[TaskParameters]]:
180
+ if not settings.TASK_PROCESSOR_MODE:
181
+ # Do not register recurring tasks if not invoked by task processor
182
+ return lambda f: f
183
+
184
+ def decorator(f: TaskCallable[TaskParameters]) -> TaskCallable[TaskParameters]:
185
+ nonlocal task_name
186
+
187
+ task_name = task_name or f.__name__
188
+ task_identifier = get_task_identifier_from_function(f, task_name)
189
+
190
+ task_kwargs = {
191
+ "serialized_args": RecurringTask.serialize_data(args or ()),
192
+ "serialized_kwargs": RecurringTask.serialize_data(kwargs or {}),
193
+ "run_every": run_every,
194
+ "first_run_time": first_run_time,
195
+ "timeout": timeout,
196
+ }
197
+
198
+ task_registry.register_recurring_task(task_identifier, f, **task_kwargs)
199
+ return f
200
+
201
+ return decorator
202
+
203
+
204
+ def _validate_inputs(*args: typing.Any, **kwargs: typing.Any) -> None:
205
+ try:
206
+ Task.serialize_data(args or ())
207
+ Task.serialize_data(kwargs or {})
208
+ except TypeError as e:
209
+ raise InvalidArgumentsError("Inputs are not serializable.") from e
@@ -0,0 +1,28 @@
1
+ from datetime import datetime
2
+
3
+
4
+ class TaskProcessingError(Exception):
5
+ pass
6
+
7
+
8
+ class InvalidArgumentsError(TaskProcessingError):
9
+ pass
10
+
11
+
12
+ class TaskBackoffError(TaskProcessingError):
13
+ """
14
+ Raise this exception inside a task to indicate that it should be retried after a delay.
15
+ This is typically used when a task fails due to a temporary issue, such as
16
+ a network error or a service being unavailable.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ delay_until: datetime | None = None,
22
+ ) -> None:
23
+ super().__init__()
24
+ self.delay_until = delay_until
25
+
26
+
27
+ class TaskQueueFullError(Exception):
28
+ pass