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.
- common/__init__.py +0 -0
- common/core/__init__.py +6 -0
- common/core/app.py +6 -0
- common/core/cli/__init__.py +0 -0
- common/core/cli/healthcheck.py +120 -0
- common/core/logging.py +24 -0
- common/core/main.py +105 -0
- common/core/management/__init__.py +0 -0
- common/core/management/commands/__init__.py +0 -0
- common/core/management/commands/docgen.py +63 -0
- common/core/management/commands/start.py +61 -0
- common/core/management/commands/waitfordb.py +87 -0
- common/core/metrics.py +25 -0
- common/core/middleware.py +22 -0
- common/core/templates/docgen-metrics.md +22 -0
- common/core/urls.py +17 -0
- common/core/utils.py +239 -0
- common/core/views.py +27 -0
- common/environments/permissions.py +15 -0
- common/features/__init__.py +0 -0
- common/features/multivariate/__init__.py +0 -0
- common/features/multivariate/serializers.py +19 -0
- common/features/serializers.py +68 -0
- common/features/versioning/__init__.py +0 -0
- common/features/versioning/serializers.py +13 -0
- common/gunicorn/__init__.py +0 -0
- common/gunicorn/conf.py +18 -0
- common/gunicorn/constants.py +23 -0
- common/gunicorn/logging.py +120 -0
- common/gunicorn/metrics.py +26 -0
- common/gunicorn/middleware.py +30 -0
- common/gunicorn/utils.py +104 -0
- common/migrations/__init__.py +0 -0
- common/migrations/helpers/__init__.py +9 -0
- common/migrations/helpers/postgres_helpers.py +41 -0
- common/organisations/permissions.py +10 -0
- common/projects/permissions.py +40 -0
- common/prometheus/__init__.py +3 -0
- common/prometheus/utils.py +38 -0
- common/py.typed +0 -0
- common/test_tools/__init__.py +11 -0
- common/test_tools/plugin.py +139 -0
- common/test_tools/types.py +56 -0
- common/test_tools/utils.py +11 -0
- common/types.py +45 -0
- flagsmith_common-2.2.4.dist-info/METADATA +196 -0
- flagsmith_common-2.2.4.dist-info/RECORD +92 -0
- flagsmith_common-2.2.4.dist-info/WHEEL +4 -0
- flagsmith_common-2.2.4.dist-info/entry_points.txt +6 -0
- flagsmith_common-2.2.4.dist-info/licenses/LICENSE +28 -0
- task_processor/__init__.py +0 -0
- task_processor/admin.py +38 -0
- task_processor/apps.py +47 -0
- task_processor/decorators.py +209 -0
- task_processor/exceptions.py +28 -0
- task_processor/health.py +44 -0
- task_processor/managers.py +18 -0
- task_processor/metrics.py +22 -0
- task_processor/migrations/0001_initial.py +44 -0
- task_processor/migrations/0002_healthcheckmodel.py +21 -0
- task_processor/migrations/0003_add_completed_to_task.py +22 -0
- task_processor/migrations/0004_recreate_task_indexes.py +43 -0
- task_processor/migrations/0005_update_conditional_index_conditions.py +45 -0
- task_processor/migrations/0006_auto_20230221_0802.py +45 -0
- task_processor/migrations/0007_add_is_locked.py +23 -0
- task_processor/migrations/0008_add_get_task_to_process_function.py +31 -0
- task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +18 -0
- task_processor/migrations/0010_task_priority.py +27 -0
- task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +27 -0
- task_processor/migrations/0012_add_locked_at_and_timeout.py +40 -0
- task_processor/migrations/0013_add_last_picked_at.py +34 -0
- task_processor/migrations/__init__.py +0 -0
- task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +30 -0
- task_processor/migrations/sql/0008_get_tasks_to_process.sql +30 -0
- task_processor/migrations/sql/0011_get_tasks_to_process.sql +30 -0
- task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +33 -0
- task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +33 -0
- task_processor/migrations/sql/__init__.py +0 -0
- task_processor/models.py +237 -0
- task_processor/monitoring.py +12 -0
- task_processor/processor.py +202 -0
- task_processor/py.typed +0 -0
- task_processor/routers.py +55 -0
- task_processor/serializers.py +7 -0
- task_processor/task_registry.py +90 -0
- task_processor/task_run_method.py +7 -0
- task_processor/tasks.py +71 -0
- task_processor/threads.py +128 -0
- task_processor/types.py +18 -0
- task_processor/urls.py +5 -0
- task_processor/utils.py +71 -0
- 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
|
+
[](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,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
|
task_processor/admin.py
ADDED
|
@@ -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
|