dasf-broker-django 0.0.1.dev0__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 (38) hide show
  1. dasf_broker/__init__.py +26 -0
  2. dasf_broker/_version.py +21 -0
  3. dasf_broker/admin.py +273 -0
  4. dasf_broker/app_settings.py +133 -0
  5. dasf_broker/apps.py +16 -0
  6. dasf_broker/constants.py +31 -0
  7. dasf_broker/consumers.py +269 -0
  8. dasf_broker/jobs/__init__.py +0 -0
  9. dasf_broker/jobs/minutely/__init__.py +0 -0
  10. dasf_broker/jobs/minutely/garbage_collect_topics.py +17 -0
  11. dasf_broker/jobs/minutely/ping_topics.py +19 -0
  12. dasf_broker/management/__init__.py +0 -0
  13. dasf_broker/management/commands/__init__.py +0 -0
  14. dasf_broker/management/commands/dasf_topic.py +353 -0
  15. dasf_broker/migrations/0001_initial.py +169 -0
  16. dasf_broker/migrations/0002_auto_20230213_1501.py +134 -0
  17. dasf_broker/migrations/__init__.py +0 -0
  18. dasf_broker/models.py +450 -0
  19. dasf_broker/py.typed +0 -0
  20. dasf_broker/routing.py +31 -0
  21. dasf_broker/templates/dasf_broker/payload_str.html +7 -0
  22. dasf_broker/templatetags/__init__.py +10 -0
  23. dasf_broker/templatetags/dasf.py +71 -0
  24. dasf_broker/tests/__init__.py +5 -0
  25. dasf_broker/tests/live_ws_server_helper.py +94 -0
  26. dasf_broker/tests/plugin.py +187 -0
  27. dasf_broker/token_auth_middleware.py +57 -0
  28. dasf_broker/urls.py +44 -0
  29. dasf_broker/views.py +87 -0
  30. dasf_broker_django-0.0.1.dev0.dist-info/METADATA +156 -0
  31. dasf_broker_django-0.0.1.dev0.dist-info/RECORD +38 -0
  32. dasf_broker_django-0.0.1.dev0.dist-info/WHEEL +5 -0
  33. dasf_broker_django-0.0.1.dev0.dist-info/entry_points.txt +2 -0
  34. dasf_broker_django-0.0.1.dev0.dist-info/licenses/LICENSES/Apache-2.0.txt +73 -0
  35. dasf_broker_django-0.0.1.dev0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +156 -0
  36. dasf_broker_django-0.0.1.dev0.dist-info/licenses/LICENSES/CC0-1.0.txt +121 -0
  37. dasf_broker_django-0.0.1.dev0.dist-info/licenses/LICENSES/EUPL-1.2.txt +190 -0
  38. dasf_broker_django-0.0.1.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,26 @@
