django-eventspipe 0.1.0b1__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.
- django_eventspipe-0.1.0b1/LICENSE.txt +21 -0
- django_eventspipe-0.1.0b1/PKG-INFO +24 -0
- django_eventspipe-0.1.0b1/README.md +1 -0
- django_eventspipe-0.1.0b1/pyproject.toml +30 -0
- django_eventspipe-0.1.0b1/setup.cfg +4 -0
- django_eventspipe-0.1.0b1/src/__init__.py +0 -0
- django_eventspipe-0.1.0b1/src/admin.py +218 -0
- django_eventspipe-0.1.0b1/src/apps.py +10 -0
- django_eventspipe-0.1.0b1/src/decorators.py +53 -0
- django_eventspipe-0.1.0b1/src/django_eventspipe.egg-info/PKG-INFO +24 -0
- django_eventspipe-0.1.0b1/src/django_eventspipe.egg-info/SOURCES.txt +28 -0
- django_eventspipe-0.1.0b1/src/django_eventspipe.egg-info/dependency_links.txt +1 -0
- django_eventspipe-0.1.0b1/src/django_eventspipe.egg-info/requires.txt +2 -0
- django_eventspipe-0.1.0b1/src/django_eventspipe.egg-info/top_level.txt +12 -0
- django_eventspipe-0.1.0b1/src/exceptions.py +2 -0
- django_eventspipe-0.1.0b1/src/migrations/0001_initial.py +91 -0
- django_eventspipe-0.1.0b1/src/migrations/__init__.py +0 -0
- django_eventspipe-0.1.0b1/src/models/__init__.py +7 -0
- django_eventspipe-0.1.0b1/src/models/artifact.py +29 -0
- django_eventspipe-0.1.0b1/src/models/pipeline.py +173 -0
- django_eventspipe-0.1.0b1/src/models/pipeline_artifact.py +37 -0
- django_eventspipe-0.1.0b1/src/models/pipeline_definition.py +77 -0
- django_eventspipe-0.1.0b1/src/models/pipeline_definition_task_definition.py +8 -0
- django_eventspipe-0.1.0b1/src/models/task.py +86 -0
- django_eventspipe-0.1.0b1/src/models/task_definition.py +40 -0
- django_eventspipe-0.1.0b1/src/signals.py +15 -0
- django_eventspipe-0.1.0b1/src/tests.py +3 -0
- django_eventspipe-0.1.0b1/src/urls.py +7 -0
- django_eventspipe-0.1.0b1/src/utils.py +38 -0
- django_eventspipe-0.1.0b1/src/views.py +16 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Giuseppe Corti <giuseppe.corti@protonmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: django_eventspipe
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: TODO
|
|
5
|
+
Author-email: Giuseppe Corti <giuseppe.corti@pm.me>
|
|
6
|
+
Project-URL: Homepage, https://github.com/r3vn/django_eventspipe
|
|
7
|
+
Project-URL: Issues, https://github.com/r3vn/django_eventspipe/issues
|
|
8
|
+
Classifier: Environment :: Web Environment
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE.txt
|
|
21
|
+
Requires-Dist: django>=5.0
|
|
22
|
+
Requires-Dist: celery>=5.4
|
|
23
|
+
|
|
24
|
+
TODO
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
TODO
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "django_eventspipe"
|
|
3
|
+
version = "0.1.0b1"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name="Giuseppe Corti", email="giuseppe.corti@pm.me" },
|
|
6
|
+
]
|
|
7
|
+
description = "TODO"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"django>=5.0",
|
|
12
|
+
"celery>=5.4"
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Environment :: Web Environment",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/r3vn/django_eventspipe"
|
|
29
|
+
Issues = "https://github.com/r3vn/django_eventspipe/issues"
|
|
30
|
+
|
|
File without changes
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from django import forms
|
|
2
|
+
from django.contrib import admin
|
|
3
|
+
from django.utils.html import format_html
|
|
4
|
+
from django.urls import reverse
|
|
5
|
+
|
|
6
|
+
from .utils import linkify, PrettyJSONEncoder
|
|
7
|
+
|
|
8
|
+
from .models import (
|
|
9
|
+
PipelineDefinition,
|
|
10
|
+
PipelineDefinitionTaskDefinition,
|
|
11
|
+
Task,
|
|
12
|
+
TaskDefinition,
|
|
13
|
+
Pipeline,
|
|
14
|
+
Artifact,
|
|
15
|
+
PipelineArtifact
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
class InlinePipeline(admin.TabularInline):
|
|
19
|
+
model = PipelineArtifact
|
|
20
|
+
extra = 0
|
|
21
|
+
ordering = ("pk",)
|
|
22
|
+
can_delete = False
|
|
23
|
+
readonly_fields = [
|
|
24
|
+
'pk',
|
|
25
|
+
'pipeline',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
class InlineTask(admin.TabularInline):
|
|
29
|
+
model = Task
|
|
30
|
+
extra = 0
|
|
31
|
+
ordering = ("pk",)
|
|
32
|
+
can_delete = False
|
|
33
|
+
readonly_fields = [
|
|
34
|
+
'_status',
|
|
35
|
+
'_name',
|
|
36
|
+
'node',
|
|
37
|
+
'start_ts',
|
|
38
|
+
'end_ts'
|
|
39
|
+
]
|
|
40
|
+
exclude = [
|
|
41
|
+
'definition',
|
|
42
|
+
'pipeline_definition',
|
|
43
|
+
'status'
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
def _status(self, obj):
|
|
47
|
+
"""
|
|
48
|
+
Read job's status properly
|
|
49
|
+
"""
|
|
50
|
+
colors = ["#2196F3","#04AA6D","#f23232", "#f8f8f8", "#ff9800"]
|
|
51
|
+
return format_html(
|
|
52
|
+
"<span style=\"background-color:%s;display:block;text-align:center;font-size:0.6rem;padding:1px;max-width:55px;\"><b>%s</b></span>"
|
|
53
|
+
% (
|
|
54
|
+
colors[obj.status],
|
|
55
|
+
obj.get_status_display().upper()
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def _name(self, obj):
|
|
60
|
+
return "%s" % obj.definition.task_definition.function
|
|
61
|
+
|
|
62
|
+
class JsonOptionsForm(forms.ModelForm):
|
|
63
|
+
options = forms.JSONField(
|
|
64
|
+
encoder=PrettyJSONEncoder,
|
|
65
|
+
required=False
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
class InlineTaskDefinition(admin.TabularInline):
|
|
69
|
+
model = PipelineDefinitionTaskDefinition
|
|
70
|
+
extra = 0
|
|
71
|
+
ordering = ("order",)
|
|
72
|
+
#form = JsonOptionsForm
|
|
73
|
+
|
|
74
|
+
exclude = ['options']
|
|
75
|
+
|
|
76
|
+
def _name(self, obj):
|
|
77
|
+
"""
|
|
78
|
+
Get Job function name from JobDefinition object
|
|
79
|
+
"""
|
|
80
|
+
return "%s" % obj.task_definition.function
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@admin.register(Pipeline)
|
|
84
|
+
class PipelineAdmin(admin.ModelAdmin):
|
|
85
|
+
list_display = (
|
|
86
|
+
"pk",
|
|
87
|
+
"_status",
|
|
88
|
+
"name",
|
|
89
|
+
linkify("user"),
|
|
90
|
+
"start_ts",
|
|
91
|
+
"end_ts"
|
|
92
|
+
)
|
|
93
|
+
readonly_fields = []
|
|
94
|
+
inlines = [InlineTask]
|
|
95
|
+
|
|
96
|
+
# https://stackoverflow.com/a/19884095
|
|
97
|
+
def get_readonly_fields(self, request, obj=None):
|
|
98
|
+
return list(self.readonly_fields) + \
|
|
99
|
+
[field.name for field in obj._meta.fields] + \
|
|
100
|
+
[field.name for field in obj._meta.many_to_many]
|
|
101
|
+
|
|
102
|
+
def has_add_permission(self, request):
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def has_delete_permission(self, request, obj=None):
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
def _status(self, obj):
|
|
109
|
+
"""
|
|
110
|
+
Read job's status properly
|
|
111
|
+
"""
|
|
112
|
+
colors = ["#2196F3","#04AA6D","#f23232", "#f8f8f8"]
|
|
113
|
+
return format_html(
|
|
114
|
+
"<span style=\"background-color:%s;display:block;text-align:center;font-size:0.6rem;padding:1px;max-width:55px;\"><b>%s</b></span>"
|
|
115
|
+
% (
|
|
116
|
+
colors[obj.status],
|
|
117
|
+
obj.get_status_display().upper()
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@admin.register(PipelineDefinition)
|
|
122
|
+
class PipelineDefinitionAdmin(admin.ModelAdmin):
|
|
123
|
+
list_display = (
|
|
124
|
+
"pk",
|
|
125
|
+
"enabled",
|
|
126
|
+
"event",
|
|
127
|
+
"filters",
|
|
128
|
+
"tasks_definition"
|
|
129
|
+
)
|
|
130
|
+
inlines = [InlineTaskDefinition]
|
|
131
|
+
form = JsonOptionsForm
|
|
132
|
+
|
|
133
|
+
@admin.action(description="Disable selected PipelineDefinition")
|
|
134
|
+
def disable_selection(self, request, queryset):
|
|
135
|
+
for object in queryset:
|
|
136
|
+
|
|
137
|
+
object.enabled = False
|
|
138
|
+
object.save()
|
|
139
|
+
|
|
140
|
+
@admin.action(description="Enable selected PipelineDefinition")
|
|
141
|
+
def enable_selection(self, request, queryset):
|
|
142
|
+
for object in queryset:
|
|
143
|
+
|
|
144
|
+
object.enabled = True
|
|
145
|
+
object.save()
|
|
146
|
+
|
|
147
|
+
@admin.action(description="Duplicate selected PipelineDefinition")
|
|
148
|
+
def duplicate_selection(self, request, queryset):
|
|
149
|
+
for object in queryset:
|
|
150
|
+
|
|
151
|
+
task_definitions = PipelineDefinitionTaskDefinition.objects.filter(pipeline_definition=object)
|
|
152
|
+
|
|
153
|
+
object.id = None
|
|
154
|
+
object.save()
|
|
155
|
+
|
|
156
|
+
for task_definition in task_definitions:
|
|
157
|
+
task_definition.id = None
|
|
158
|
+
task_definition.pipeline_definition = object
|
|
159
|
+
task_definition.save()
|
|
160
|
+
|
|
161
|
+
actions = [duplicate_selection, enable_selection, disable_selection]
|
|
162
|
+
|
|
163
|
+
def tasks_definition(self, obj):
|
|
164
|
+
"""
|
|
165
|
+
Return a "pretty" tasks execution order view
|
|
166
|
+
"""
|
|
167
|
+
rt_string = ""
|
|
168
|
+
for definition in PipelineDefinitionTaskDefinition.objects.filter(pipeline_definition=obj).order_by('order'):
|
|
169
|
+
if definition.enabled:
|
|
170
|
+
job_name = definition.task_definition.function
|
|
171
|
+
else:
|
|
172
|
+
# task is disabled, Strikethrough the function name
|
|
173
|
+
job_name = "<s style=\"color:var(--delete-button-bg)\">%s</s>" % definition.task_definition.function
|
|
174
|
+
|
|
175
|
+
rt_string = "%s <span style=\"color:var(--link-fg)\">→</span> %s" % (rt_string, job_name)
|
|
176
|
+
|
|
177
|
+
return format_html(rt_string)
|
|
178
|
+
|
|
179
|
+
@admin.register(TaskDefinition)
|
|
180
|
+
class TaskDefinitionAdmin(admin.ModelAdmin):
|
|
181
|
+
list_display = (
|
|
182
|
+
"function",
|
|
183
|
+
"description"
|
|
184
|
+
)
|
|
185
|
+
ordering = ('function',)
|
|
186
|
+
|
|
187
|
+
@admin.register(Artifact)
|
|
188
|
+
class ArtifactAdmin(admin.ModelAdmin):
|
|
189
|
+
list_display = (
|
|
190
|
+
"pk",
|
|
191
|
+
"name",
|
|
192
|
+
"size",
|
|
193
|
+
"md5sum",
|
|
194
|
+
"timestamp",
|
|
195
|
+
"download"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
inlines = [InlinePipeline]
|
|
199
|
+
readonly_fields = []
|
|
200
|
+
|
|
201
|
+
# https://stackoverflow.com/a/19884095
|
|
202
|
+
def get_readonly_fields(self, request, obj=None):
|
|
203
|
+
return list(self.readonly_fields) + \
|
|
204
|
+
[field.name for field in obj._meta.fields] + \
|
|
205
|
+
[field.name for field in obj._meta.many_to_many]
|
|
206
|
+
|
|
207
|
+
def has_add_permission(self, request):
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
def has_delete_permission(self, request, obj=None):
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
def download(self, obj: Artifact) -> str:
|
|
214
|
+
url = reverse('get_artifact', args=(obj.pk,))
|
|
215
|
+
return format_html("<a class='button' href='%s'>📝 DOWNLOAD</a>" % url)
|
|
216
|
+
|
|
217
|
+
def size(self, obj: Artifact) -> str:
|
|
218
|
+
return "%s KB" % str(obj.get_size())
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
import platform
|
|
3
|
+
|
|
4
|
+
from functools import wraps
|
|
5
|
+
|
|
6
|
+
from .models import Pipeline, Task
|
|
7
|
+
from .exceptions import TaskFailed
|
|
8
|
+
|
|
9
|
+
def tracked_task(func):
|
|
10
|
+
@wraps(func)
|
|
11
|
+
def wrapper(self, context: dict[str, object], *args, **kw) -> dict[str, object]:
|
|
12
|
+
|
|
13
|
+
# HACK: Get task name
|
|
14
|
+
if str(func.__name__) in str(func.__module__):
|
|
15
|
+
taskname = str(func.__module__)
|
|
16
|
+
else:
|
|
17
|
+
taskname = "%s.%s" % (func.__module__, func.__name__)
|
|
18
|
+
|
|
19
|
+
# Get Pipeline object
|
|
20
|
+
self.pipeline = Pipeline.objects.get(pk=context["pipeline"])
|
|
21
|
+
|
|
22
|
+
# Get current pipeline's task
|
|
23
|
+
pipeline_task = Task.objects.get(
|
|
24
|
+
pipeline=self.pipeline,
|
|
25
|
+
definition__task_definition__function=taskname
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
self.pipeline.log("executing '%s'..." % taskname)
|
|
29
|
+
|
|
30
|
+
# Start Task's tracking
|
|
31
|
+
pipeline_task.tracking_start(node=platform.node())
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
# Execute Task
|
|
35
|
+
result = func(self, context, *args, **kw)
|
|
36
|
+
|
|
37
|
+
# Set Task's status as success
|
|
38
|
+
pipeline_task.tracking_update(1)
|
|
39
|
+
|
|
40
|
+
except Exception as E:
|
|
41
|
+
# Log the Exception
|
|
42
|
+
self.pipeline.log(str(traceback.format_exc()))
|
|
43
|
+
|
|
44
|
+
# Set Task's status as failed
|
|
45
|
+
pipeline_task.tracking_update(2)
|
|
46
|
+
|
|
47
|
+
raise TaskFailed
|
|
48
|
+
|
|
49
|
+
self.pipeline.log("'%s' execution complete." % taskname)
|
|
50
|
+
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
return wrapper
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: django_eventspipe
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: TODO
|
|
5
|
+
Author-email: Giuseppe Corti <giuseppe.corti@pm.me>
|
|
6
|
+
Project-URL: Homepage, https://github.com/r3vn/django_eventspipe
|
|
7
|
+
Project-URL: Issues, https://github.com/r3vn/django_eventspipe/issues
|
|
8
|
+
Classifier: Environment :: Web Environment
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE.txt
|
|
21
|
+
Requires-Dist: django>=5.0
|
|
22
|
+
Requires-Dist: celery>=5.4
|
|
23
|
+
|
|
24
|
+
TODO
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/__init__.py
|
|
5
|
+
src/admin.py
|
|
6
|
+
src/apps.py
|
|
7
|
+
src/decorators.py
|
|
8
|
+
src/exceptions.py
|
|
9
|
+
src/signals.py
|
|
10
|
+
src/tests.py
|
|
11
|
+
src/urls.py
|
|
12
|
+
src/utils.py
|
|
13
|
+
src/views.py
|
|
14
|
+
src/django_eventspipe.egg-info/PKG-INFO
|
|
15
|
+
src/django_eventspipe.egg-info/SOURCES.txt
|
|
16
|
+
src/django_eventspipe.egg-info/dependency_links.txt
|
|
17
|
+
src/django_eventspipe.egg-info/requires.txt
|
|
18
|
+
src/django_eventspipe.egg-info/top_level.txt
|
|
19
|
+
src/migrations/0001_initial.py
|
|
20
|
+
src/migrations/__init__.py
|
|
21
|
+
src/models/__init__.py
|
|
22
|
+
src/models/artifact.py
|
|
23
|
+
src/models/pipeline.py
|
|
24
|
+
src/models/pipeline_artifact.py
|
|
25
|
+
src/models/pipeline_definition.py
|
|
26
|
+
src/models/pipeline_definition_task_definition.py
|
|
27
|
+
src/models/task.py
|
|
28
|
+
src/models/task_definition.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Generated by Django 5.0.6 on 2024-09-08 12:57
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
import django_eventspipe.utils
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.db import migrations, models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
initial = True
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
operations = [
|
|
18
|
+
migrations.CreateModel(
|
|
19
|
+
name='Artifact',
|
|
20
|
+
fields=[
|
|
21
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
22
|
+
('name', models.CharField(max_length=1024)),
|
|
23
|
+
('data', models.BinaryField(blank=True)),
|
|
24
|
+
('timestamp', models.DateTimeField(auto_now_add=True)),
|
|
25
|
+
('md5sum', models.CharField(blank=True, editable=False, max_length=32, unique=True)),
|
|
26
|
+
],
|
|
27
|
+
),
|
|
28
|
+
migrations.CreateModel(
|
|
29
|
+
name='PipelineDefinition',
|
|
30
|
+
fields=[
|
|
31
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
32
|
+
('event', models.CharField(max_length=256)),
|
|
33
|
+
('filters', models.JSONField(blank=True, default=dict, null=True)),
|
|
34
|
+
('options', models.JSONField(blank=True, default=dict, null=True)),
|
|
35
|
+
('enabled', models.BooleanField(default=True)),
|
|
36
|
+
],
|
|
37
|
+
),
|
|
38
|
+
migrations.CreateModel(
|
|
39
|
+
name='TaskDefinition',
|
|
40
|
+
fields=[
|
|
41
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
42
|
+
('function', models.CharField(max_length=512, unique=True)),
|
|
43
|
+
('description', models.TextField(blank=True, null=True)),
|
|
44
|
+
],
|
|
45
|
+
),
|
|
46
|
+
migrations.CreateModel(
|
|
47
|
+
name='Pipeline',
|
|
48
|
+
fields=[
|
|
49
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
50
|
+
('node', models.CharField(default='undefined', max_length=256)),
|
|
51
|
+
('name', models.CharField(max_length=128)),
|
|
52
|
+
('status', models.IntegerField(choices=[(0, 'running'), (1, 'success'), (2, 'error'), (3, 'queued')], default=3)),
|
|
53
|
+
('tasks_count', models.IntegerField(default=0)),
|
|
54
|
+
('current_task', models.IntegerField(default=0)),
|
|
55
|
+
('start_ts', models.DateTimeField(auto_now_add=True)),
|
|
56
|
+
('end_ts', models.DateTimeField(blank=True, null=True)),
|
|
57
|
+
('user', models.ForeignKey(on_delete=models.SET(django_eventspipe.utils.get_sentinel_user), to=settings.AUTH_USER_MODEL)),
|
|
58
|
+
('definition', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_eventspipe.pipelinedefinition')),
|
|
59
|
+
],
|
|
60
|
+
),
|
|
61
|
+
migrations.CreateModel(
|
|
62
|
+
name='PipelineArtifact',
|
|
63
|
+
fields=[
|
|
64
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
65
|
+
('artifact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_eventspipe.artifact')),
|
|
66
|
+
('pipeline', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_eventspipe.pipeline')),
|
|
67
|
+
],
|
|
68
|
+
),
|
|
69
|
+
migrations.CreateModel(
|
|
70
|
+
name='PipelineDefinitionTaskDefinition',
|
|
71
|
+
fields=[
|
|
72
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
73
|
+
('enabled', models.BooleanField(default=True)),
|
|
74
|
+
('order', models.IntegerField(default=20)),
|
|
75
|
+
('pipeline_definition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_eventspipe.pipelinedefinition')),
|
|
76
|
+
('task_definition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_eventspipe.taskdefinition')),
|
|
77
|
+
],
|
|
78
|
+
),
|
|
79
|
+
migrations.CreateModel(
|
|
80
|
+
name='Task',
|
|
81
|
+
fields=[
|
|
82
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
83
|
+
('status', models.IntegerField(choices=[(0, 'running'), (1, 'success'), (2, 'error'), (3, 'queued'), (4, 'skipped')], default=2)),
|
|
84
|
+
('node', models.CharField(default='undefined', max_length=256)),
|
|
85
|
+
('start_ts', models.DateTimeField(blank=True, null=True)),
|
|
86
|
+
('end_ts', models.DateTimeField(blank=True, null=True)),
|
|
87
|
+
('definition', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_eventspipe.pipelinedefinitiontaskdefinition')),
|
|
88
|
+
('pipeline', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_eventspipe.pipeline')),
|
|
89
|
+
],
|
|
90
|
+
),
|
|
91
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .task_definition import TaskDefinition
|
|
2
|
+
from .artifact import Artifact
|
|
3
|
+
from .pipeline_definition import PipelineDefinition
|
|
4
|
+
from .pipeline_definition_task_definition import PipelineDefinitionTaskDefinition
|
|
5
|
+
from .pipeline import Pipeline
|
|
6
|
+
from .task import Task
|
|
7
|
+
from .pipeline_artifact import PipelineArtifact
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from django.db import models
|
|
3
|
+
|
|
4
|
+
class Artifact(models.Model):
|
|
5
|
+
name = models.CharField(max_length=1024)
|
|
6
|
+
data = models.BinaryField(blank=True)
|
|
7
|
+
timestamp = models.DateTimeField(auto_now_add=True)
|
|
8
|
+
md5sum = models.CharField(max_length=32, blank=True, editable=False, unique=True) # Unique MD5 checksum
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
def get_or_create(cls, name:str, data:bytes) -> object:
|
|
12
|
+
# Compute the MD5 checksum of the data
|
|
13
|
+
md5sum = hashlib.md5(data).hexdigest()
|
|
14
|
+
|
|
15
|
+
if cls.objects.filter(md5sum=md5sum).exists():
|
|
16
|
+
return cls.objects.get(md5sum=md5sum)
|
|
17
|
+
|
|
18
|
+
file = cls(name=name,data=data,md5sum=md5sum)
|
|
19
|
+
file.save()
|
|
20
|
+
|
|
21
|
+
return file
|
|
22
|
+
|
|
23
|
+
def get_size(self) -> float:
|
|
24
|
+
"""
|
|
25
|
+
Get size in KB of a file
|
|
26
|
+
"""
|
|
27
|
+
return len(self.data) / 1000
|
|
28
|
+
|
|
29
|
+
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
|
|
3
|
+
from celery import chain
|
|
4
|
+
|
|
5
|
+
from django.db import models
|
|
6
|
+
from django.contrib.auth.models import User
|
|
7
|
+
from django.contrib.admin.models import LogEntry, ADDITION
|
|
8
|
+
from django.contrib.contenttypes.models import ContentType
|
|
9
|
+
from django.apps import apps
|
|
10
|
+
from django.utils import timezone
|
|
11
|
+
|
|
12
|
+
from django_eventspipe.utils import get_sentinel_user
|
|
13
|
+
|
|
14
|
+
class Pipeline(models.Model):
|
|
15
|
+
|
|
16
|
+
STATUS_CHOICES = [
|
|
17
|
+
(0, "running"),
|
|
18
|
+
(1, "success"),
|
|
19
|
+
(2, "error"),
|
|
20
|
+
(3, "queued")
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
node = models.CharField(max_length=256, default="undefined")
|
|
24
|
+
name = models.CharField(max_length=128)
|
|
25
|
+
status = models.IntegerField(default=3, choices=STATUS_CHOICES)
|
|
26
|
+
definition = models.ForeignKey('django_eventspipe.PipelineDefinition', on_delete=models.SET_NULL, blank=True, null=True)
|
|
27
|
+
tasks_count = models.IntegerField(default=0)
|
|
28
|
+
current_task = models.IntegerField(default=0)
|
|
29
|
+
start_ts = models.DateTimeField(auto_now_add=True)
|
|
30
|
+
end_ts = models.DateTimeField(blank=True, null=True)
|
|
31
|
+
user = models.ForeignKey(User, on_delete=models.SET(get_sentinel_user))
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def new_from_event(cls, event: dict) -> object | bool:
|
|
35
|
+
"""
|
|
36
|
+
This function attempt to create and execute a `Pipeline` objects from a dict.
|
|
37
|
+
`Pipeline` objects require a `PipelineDefinition` object defined in order to start.
|
|
38
|
+
Filtered `PipelineDefinition` objects always have priority over generic ones,
|
|
39
|
+
this allow replacing 's definitions only when required.
|
|
40
|
+
"""
|
|
41
|
+
PipelineDefinition = apps.get_model("django_eventspipe.PipelineDefinition")
|
|
42
|
+
|
|
43
|
+
# Check for available info for this event
|
|
44
|
+
if "info" in event.keys():
|
|
45
|
+
pipeline_name = "%s %s" % (event["name"], event["info"])
|
|
46
|
+
else:
|
|
47
|
+
pipeline_name = event["name"]
|
|
48
|
+
|
|
49
|
+
# Create the pipeline objects
|
|
50
|
+
pipelines = []
|
|
51
|
+
definitions = PipelineDefinition.get_definitions(event)
|
|
52
|
+
|
|
53
|
+
if len(definitions) <= 0:
|
|
54
|
+
# no such pipeline definition
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
for definition in definitions:
|
|
58
|
+
# Create Pipeline object
|
|
59
|
+
pipeline = cls(
|
|
60
|
+
name=pipeline_name,
|
|
61
|
+
user=User.objects.get(pk=event["user"]),
|
|
62
|
+
node=platform.node(),
|
|
63
|
+
definition=definition
|
|
64
|
+
)
|
|
65
|
+
pipeline.save()
|
|
66
|
+
|
|
67
|
+
# Run Pipeline
|
|
68
|
+
pipeline.run(event)
|
|
69
|
+
pipelines.append(pipeline)
|
|
70
|
+
|
|
71
|
+
return pipelines
|
|
72
|
+
|
|
73
|
+
def __str__(self):
|
|
74
|
+
return "Pipeline #%d" % self.pk
|
|
75
|
+
|
|
76
|
+
def save_artifact(self, file_name: str, file_data: bytes) -> bool:
|
|
77
|
+
"""
|
|
78
|
+
Save an artifact for this Pipeline
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
PipelineArtifact = apps.get_model("django_eventspipe.PipelineArtifact")
|
|
82
|
+
|
|
83
|
+
return PipelineArtifact.add_artifact(
|
|
84
|
+
pipeline = self,
|
|
85
|
+
file_name = file_name,
|
|
86
|
+
file_data = file_data,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def get_artifacts(self) -> dict[str, object]:
|
|
90
|
+
"""
|
|
91
|
+
Get stored artifacts for this Pipeline
|
|
92
|
+
"""
|
|
93
|
+
PipelineArtifact = apps.get_model("django_eventspipe.PipelineArtifact")
|
|
94
|
+
|
|
95
|
+
return PipelineArtifact.get_artifacts(self)
|
|
96
|
+
|
|
97
|
+
def run(self, event: dict) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Execute a `Pipeline`
|
|
100
|
+
"""
|
|
101
|
+
Task = apps.get_model("django_eventspipe.Task")
|
|
102
|
+
|
|
103
|
+
# add initial pipeline's log entry
|
|
104
|
+
self.log("Event received %s" % str(event))
|
|
105
|
+
|
|
106
|
+
# Get initial context from event
|
|
107
|
+
context = {
|
|
108
|
+
"pipeline" : self.pk
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# retrive context data from event
|
|
112
|
+
for data in event.keys():
|
|
113
|
+
context[data] = event[data]
|
|
114
|
+
|
|
115
|
+
# retrive context data from PipelineDefinitinon's options
|
|
116
|
+
for data in self.definition.options.keys():
|
|
117
|
+
context[data] = self.definition.options[data]
|
|
118
|
+
|
|
119
|
+
# Get Task definition for this event on this perimeter
|
|
120
|
+
pipeline_tasks = Task.create_tasks(self)
|
|
121
|
+
|
|
122
|
+
# Get celery chain for this pipeline
|
|
123
|
+
tasks_chain_list = self.definition.get_tasks_chain(context)
|
|
124
|
+
|
|
125
|
+
# No Tasks defined for this pipeline, exit
|
|
126
|
+
if len(pipeline_tasks) <= 0:
|
|
127
|
+
self.status = 1
|
|
128
|
+
self.tasks_count = 0
|
|
129
|
+
self.current_task = 0
|
|
130
|
+
self.end_ts = timezone.now()
|
|
131
|
+
self.save()
|
|
132
|
+
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
# Set pipeline as queued
|
|
136
|
+
self.status = 3
|
|
137
|
+
self.tasks_count = len(pipeline_tasks)
|
|
138
|
+
self.save()
|
|
139
|
+
|
|
140
|
+
# Start celery chain
|
|
141
|
+
pipeline_chain = chain(tasks_chain_list)
|
|
142
|
+
pipeline_chain.apply_async()
|
|
143
|
+
|
|
144
|
+
def log(self, entry: str) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Add a LogEntry on a `Pipeline`
|
|
147
|
+
"""
|
|
148
|
+
progress = "[%d/%d] " % (self.current_task, self.tasks_count)
|
|
149
|
+
log_entry = "%s%s" % (progress, entry)
|
|
150
|
+
|
|
151
|
+
# Add an ADDITION logentry for this asset
|
|
152
|
+
LogEntry.objects.log_action(
|
|
153
|
+
user_id=self.user.pk,
|
|
154
|
+
content_type_id=ContentType.objects.get_for_model(Pipeline).pk,
|
|
155
|
+
object_id=self.id,
|
|
156
|
+
object_repr="Pipeline #%d" % self.id,
|
|
157
|
+
action_flag=ADDITION,
|
|
158
|
+
change_message=log_entry
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def fail(self) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Set a `Pipeline` and his uncompleted `Task` objects as failed.
|
|
164
|
+
"""
|
|
165
|
+
Task = apps.get_model("django_eventspipe.Task")
|
|
166
|
+
|
|
167
|
+
# Update Pipeline object
|
|
168
|
+
self.status = 2
|
|
169
|
+
self.end_ts = timezone.now()
|
|
170
|
+
self.save()
|
|
171
|
+
|
|
172
|
+
# Update queued Tasks
|
|
173
|
+
Task.pipeline_failed(self)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.apps import apps
|
|
3
|
+
|
|
4
|
+
class PipelineArtifact(models.Model):
|
|
5
|
+
"""
|
|
6
|
+
Artifact -> Pipeline association
|
|
7
|
+
"""
|
|
8
|
+
pipeline = models.ForeignKey("django_eventspipe.Pipeline", on_delete=models.CASCADE)
|
|
9
|
+
artifact = models.ForeignKey("django_eventspipe.Artifact", on_delete=models.CASCADE)
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def add_artifact(cls, pipeline: object, file_name: str, file_data: bytes) -> bool:
|
|
13
|
+
"""
|
|
14
|
+
Add a new artifact to the database
|
|
15
|
+
"""
|
|
16
|
+
Artifact = apps.get_model("django_eventspipe.Artifact")
|
|
17
|
+
|
|
18
|
+
artifact = Artifact.get_or_create(name=file_name, data=file_data)
|
|
19
|
+
|
|
20
|
+
pipeline_artifact = cls(pipeline=pipeline, artifact=artifact)
|
|
21
|
+
pipeline_artifact.save()
|
|
22
|
+
|
|
23
|
+
return True
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def get_artifacts(cls, pipeline: object) -> dict[str, object]:
|
|
27
|
+
"""
|
|
28
|
+
Get stored artifacts for a Pipeline
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
out = {}
|
|
32
|
+
artifacts = cls.objects.filter(pipeline=pipeline)
|
|
33
|
+
|
|
34
|
+
for artifact in artifacts:
|
|
35
|
+
out[artifact.artifact.name] = bytes(artifact.artifact.data)
|
|
36
|
+
|
|
37
|
+
return out
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.apps import apps
|
|
3
|
+
from django.utils.module_loading import import_string
|
|
4
|
+
|
|
5
|
+
class PipelineDefinition(models.Model):
|
|
6
|
+
event = models.CharField(max_length=256)
|
|
7
|
+
filters = models.JSONField(blank=True, null=True, default=dict)
|
|
8
|
+
options = models.JSONField(blank=True, null=True, default=dict)
|
|
9
|
+
enabled = models.BooleanField(default=True)
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def get_definitions(cls, event):
|
|
13
|
+
"""
|
|
14
|
+
Get pipeline definitions for a given event.
|
|
15
|
+
"""
|
|
16
|
+
event_name = event.get("name")
|
|
17
|
+
|
|
18
|
+
# Use ORM to fetch enabled definitions for the event in one query
|
|
19
|
+
all_definitions = cls.objects.filter(event=event_name, enabled=True)
|
|
20
|
+
|
|
21
|
+
# Separate generic and custom definitions
|
|
22
|
+
generic_definitions = []
|
|
23
|
+
custom_definitions = []
|
|
24
|
+
|
|
25
|
+
for definition in all_definitions:
|
|
26
|
+
filters = definition.filters or {}
|
|
27
|
+
|
|
28
|
+
# Check if filters exist, otherwise it's a generic definition
|
|
29
|
+
if filters:
|
|
30
|
+
match = all(
|
|
31
|
+
event.get(key) == value
|
|
32
|
+
for key, value in filters.items()
|
|
33
|
+
if key in event
|
|
34
|
+
)
|
|
35
|
+
if match:
|
|
36
|
+
custom_definitions.append(definition)
|
|
37
|
+
else:
|
|
38
|
+
generic_definitions.append(definition)
|
|
39
|
+
|
|
40
|
+
# Return custom definitions if available, otherwise return generic ones
|
|
41
|
+
return custom_definitions if custom_definitions else generic_definitions
|
|
42
|
+
|
|
43
|
+
def get_defined_tasks(self) -> list:
|
|
44
|
+
"""
|
|
45
|
+
Get Tasks defined for this PipelineDefinition
|
|
46
|
+
"""
|
|
47
|
+
PipelineDefinitionTaskDefinition = apps.get_model('django_eventspipe.PipelineDefinitionTaskDefinition')
|
|
48
|
+
|
|
49
|
+
tasks = []
|
|
50
|
+
for definition in PipelineDefinitionTaskDefinition.objects.filter(pipeline_definition=self, enabled=True).order_by('order'):
|
|
51
|
+
tasks.append(definition)
|
|
52
|
+
|
|
53
|
+
return tasks
|
|
54
|
+
|
|
55
|
+
def get_tasks_chain(self, context) -> list:
|
|
56
|
+
"""
|
|
57
|
+
Get Tasks defined for this PipelineDefinition as a celery chain
|
|
58
|
+
"""
|
|
59
|
+
PipelineDefinitionTaskDefinition = apps.get_model('django_eventspipe.PipelineDefinitionTaskDefinition')
|
|
60
|
+
|
|
61
|
+
chain = []
|
|
62
|
+
first = True
|
|
63
|
+
|
|
64
|
+
for task_function in PipelineDefinitionTaskDefinition.objects.filter(
|
|
65
|
+
pipeline_definition=self,
|
|
66
|
+
enabled=True
|
|
67
|
+
).order_by('order').values_list('task_definition__function'):
|
|
68
|
+
|
|
69
|
+
if first:
|
|
70
|
+
signature = import_string(task_function[0]).s(context)
|
|
71
|
+
first = False
|
|
72
|
+
else:
|
|
73
|
+
signature = import_string(task_function[0]).s()
|
|
74
|
+
|
|
75
|
+
chain.append(signature)
|
|
76
|
+
|
|
77
|
+
return chain
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
class PipelineDefinitionTaskDefinition(models.Model):
|
|
4
|
+
pipeline_definition = models.ForeignKey('django_eventspipe.PipelineDefinition', on_delete=models.CASCADE)
|
|
5
|
+
task_definition = models.ForeignKey('django_eventspipe.TaskDefinition', on_delete=models.CASCADE)
|
|
6
|
+
enabled = models.BooleanField(default=True)
|
|
7
|
+
order = models.IntegerField(default=20)
|
|
8
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.utils import timezone
|
|
3
|
+
|
|
4
|
+
class Task(models.Model):
|
|
5
|
+
|
|
6
|
+
STATUS_CHOICES = [
|
|
7
|
+
(0, "running"),
|
|
8
|
+
(1, "success"),
|
|
9
|
+
(2, "error"),
|
|
10
|
+
(3, "queued"),
|
|
11
|
+
(4, "skipped")
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
status = models.IntegerField(default=2, choices=STATUS_CHOICES)
|
|
15
|
+
node = models.CharField(max_length=256, default="undefined")
|
|
16
|
+
pipeline = models.ForeignKey('django_eventspipe.Pipeline', on_delete=models.CASCADE)
|
|
17
|
+
definition = models.ForeignKey('django_eventspipe.PipelineDefinitionTaskDefinition', null=True, on_delete=models.SET_NULL)
|
|
18
|
+
start_ts = models.DateTimeField(blank=True, null=True)
|
|
19
|
+
end_ts = models.DateTimeField(blank=True, null=True)
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def create_tasks(cls, pipeline: object) -> list:
|
|
23
|
+
"""
|
|
24
|
+
Create `Task` for a given pipeline
|
|
25
|
+
"""
|
|
26
|
+
tasks = []
|
|
27
|
+
|
|
28
|
+
for defined_task in pipeline.definition.get_defined_tasks():
|
|
29
|
+
# Create Task
|
|
30
|
+
task = cls(
|
|
31
|
+
status = 3,
|
|
32
|
+
pipeline = pipeline,
|
|
33
|
+
definition = defined_task
|
|
34
|
+
)
|
|
35
|
+
task.save()
|
|
36
|
+
tasks.append(task)
|
|
37
|
+
|
|
38
|
+
return tasks
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def pipeline_failed(cls, pipeline: object) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Set all queued `Task` as failed.
|
|
44
|
+
"""
|
|
45
|
+
tasks = cls.objects.all().filter(status=3, pipeline=pipeline) # kill both queued and running
|
|
46
|
+
|
|
47
|
+
for task in tasks:
|
|
48
|
+
task.status = 2
|
|
49
|
+
task.save()
|
|
50
|
+
|
|
51
|
+
def tracking_start(self, node:str) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Start tracking a `Task`
|
|
54
|
+
"""
|
|
55
|
+
self.status = 0
|
|
56
|
+
self.node = node
|
|
57
|
+
self.start_ts = timezone.now()
|
|
58
|
+
self.save()
|
|
59
|
+
|
|
60
|
+
# Increase Pipeline's current_task
|
|
61
|
+
self.pipeline.current_task += 1
|
|
62
|
+
|
|
63
|
+
# Set Pipeline's status as running
|
|
64
|
+
if self.pipeline.status != 0:
|
|
65
|
+
self.pipeline.status = 0
|
|
66
|
+
|
|
67
|
+
self.pipeline.save()
|
|
68
|
+
|
|
69
|
+
def tracking_update(self, status: int) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Update `Task`'s tracking data
|
|
72
|
+
"""
|
|
73
|
+
# Update this object
|
|
74
|
+
self.status = status
|
|
75
|
+
self.end_ts = timezone.now()
|
|
76
|
+
self.save()
|
|
77
|
+
|
|
78
|
+
if status == 1:
|
|
79
|
+
# Check if pipeline is completed
|
|
80
|
+
if self.pipeline.current_task == self.pipeline.tasks_count:
|
|
81
|
+
self.pipeline.status = status
|
|
82
|
+
self.pipeline.end_ts = timezone.now()
|
|
83
|
+
self.pipeline.save()
|
|
84
|
+
else:
|
|
85
|
+
# Set pipeline and all queued Tasks as failed
|
|
86
|
+
self.pipeline.fail()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.utils.module_loading import import_string
|
|
3
|
+
|
|
4
|
+
class TaskDefinition(models.Model):
|
|
5
|
+
function = models.CharField(max_length=512, unique=True)
|
|
6
|
+
description = models.TextField(null=True, blank=True)
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
def check_definitions(cls, tasks: list) -> None:
|
|
10
|
+
"""
|
|
11
|
+
Automatically import `Task` objects from celery
|
|
12
|
+
"""
|
|
13
|
+
clean_tasks = []
|
|
14
|
+
|
|
15
|
+
# skip celery stuff
|
|
16
|
+
for task in tasks:
|
|
17
|
+
if "celery." in task: continue
|
|
18
|
+
clean_tasks.append(task)
|
|
19
|
+
|
|
20
|
+
# remove uknown `TaskDefinition`
|
|
21
|
+
for taskdef in cls.objects.all():
|
|
22
|
+
if taskdef.function not in clean_tasks:
|
|
23
|
+
taskdef.delete()
|
|
24
|
+
|
|
25
|
+
# import registered `Task` objects
|
|
26
|
+
for task in clean_tasks:
|
|
27
|
+
try:
|
|
28
|
+
imported_task = import_string(task)
|
|
29
|
+
|
|
30
|
+
if not cls.objects.filter(function=task).exists():
|
|
31
|
+
# Get `Task` function and documentation
|
|
32
|
+
new_task_def = cls(
|
|
33
|
+
function=task,
|
|
34
|
+
description=imported_task.__doc__
|
|
35
|
+
)
|
|
36
|
+
new_task_def.save()
|
|
37
|
+
except: pass
|
|
38
|
+
|
|
39
|
+
def __str__(self) -> str:
|
|
40
|
+
return "%s" % self.function
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django.dispatch import Signal, receiver
|
|
2
|
+
from .models import Pipeline
|
|
3
|
+
|
|
4
|
+
event_signal = Signal()
|
|
5
|
+
|
|
6
|
+
@receiver(event_signal)
|
|
7
|
+
def create_pipelines(
|
|
8
|
+
sender: object,
|
|
9
|
+
event: dict[str, object],
|
|
10
|
+
**kwargs
|
|
11
|
+
) -> None:
|
|
12
|
+
"""
|
|
13
|
+
Signal handler to create `Pipeline` objects from an event
|
|
14
|
+
"""
|
|
15
|
+
Pipeline.new_from_event(event)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from django.urls import reverse
|
|
4
|
+
from django.utils.html import format_html
|
|
5
|
+
from django.contrib.auth import get_user_model
|
|
6
|
+
from django.contrib.auth.models import User
|
|
7
|
+
|
|
8
|
+
def get_sentinel_user() -> User:
|
|
9
|
+
"""
|
|
10
|
+
Prevent pipeline removal when deleting an user
|
|
11
|
+
"""
|
|
12
|
+
return get_user_model().objects.get_or_create(username="deleted")[0]
|
|
13
|
+
|
|
14
|
+
# https://stackoverflow.com/a/53092940
|
|
15
|
+
def linkify(field_name: str) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Converts a foreign key value into clickable links.
|
|
18
|
+
|
|
19
|
+
If field_name is 'parent', link text will be str(obj.parent)
|
|
20
|
+
Link will be admin url for the admin url for obj.parent.id:change
|
|
21
|
+
"""
|
|
22
|
+
def _linkify(obj):
|
|
23
|
+
linked_obj = getattr(obj, field_name)
|
|
24
|
+
if linked_obj is None:
|
|
25
|
+
return '-'
|
|
26
|
+
app_label = linked_obj._meta.app_label
|
|
27
|
+
model_name = linked_obj._meta.model_name
|
|
28
|
+
view_name = f'admin:{app_label}_{model_name}_change'
|
|
29
|
+
link_url = reverse(view_name, args=[linked_obj.pk])
|
|
30
|
+
return format_html('<a href="{}">{}</a>', link_url, linked_obj)
|
|
31
|
+
|
|
32
|
+
_linkify.short_description = field_name # Sets column name
|
|
33
|
+
return _linkify
|
|
34
|
+
|
|
35
|
+
# https://stackoverflow.com/a/72256767
|
|
36
|
+
class PrettyJSONEncoder(json.JSONEncoder):
|
|
37
|
+
def __init__(self, *args, indent: int, sort_keys: bool, **kwargs):
|
|
38
|
+
super().__init__(*args, indent=2, sort_keys=True, **kwargs)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from django.http import HttpRequest, HttpResponse
|
|
2
|
+
from django.contrib.auth.decorators import login_required
|
|
3
|
+
|
|
4
|
+
from .models import Artifact
|
|
5
|
+
|
|
6
|
+
@login_required
|
|
7
|
+
def get_artifact(request: HttpRequest, artifact_id: int) -> HttpResponse:
|
|
8
|
+
"""
|
|
9
|
+
Get a file from Database
|
|
10
|
+
"""
|
|
11
|
+
file = Artifact.objects.get(pk=artifact_id)
|
|
12
|
+
|
|
13
|
+
response = HttpResponse(file.data, content_type='application/octet-stream')
|
|
14
|
+
response['Content-Disposition']='attachment;filename=%s' % file.name
|
|
15
|
+
|
|
16
|
+
return response
|