1
+ # SPDX-FileCopyrightText: 2022-2025 Helmholtz-Zentrum hereon GmbH
2
+ #
3
+ # SPDX-License-Identifier: EUPL-1.2
4
+
5
+ """Django-based Message Broker for DASF
6
+
7
+ A Django-based message broker for the Data Analytics Software Framework (DASF)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from . import _version
13
+
14
+ __version__ = _version.get_versions()["version"]
15
+
16
+ __author__ = "Philipp S. Sommer"
17
+ __copyright__ = "Copyright (C) 2022 Helmholtz-Zentrum Hereon"
18
+ __credits__ = [
19
+ "Philipp S. Sommer",
20
+ ]
21
+ __license__ = "EUPL-1.2"
22
+
23
+ __maintainer__ = "Helmholtz-Zentrum Hereon"
24
+ __email__ = "hcdc_support@hereon.de"
25
+
26
+ __status__ = "Pre-Alpha"
@@ -0,0 +1,21 @@
1
+
2
+ # This file was generated by 'versioneer.py' (0.29) from
3
+ # revision-control system data, or from the parent directory name of an
4
+ # unpacked source archive. Distribution tarballs contain a pre-generated copy
5
+ # of this file.
6
+
7
+ import json
8
+
9
+ version_json = '''
10
+ {
11
+ "date": "2025-04-16T22:10:07+0200",
12
+ "dirty": false,
13
+ "error": null,
14
+ "full-revisionid": "b5fb8bd0d75756eb7c531bba7b50a9d73401626b",
15
+ "version": "0.0.1.dev0"
16
+ }
17
+ ''' # END VERSION_JSON
18
+
19
+
20
+ def get_versions():
21
+ return json.loads(version_json)
dasf_broker/admin.py ADDED
@@ -0,0 +1,273 @@
1
+ # SPDX-FileCopyrightText: 2022-2025 Helmholtz-Zentrum hereon GmbH
2
+ #
3
+ # SPDX-License-Identifier: EUPL-1.2
4
+
5
+ """Admin interfaces
6
+ ----------------
7
+
8
+ This module defines the dasf-broker-django
9
+ Admin interfaces.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING, Any, List, Optional, Tuple
15
+
16
+ from django.contrib import admin # noqa: F401
17
+ from django.utils.timezone import now
18
+ from guardian.admin import GuardedModelAdmin
19
+
20
+ from dasf_broker import models # noqa: F401
21
+
22
+ if TYPE_CHECKING:
23
+ from django.db.models import QuerySet
24
+
25
+
26
+ class ResponseTopicExistsFilter(admin.SimpleListFilter):
27
+ """Filter topics for response topics"""
28
+
29
+ title = "is response topic"
30
+
31
+ parameter_name = "responsetopic"
32
+
33
+ def lookups(self, request: Any, model_admin: Any) -> List[Tuple[Any, str]]:
34
+ return [("1", "Yes"), ("0", "No")]
35
+
36
+ def queryset(
37
+ self, request: Any, queryset: QuerySet[Any]
38
+ ) -> Optional[QuerySet[Any]]:
39
+ value = self.value()
40
+ if value == "1":
41
+ return queryset.filter(responsetopic__isnull=False)
42
+ elif value == "0":
43
+ return queryset.filter(responsetopic__isnull=True)
44
+ else:
45
+ return queryset
46
+
47
+
48
+ class AvailabilityFilter(admin.SimpleListFilter):
49
+ """Filter topics based on their status."""
50
+
51
+ title = "Availability"
52
+
53
+ parameter_name = "last_pong"
54
+
55
+ def lookups(self, request: Any, model_admin: Any) -> List[Tuple[Any, str]]:
56
+ return [
57
+ ("offline", "Offline"),
58
+ ("online", "Online"),
59
+ ("unknown", "Unknown"),
60
+ ]
61
+
62
+ def queryset( # type: ignore
63
+ self,
64
+ request: Any,
65
+ queryset: models.BrokerTopicQuerySet, # type: ignore
66
+ ) -> Optional[QuerySet[Any]]:
67
+ value = self.value()
68
+ if value == "offline":
69
+ return queryset.filter_offline()
70
+ elif value == "online":
71
+ return queryset.filter_online()
72
+ elif value == "unknown":
73
+ return queryset.filter_unknown_availability()
74
+ else:
75
+ return queryset
76
+
77
+
78
+ @admin.action(description="Ping topics")
79
+ def ping_topics(modeladmin, request, queryset):
80
+ """Ping the broker topics."""
81
+ for topic in queryset.filter(supports_dasf=True):
82
+ topic.ping()
83
+
84
+
85
+ @admin.action(description="Garbage collect topics")
86
+ def garbage_collect(modeladmin, request, queryset):
87
+ queryset.filter(garbage_collect_on__lte=now()).delete()
88
+
89
+
90
+ @admin.register(models.BrokerTopic)
91
+ class BrokerTopicAdmin(GuardedModelAdmin):
92
+ """An admin for :model:`dasf_broker.BrokerTopic"""
93
+
94
+ search_fields = [
95
+ "slug__icontains",
96
+ ]
97
+
98
+ list_display = [
99
+ "slug",
100
+ "get_consumers",
101
+ "get_producers",
102
+ "is_public",
103
+ "is_response_topic",
104
+ "get_availability",
105
+ "pending_messages",
106
+ "stored_messages",
107
+ "stored_responses",
108
+ ]
109
+
110
+ list_filter = [
111
+ "is_public",
112
+ "date_created",
113
+ ResponseTopicExistsFilter,
114
+ AvailabilityFilter,
115
+ ]
116
+
117
+ actions = [ping_topics, garbage_collect]
118
+
119
+ @admin.display(description="Consumer(s)") # type: ignore
120
+ def get_consumers(self, obj: models.BrokerTopic) -> str:
121
+ return ", ".join(map(str, obj.consumers))[:60]
122
+
123
+ @admin.display(description="Producer(s)") # type: ignore
124
+ def get_producers(self, obj: models.BrokerTopic) -> str:
125
+ return ", ".join(map(str, obj.producers))[:60]
126
+
127
+ @admin.display(boolean=True) # type: ignore
128
+ def is_response_topic(self, obj: models.BrokerTopic) -> bool:
129
+ return obj.is_response_topic
130
+
131
+ @admin.display(description="Online?", boolean=True) # type: ignore
132
+ def get_availability(self, obj: models.BrokerTopic) -> Optional[bool]:
133
+ return obj.availability
134
+
135
+ def pending_messages(self, obj: models.BrokerTopic) -> str:
136
+ """Get the number of messages that have not yet been delivered"""
137
+ return str(obj.brokermessage_set.filter(delivered_to=None).count())
138
+
139
+ def stored_messages(self, obj: models.BrokerTopic) -> str:
140
+ """Get the number of messages in the database."""
141
+ return str(obj.brokermessage_set.count())
142
+
143
+ def stored_responses(self, obj: models.BrokerTopic) -> str:
144
+ """Get the number of messages in the database."""
145
+ if obj.is_response_topic:
146
+ return "-"
147
+ else:
148
+ return str(
149
+ models.BrokerMessage.objects.filter(
150
+ topic__responsetopic__is_response_for=obj
151
+ ).count()
152
+ )
153
+
154
+
155
+ @admin.register(models.ResponseTopic)
156
+ class ResponseTopicAdmin(BrokerTopicAdmin):
157
+ """An admin for response topics."""
158
+
159
+ search_fields = BrokerTopicAdmin.search_fields + [
160
+ "is_response_for__slug__icontains"
161
+ ]
162
+
163
+ list_display = BrokerTopicAdmin.list_display[:-1]
164
+
165
+ list_filter = [
166
+ "is_public",
167
+ "date_created",
168
+ ]
169
+
170
+
171
+ @admin.action(description="Send messages")
172
+ def send_messages(modeladmin, request, queryset):
173
+ for message in queryset:
174
+ message.send()
175
+
176
+
177
+ @admin.action(description="Ping topics")
178
+ def ping_message_topics(modeladmin, request, queryset):
179
+ pks = queryset.values_list("topic__pk", flat=True)
180
+ for topic in models.BrokerTopic.objects.filter(
181
+ pk__in=pks, supports_dasf=True
182
+ ):
183
+ topic.ping()
184
+
185
+
186
+ class IsResponseMessageFilter(admin.SimpleListFilter):
187
+ """Filter broker messages to a response topic"""
188
+
189
+ title = "is response message"
190
+
191
+ parameter_name = "topic"
192
+
193
+ def lookups(self, request: Any, model_admin: Any) -> List[Tuple[Any, str]]:
194
+ return [("1", "Yes"), ("0", "No")]
195
+
196
+ def queryset(
197
+ self, request: Any, queryset: QuerySet[Any]
198
+ ) -> Optional[QuerySet[Any]]:
199
+ value = self.value()
200
+ if value == "1":
201
+ return queryset.filter(topic__responsetopic__isnull=False)
202
+ elif value == "0":
203
+ return queryset.filter(topic__responsetopic__isnull=True)
204
+ else:
205
+ return queryset
206
+
207
+
208
+ class HasBeenDeliveredFilter(admin.SimpleListFilter):
209
+ """Filter broker messages by their delivery state"""
210
+
211
+ title = "has been delivered"
212
+
213
+ parameter_name = "delivered_to"
214
+
215
+ def lookups(self, request: Any, model_admin: Any) -> List[Tuple[Any, str]]:
216
+ return [("1", "Yes"), ("0", "No")]
217
+
218
+ def queryset(
219
+ self, request: Any, queryset: QuerySet[Any]
220
+ ) -> Optional[QuerySet[Any]]:
221
+ value = self.value()
222
+ if value == "1":
223
+ return queryset.filter(delivered_to=True)
224
+ elif value == "0":
225
+ return queryset.filter(delivered_to=None)
226
+ else:
227
+ return queryset
228
+
229
+
230
+ @admin.register(models.BrokerMessage)
231
+ class BrokerMessageAdmin(GuardedModelAdmin):
232
+ """An admin for a broker message"""
233
+
234
+ search_fields = [
235
+ "topic__slug__icontains",
236
+ "topic__responsetopic__is_response_for__slug__icontains",
237
+ "user__username__icontains",
238
+ "user__email__icontains",
239
+ "user__first_name__icontains",
240
+ "user__last_name__icontains",
241
+ ]
242
+
243
+ list_display = [
244
+ "message_id",
245
+ "topic",
246
+ "topic_availability",
247
+ "is_response",
248
+ "user",
249
+ "delivered",
250
+ "date_created",
251
+ ]
252
+
253
+ list_filter = [
254
+ "date_created",
255
+ IsResponseMessageFilter,
256
+ HasBeenDeliveredFilter,
257
+ ]
258
+
259
+ readonly_fields = ["content"]
260
+
261
+ actions = [send_messages, ping_message_topics]
262
+
263
+ @admin.display(boolean=True) # type: ignore
264
+ def delivered(self, obj: models.BrokerMessage) -> bool:
265
+ return obj.delivered_to.exists()
266
+
267
+ @admin.display(boolean=True) # type: ignore
268
+ def is_response(self, obj: models.BrokerMessage) -> bool:
269
+ return obj.is_response
270
+
271
+ @admin.display(boolean=True, description="Topic online?")
272
+ def topic_availability(self, obj: models.BrokerMessage) -> Optional[bool]:
273
+ return obj.topic.availability
@@ -0,0 +1,133 @@
1
+ # SPDX-FileCopyrightText: 2022-2025 Helmholtz-Zentrum hereon GmbH
2
+ #
3
+ # SPDX-License-Identifier: EUPL-1.2
4
+
5
+ """App settings
6
+ ------------
7
+
8
+ This module defines the settings options for the
9
+ ``dasf-broker-django`` app.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from enum import Enum
15
+ from typing import Optional
16
+
17
+ from django.conf import settings # noqa: F401
18
+
19
+ #: Create new topics on message
20
+ #:
21
+ #: This flag controls if new topics are created when a message comes from a
22
+ #: producer. If False, messages with non-existing topics are ignored.
23
+ #:
24
+ #: Note that a user also needs the `dasf_broker.add_BrokerTopic`
25
+ #: permission to create topics.
26
+ DASF_CREATE_TOPIC_ON_MESSAGE: bool = getattr(
27
+ settings, "DASF_CREATE_TOPIC_ON_MESSAGE", True
28
+ )
29
+
30
+
31
+ class StoreMessageOptions(str, Enum):
32
+ DISABLED = "disabled"
33
+ CACHE = "cache"
34
+ CACHEALL = "cacheall"
35
+ STORE = "store"
36
+
37
+
38
+ #: Shall the messages be stored?
39
+ #:
40
+ #: This flag controls whether the message broker caches messages from producers
41
+ #: until they are consumed by the consumer. This is useful if the consumer
42
+ #: looses connection to the server. This settings can take three different
43
+ #: values:
44
+ #:
45
+ #: ``"disabled"``
46
+ #: The message is not stored at all
47
+ #: ``"cache"``
48
+ #: The message is stored and removed ones one of the potential `consumers`
49
+ #: acknowledges the message
50
+ #: ``"cacheall"``
51
+ #: The message is stored and removed ones **all** `consumers` acknowledged
52
+ #: the message
53
+ #: ``"store"``
54
+ #: The message and response topics are stored forever and are not
55
+ #: automatically removed
56
+ #:
57
+ #: See Also
58
+ #: --------
59
+ #: DASF_STORE_SOURCE_MESSAGES
60
+ #: DASF_STORE_RESPONSE_MESSAGES
61
+ DASF_STORE_MESSAGES: StoreMessageOptions = StoreMessageOptions(
62
+ getattr(settings, "DASF_STORE_MESSAGES", StoreMessageOptions.CACHE)
63
+ )
64
+
65
+ #: Shall the source messages be stored?
66
+ #:
67
+ #: This flag controls whether the message broker caches messages from producers
68
+ #: to topics that are **not** marked as response topic. If this setting is not
69
+ #: set, we use the ``DASF_STORE_MESSAGES`` setting.
70
+ DASF_STORE_SOURCE_MESSAGES: StoreMessageOptions = StoreMessageOptions(
71
+ getattr(settings, "DASF_STORE_SOURCE_MESSAGES", DASF_STORE_MESSAGES)
72
+ )
73
+
74
+ #: Shall the messages to response topics be stored?
75
+ #:
76
+ #: This flag controls whether the message broker caches messages from producers
77
+ #: to topics that are marked as response topic. If this setting is not set, we
78
+ #: use the ``DASF_STORE_MESSAGES`` setting.
79
+ DASF_STORE_RESPONSE_MESSAGES: StoreMessageOptions = StoreMessageOptions(
80
+ getattr(settings, "DASF_STORE_RESPONSE_MESSAGES", DASF_STORE_MESSAGES)
81
+ )
82
+
83
+ #: URL route for the websocket
84
+ #:
85
+ #: This setting controls, where we expect to find the websockets.
86
+ #: As there is no analog to :func:`django.urls.reverse` for channels, you
87
+ #: should use this setting in your ``asgi.py`` file to include the routes
88
+ #: of this package.
89
+ #:
90
+ #: Examples
91
+ #: --------
92
+ #: In your ``asgi.py`` file, include it like:
93
+ #:
94
+ #: .. code-block:: python
95
+ #:
96
+ #: from channels.routing import ProtocolTypeRouter, URLRouter
97
+ #: from django.core.asgi import get_asgi_application
98
+ #: from channels.auth import AuthMiddlewareStack
99
+ #: import dasf_broker.routing as dasf_routing
100
+ #: from dasf_broker.app_settings import DASF_WEBSOCKET_URL_ROUTE
101
+ #:
102
+ #: application = ProtocolTypeRouter(
103
+ #: {
104
+ #: "http": get_asgi_application(),
105
+ #: "websocket": AuthMiddlewareStack(
106
+ #: URLRouter(
107
+ #: [
108
+ #: path(
109
+ #: DASF_WEBSOCKET_URL_ROUTE,
110
+ #: URLRouter(dasf_routing.websocket_urlpatterns),
111
+ #: )
112
+ #: ]
113
+ #: )
114
+ #: ),
115
+ #: }
116
+ #: )
117
+ DASF_WEBSOCKET_URL_ROUTE: str = getattr(
118
+ settings, "DASF_WEBSOCKET_URL_ROUTE", "ws/"
119
+ )
120
+
121
+
122
+ #: root URL to your application
123
+ #:
124
+ #: You can use this setting if you are behind a reverse proxy and the
125
+ #: host names, etc. are not handled correctly.
126
+ #:
127
+ #: If you leave this empty, we will use the ``build_absolute_uri`` method
128
+ #: of the http request.
129
+ #:
130
+ #: Examples
131
+ #: --------
132
+ #: A standard value for this would be ``http://localhost:8000``
133
+ ROOT_URL: Optional[str] = None
dasf_broker/apps.py ADDED
@@ -0,0 +1,16 @@
1
+ # SPDX-FileCopyrightText: 2022-2025 Helmholtz-Zentrum hereon GmbH
2
+ #
3
+ # SPDX-License-Identifier: EUPL-1.2
4
+
5
+ """App config
6
+ ----------
7
+
8
+ App config for the dasf_broker app.
9
+ """
10
+
11
+ from django.apps import AppConfig
12
+
13
+
14
+ class DjangoHelmholtzAaiConfig(AppConfig):
15
+ default_auto_field = "django.db.models.BigAutoField"
16
+ name = "dasf_broker"
@@ -0,0 +1,31 @@
1
+ # SPDX-FileCopyrightText: 2022-2025 Helmholtz-Zentrum hereon GmbH
2
+ #
3
+ # SPDX-License-Identifier: EUPL-1.2
4
+
5
+ """Constants within the DASF Framework
6
+ -----------------------------------
7
+
8
+ Constants from :mod:`demessaging.PulsarMessageConstants`.
9
+ """
10
+
11
+ from enum import Enum
12
+
13
+
14
+ class PropertyKeys(str, Enum):
15
+ REQUEST_CONTEXT = "requestContext"
16
+ RESPONSE_TOPIC = "response_topic"
17
+ REQUEST_MESSAGEID = "requestMessageId"
18
+ MESSAGE_TYPE = "messageType"
19
+ FRAGMENT = "fragment"
20
+ NUM_FRAGMENTS = "num_fragments"
21
+ STATUS = "status"
22
+
23
+
24
+ class MessageType(str, Enum):
25
+ PING = "ping"
26
+ PONG = "pong"
27
+ REQUEST = "request"
28
+ RESPONSE = "response"
29
+ LOG = "log"
30
+ INFO = "info"
31
+ PROGRESS = "progress"