openmodule 15.2.0__tar.gz → 16.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openmodule-15.2.0 → openmodule-16.0.0}/PKG-INFO +1 -1
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/alert.py +2 -2
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/core.py +1 -1
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/database/custom_types.py +1 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/database/migration.py +17 -5
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/dispatcher.py +5 -18
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/health.py +38 -35
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/messaging.py +7 -2
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/models/access_service.py +18 -18
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/models/base.py +32 -3
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/models/presence.py +25 -14
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/rpc/server.py +25 -19
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/sentry.py +1 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/charset.py +12 -3
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/io.py +5 -5
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/kv_store.py +12 -8
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/matching.py +43 -11
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/package_reader.py +19 -10
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/presence.py +1 -1
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/schedule.py +1 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/signal_listener.py +7 -4
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/validation.py +1 -1
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule.egg-info/PKG-INFO +1 -1
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule.egg-info/SOURCES.txt +0 -2
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_connection_status.py +1 -1
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_core.py +2 -2
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_database.py +7 -4
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_dispatcher.py +6 -9
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_model.py +23 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_rpc.py +5 -5
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_sentry.py +3 -4
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_access_service.py +18 -15
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_eventlog.py +9 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_kv_store.py +9 -8
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_kv_store_multiple.py +26 -22
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_matching.py +7 -4
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_presence.py +1 -1
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_vehicle.py +1 -1
- openmodule-15.2.0/openmodule/utils/schema.py +0 -188
- openmodule-15.2.0/tests/test_schema.py +0 -243
- {openmodule-15.2.0 → openmodule-16.0.0}/LICENSE +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/README.md +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/__init__.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/config.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/connection_status.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/database/__init__.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/database/database.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/database/env.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/logging.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/models/__init__.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/models/alert.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/models/io.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/models/kv_store.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/models/privacy.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/models/rpc.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/models/settings.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/models/signals.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/models/validation.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/models/vehicle.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/rpc/__init__.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/rpc/client.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/rpc/common.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/threading.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/__init__.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/access_service.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/cleanup.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/csv_export.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/databox.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/db_helper.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/eventlog.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/misc_functions.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/settings.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule/utils/translation.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule.egg-info/dependency_links.txt +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule.egg-info/not-zip-safe +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule.egg-info/requires.txt +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/openmodule.egg-info/top_level.txt +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/setup.cfg +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/setup.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_alembic_migrations.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_alert.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_checks.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_config.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_health.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_interrupt.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_io_listen.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_logging.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_messaging.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_mockrpcclient.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_test_alert.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_test_gate.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_test_zeromq.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_charset.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_cleanup.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_csv_export.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_databox.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_misc_functions.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_package_reader.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_schedule.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_settings.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_signal.py +0 -0
- {openmodule-15.2.0 → openmodule-16.0.0}/tests/test_utils_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import threading
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
5
|
from openmodule.models.alert import AlertHandleType, AlertMessage, AlertStatus
|
|
6
6
|
|
|
@@ -83,7 +83,7 @@ class AlertHandler(object):
|
|
|
83
83
|
Returns True if alert was send else False"""
|
|
84
84
|
assert status in [AlertStatus.error, AlertStatus.ok, AlertStatus.offline]
|
|
85
85
|
|
|
86
|
-
status_dict = {
|
|
86
|
+
status_dict: dict[str, Any] = {
|
|
87
87
|
"status": status,
|
|
88
88
|
"alert_meta": (meta or {}),
|
|
89
89
|
}
|
|
@@ -51,7 +51,7 @@ class OpenModuleCore(threading.Thread):
|
|
|
51
51
|
self.alerts = AlertHandler(self)
|
|
52
52
|
self.connection_listener = ConnectionStatusListener(self._messages_internal, self.rpc_client)
|
|
53
53
|
self._messages_internal.register_handler("healthz", HealthPingMessage, self.health.process_message,
|
|
54
|
-
match_type=True
|
|
54
|
+
match_type=True)
|
|
55
55
|
|
|
56
56
|
def init_database(self):
|
|
57
57
|
from openmodule.database.database import Database
|
|
@@ -11,6 +11,7 @@ class CustomTypeImportRegistration(type):
|
|
|
11
11
|
This metaclass is used to register custom types with their module names.
|
|
12
12
|
This is used in generating alembic migrations.
|
|
13
13
|
"""
|
|
14
|
+
|
|
14
15
|
def __new__(cls, name, bases, dct):
|
|
15
16
|
x = super().__new__(cls, name, bases, dct)
|
|
16
17
|
|
|
@@ -3,13 +3,12 @@ import shutil
|
|
|
3
3
|
import sys
|
|
4
4
|
import warnings
|
|
5
5
|
from typing import Optional
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
from alembic import command, context
|
|
8
8
|
from alembic.autogenerate import comparators, renderers
|
|
9
9
|
from alembic.config import Config
|
|
10
10
|
from alembic.operations import Operations, MigrateOperation
|
|
11
11
|
from alembic.runtime.migration import MigrationContext
|
|
12
|
-
|
|
13
12
|
from sqlalchemy import MetaData, DateTime, inspect, text
|
|
14
13
|
from sqlalchemy.engine import Engine
|
|
15
14
|
|
|
@@ -21,13 +20,26 @@ class PreUpgradeOp(MigrateOperation):
|
|
|
21
20
|
@classmethod
|
|
22
21
|
def pre_upgrade(cls, operations, **kw):
|
|
23
22
|
migration_context: MigrationContext = operations.migration_context
|
|
24
|
-
|
|
23
|
+
db_file = migration_context.connection.engine.url.database
|
|
24
|
+
db_dir = os.path.dirname(db_file)
|
|
25
|
+
basename, _ = os.path.splitext(os.path.basename(db_file))
|
|
25
26
|
timestamp = utcnow().strftime('%Y%m%d%H%M%S')
|
|
26
27
|
migration_revision = migration_context.get_current_revision()
|
|
27
28
|
filename = f"{basename}_{timestamp}_{migration_revision}.sqlite3.backup"
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
for file in os.listdir(db_dir):
|
|
31
|
+
if file.startswith(f"{basename}_") and file.endswith(".sqlite3.backup"):
|
|
32
|
+
try:
|
|
33
|
+
file_timestamp = file[len(basename) + 1:len(basename) + 15]
|
|
34
|
+
file_time = utcnow().strptime(file_timestamp, '%Y%m%d%H%M%S')
|
|
35
|
+
if (utcnow() - file_time).seconds < 60:
|
|
36
|
+
# No backup if we created one in the last 60 seconds
|
|
37
|
+
break
|
|
38
|
+
except Exception:
|
|
39
|
+
# ignore any issues here
|
|
40
|
+
pass
|
|
41
|
+
else:
|
|
42
|
+
shutil.copy(db_file, os.path.join(db_dir, filename))
|
|
31
43
|
|
|
32
44
|
op = PreUpgradeOp(**kw)
|
|
33
45
|
return operations.invoke(op)
|
|
@@ -18,7 +18,6 @@ from sentry_sdk.utils import qualname_from_function
|
|
|
18
18
|
from openmodule import sentry
|
|
19
19
|
from openmodule.config import settings
|
|
20
20
|
from openmodule.models.base import ZMQMessage
|
|
21
|
-
from openmodule.utils.schema import Schema
|
|
22
21
|
|
|
23
22
|
|
|
24
23
|
class DummyExecutor(Executor):
|
|
@@ -142,22 +141,11 @@ class MessageDispatcher:
|
|
|
142
141
|
except ValueError:
|
|
143
142
|
pass
|
|
144
143
|
|
|
145
|
-
def _is_test_handler(self, handler):
|
|
146
|
-
"""
|
|
147
|
-
this breaks separation a bit by including test specific code in the main module
|
|
148
|
-
but it improves developer usability drastically
|
|
149
|
-
"""
|
|
150
|
-
if settings.TESTING and "Mock" in str(handler):
|
|
151
|
-
return True
|
|
152
|
-
else:
|
|
153
|
-
return False
|
|
154
|
-
|
|
155
144
|
def register_handler(self, topic: str,
|
|
156
145
|
message_class: type[ZMQMessageSub],
|
|
157
146
|
handler: Callable[[ZMQMessageSub], None], *,
|
|
158
147
|
filter: dict | Callable[[dict], bool] | None = None,
|
|
159
|
-
match_type=True
|
|
160
|
-
register_schema=True):
|
|
148
|
+
match_type=True):
|
|
161
149
|
"""
|
|
162
150
|
registers a message handler. without any filters all messages from the topic are
|
|
163
151
|
sent to the message handler.
|
|
@@ -184,8 +172,9 @@ class MessageDispatcher:
|
|
|
184
172
|
listener = Listener(message_class, type_, filter, handler)
|
|
185
173
|
self.listeners[topic].append(listener)
|
|
186
174
|
|
|
187
|
-
if
|
|
188
|
-
|
|
175
|
+
if settings.TESTING and hasattr(handler, "__global__") and "/tests/" not in handler.__globals__['__file__']:
|
|
176
|
+
# only doc string necessary for functions or lambdas (so not for mocks) outside of tests folder
|
|
177
|
+
assert handler.__doc__, f"You need to describe the message handler {handler} with a doc string"
|
|
189
178
|
|
|
190
179
|
return listener
|
|
191
180
|
|
|
@@ -243,13 +232,11 @@ class SubscribingMessageDispatcher(MessageDispatcher):
|
|
|
243
232
|
message_class: type[ZMQMessageSub],
|
|
244
233
|
handler: Callable[[ZMQMessageSub], None], *,
|
|
245
234
|
filter: dict | None = None,
|
|
246
|
-
register_schema=True,
|
|
247
235
|
match_type=True):
|
|
248
236
|
assert isinstance(topic, str), "channel must be a string"
|
|
249
237
|
|
|
250
238
|
self.subscribe(topic)
|
|
251
|
-
return super().register_handler(topic, message_class, handler, filter=filter,
|
|
252
|
-
register_schema=register_schema, match_type=match_type)
|
|
239
|
+
return super().register_handler(topic, message_class, handler, filter=filter, match_type=match_type)
|
|
253
240
|
|
|
254
241
|
def unregister_handler(self, listener: Listener):
|
|
255
242
|
super().unregister_handler(listener)
|
|
@@ -34,25 +34,26 @@ class HealthMetricType(StrEnum):
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class HealthMetric(OpenModuleModel):
|
|
37
|
-
id: str = Field(
|
|
38
|
-
|
|
39
|
-
name: str = Field(
|
|
40
|
-
description: str = Field(
|
|
41
|
-
|
|
42
|
-
type: HealthMetricType = Field(
|
|
43
|
-
|
|
44
|
-
labels: list[str] = Field(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
values: dict[str, Any] = Field(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
37
|
+
id: str = Field(description="A identifier for this metric. Must be unique per `package` this check is "
|
|
38
|
+
"assigned to, independently of the source service which is returning the metric.")
|
|
39
|
+
name: str = Field(description="Human readable name of the Metric.")
|
|
40
|
+
description: str = Field(description="Human-readable description of the metric. The same description will be"
|
|
41
|
+
"displayed when the metric is displayed.")
|
|
42
|
+
type: HealthMetricType = Field(description="The type of this metric. This is used to determine how the metric"
|
|
43
|
+
"is used.")
|
|
44
|
+
labels: list[str] = Field(description="List of label names for this metric. In debug and testing mode exactly"
|
|
45
|
+
"those labels are required for the metric to be valid. In production"
|
|
46
|
+
"wrong labels are still accepted.")
|
|
47
|
+
values: dict[str, Any] = Field(default={},
|
|
48
|
+
description="Contains the values for this metric, grouped by labels. The labels are "
|
|
49
|
+
"represented as a the string of the sorted list of label items. "
|
|
50
|
+
"str(list(<label_dict>.items()))'[(<label_name>, <label_value>), "
|
|
51
|
+
"(<label_name>, <label_value>), ...]: <metric_value>'")
|
|
52
|
+
package: str = Field(description="The package this metric is assigned to. It may be your own service"
|
|
53
|
+
"but can also be any other hardware or software service.")
|
|
54
|
+
source: str | None = Field(default=None,
|
|
54
55
|
description="The service which reported the health check. This value is "
|
|
55
|
-
|
|
56
|
+
"automatically set by health service and must not be set manually.")
|
|
56
57
|
|
|
57
58
|
|
|
58
59
|
class HealthCheckState(StrEnum):
|
|
@@ -62,25 +63,24 @@ class HealthCheckState(StrEnum):
|
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
class HealthCheck(OpenModuleModel):
|
|
65
|
-
id: str = Field(
|
|
66
|
-
|
|
67
|
-
package: str = Field(
|
|
68
|
-
|
|
69
|
-
state: HealthCheckState = Field(HealthCheckState.no_data, description="The state of this check")
|
|
70
|
-
name: str = Field(
|
|
71
|
-
description: str | None = Field(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
message: str | None = Field(None,
|
|
66
|
+
id: str = Field(description="A identifier for this check. Must be unique per `package` this check is "
|
|
67
|
+
"assigned to, independently of the source service which is returning the check.")
|
|
68
|
+
package: str = Field(description="The package this check is assigned to. It may be your own service"
|
|
69
|
+
"but can also be any other hardware or software service.")
|
|
70
|
+
state: HealthCheckState = Field(default=HealthCheckState.no_data, description="The state of this check")
|
|
71
|
+
name: str = Field(description="Human readable name of the check. ")
|
|
72
|
+
description: str | None = Field(description="Human-readable description of the check. The same description"
|
|
73
|
+
"will be displayed when the check fails or succeeds. Consider this"
|
|
74
|
+
"when choosing the wording.")
|
|
75
|
+
message: str | None = Field(default=None,
|
|
76
76
|
description="A human readable text which is displayed next to the check in the "
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
source: str | None = Field(None,
|
|
77
|
+
"user interface. Can be used to convey more information. Please "
|
|
78
|
+
"note that no efforts are taken to notify the user about changes "
|
|
79
|
+
"in this value (i.e. no new e-mails are sent if the message "
|
|
80
|
+
"changes).")
|
|
81
|
+
source: str | None = Field(default=None,
|
|
82
82
|
description="The service which reported the health check. This value is "
|
|
83
|
-
|
|
83
|
+
"automatically set by health service and must not be set manually.")
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
class HealthPongMessage(ZMQMessage):
|
|
@@ -262,6 +262,9 @@ class Healthz:
|
|
|
262
262
|
metric.values[self._labels_string(labels)] = value
|
|
263
263
|
|
|
264
264
|
def process_message(self, _: HealthPingMessage):
|
|
265
|
+
"""
|
|
266
|
+
Process a health ping message and respond with the current health status.
|
|
267
|
+
"""
|
|
265
268
|
checks = list(self.checks.values())
|
|
266
269
|
status = "ok"
|
|
267
270
|
message = None
|
|
@@ -4,6 +4,7 @@ import random
|
|
|
4
4
|
import string
|
|
5
5
|
import time
|
|
6
6
|
from contextlib import contextmanager
|
|
7
|
+
from typing import Generator
|
|
7
8
|
|
|
8
9
|
import orjson
|
|
9
10
|
import zmq
|
|
@@ -42,7 +43,10 @@ def wait_for_connection(dispatcher: MessageDispatcher, pub_socket=None, pub_lock
|
|
|
42
43
|
"""
|
|
43
44
|
waits until the pub_socket's messages can be received at the dispatcher.
|
|
44
45
|
This dispatcher needs to run in a separate thread as this method is blocking
|
|
46
|
+
:param dispatcher: message dispatcher
|
|
47
|
+
:param pub_socket: socket for message publishing (defaults to the core's pub socket, if None)
|
|
45
48
|
:param pub_lock: optionally locks the lock during publishing
|
|
49
|
+
:param timeout: timeout in sec
|
|
46
50
|
"""
|
|
47
51
|
|
|
48
52
|
if pub_socket is None:
|
|
@@ -56,13 +60,14 @@ def wait_for_connection(dispatcher: MessageDispatcher, pub_socket=None, pub_lock
|
|
|
56
60
|
received = False
|
|
57
61
|
|
|
58
62
|
def handler(_):
|
|
63
|
+
""" handler to set received true"""
|
|
59
64
|
nonlocal received
|
|
60
65
|
received = True
|
|
61
66
|
|
|
62
67
|
# topic needs to start with __ping because these messages are ignored by the deeplog
|
|
63
68
|
random_topic = "__ping_" + "".join(random.choices(string.ascii_letters, k=10))
|
|
64
69
|
|
|
65
|
-
listener = dispatcher.register_handler(random_topic, ZMQMessage, handler,
|
|
70
|
+
listener = dispatcher.register_handler(random_topic, ZMQMessage, handler, match_type=False)
|
|
66
71
|
|
|
67
72
|
check_delay = 0.1
|
|
68
73
|
check_iterations = int(timeout / check_delay) + 1
|
|
@@ -82,7 +87,7 @@ def wait_for_connection(dispatcher: MessageDispatcher, pub_socket=None, pub_lock
|
|
|
82
87
|
|
|
83
88
|
|
|
84
89
|
@contextmanager
|
|
85
|
-
def receive_message_from_socket(sub_socket: zmq.Socket) -> tuple[str | None, dict | None]:
|
|
90
|
+
def receive_message_from_socket(sub_socket: zmq.Socket) -> Generator[tuple[str | None, dict | None], None, None]:
|
|
86
91
|
"""
|
|
87
92
|
:param sub_socket: zmq socket to receive
|
|
88
93
|
Enters a sentry trace for the received message. For this reason, the function should be used as a context manager.
|
|
@@ -50,24 +50,24 @@ class AccessCheckRejectReason(StrEnum): # see DEV-A-916
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
class AccessCheckAccess(OpenModuleModel):
|
|
53
|
-
id: str
|
|
54
|
-
group_id: str | None = None
|
|
55
|
-
group_limit: int | None = Field(None, ge=1) # group_limit must be greater or equal 1
|
|
56
|
-
customer_id: str | None = None
|
|
57
|
-
car_id: str | None = None
|
|
58
|
-
source: str
|
|
59
|
-
access_infos: dict = {}
|
|
60
|
-
parksettings_id: str | None = None
|
|
61
|
-
clearing: str | None = None
|
|
62
|
-
clearing_infos: dict | None = None
|
|
63
|
-
category: AccessCategory
|
|
64
|
-
used_medium: Medium
|
|
65
|
-
access_data: dict
|
|
66
|
-
valid_from: Datetime | None = None
|
|
67
|
-
valid_to: Datetime | None = None
|
|
68
|
-
|
|
69
|
-
accepted: bool
|
|
70
|
-
reject_reason: AccessCheckRejectReason | None = None
|
|
53
|
+
id: str # id of the access
|
|
54
|
+
group_id: str | None = None # id for group-passback, in Digimon Contract ID, optional for 3rd Party Backends
|
|
55
|
+
group_limit: int | None = Field(default=None, ge=1) # group_limit must be greater or equal 1
|
|
56
|
+
customer_id: str | None = None # in Digimon Customer ID, optional for 3rd Party Backends
|
|
57
|
+
car_id: str | None = None # in Digimon Customer Car ID, optional for 3rd Party Backends
|
|
58
|
+
source: str # Access Service providing the access
|
|
59
|
+
access_infos: dict = {} # Infos, which are added everywhere for later usage
|
|
60
|
+
parksettings_id: str | None = None # None is no cost entries and access at any gate
|
|
61
|
+
clearing: str | None = None # Clearing
|
|
62
|
+
clearing_infos: dict | None = None # Infos, which are added everywhere for later usage when using clearing
|
|
63
|
+
category: AccessCategory # category used for sorting and eventlog
|
|
64
|
+
used_medium: Medium # medium of the access
|
|
65
|
+
access_data: dict # complete access data, will be used only for display and debug purposes
|
|
66
|
+
valid_from: Datetime | None = None # access is valid from this time. Only used for reservations
|
|
67
|
+
valid_to: Datetime | None = None # access is valid until this time. Only used for reservations
|
|
68
|
+
|
|
69
|
+
accepted: bool # if access service decided access can enter
|
|
70
|
+
reject_reason: AccessCheckRejectReason | None = None # only if not accepted: reason for not accepted
|
|
71
71
|
# additional infos shown in events if reject reason is "custom". Required if reject_reason == "custom"
|
|
72
72
|
supplementary_infos: str | None = None
|
|
73
73
|
|
|
@@ -5,13 +5,13 @@ from datetime import datetime
|
|
|
5
5
|
from decimal import Decimal
|
|
6
6
|
from enum import StrEnum
|
|
7
7
|
from json.encoder import ESCAPE_ASCII
|
|
8
|
-
from typing import Annotated
|
|
8
|
+
from typing import Annotated, Optional, Any, Self
|
|
9
9
|
|
|
10
10
|
import orjson
|
|
11
11
|
from pydantic_core import PydanticUndefined
|
|
12
12
|
import zmq
|
|
13
13
|
from dateutil.tz import UTC
|
|
14
|
-
from pydantic import AfterValidator, ConfigDict, Field, BaseModel, RootModel
|
|
14
|
+
from pydantic import AfterValidator, ConfigDict, Field, BaseModel, PrivateAttr, RootModel, model_validator, field_validator
|
|
15
15
|
|
|
16
16
|
from openmodule import sentry
|
|
17
17
|
from openmodule.config import run_checks, settings
|
|
@@ -158,11 +158,18 @@ def datetime_to_timestamp(dt: datetime):
|
|
|
158
158
|
|
|
159
159
|
|
|
160
160
|
class ZMQMessage(OpenModuleModel):
|
|
161
|
+
_ALLOW_CUSTOM_TIMESTAMP: bool = PrivateAttr(default=False) # set to true for tools scripts, where you need to set the timestamp manually
|
|
162
|
+
_using_default_timestamp: bool = PrivateAttr()
|
|
163
|
+
|
|
161
164
|
timestamp: Datetime = Field(default_factory=lambda: utcnow())
|
|
162
165
|
name: str = Field(default_factory=lambda: settings.NAME)
|
|
163
166
|
type: str
|
|
164
167
|
baggage: str | None = None
|
|
165
|
-
sentry_trace: str | None = Field(
|
|
168
|
+
sentry_trace: str | None = Field(
|
|
169
|
+
default=None,
|
|
170
|
+
validation_alias="sentry-trace",
|
|
171
|
+
serialization_alias="sentry-trace",
|
|
172
|
+
)
|
|
166
173
|
|
|
167
174
|
def publish_on_topic(self, pub_socket: zmq.Socket, topic: str):
|
|
168
175
|
assert isinstance(topic, str), "topic must be a string"
|
|
@@ -175,6 +182,28 @@ class ZMQMessage(OpenModuleModel):
|
|
|
175
182
|
data["timestamp"] = datetime_to_timestamp(data["timestamp"])
|
|
176
183
|
return data
|
|
177
184
|
|
|
185
|
+
@classmethod
|
|
186
|
+
def model_validate(cls, obj: Any, *, context: Optional[Any] = None, **kwargs) -> Self:
|
|
187
|
+
context = {**(context or {}), "from_model_validate": True}
|
|
188
|
+
return super().model_validate(obj, context=context, **kwargs)
|
|
189
|
+
|
|
190
|
+
@model_validator(mode="before")
|
|
191
|
+
@classmethod
|
|
192
|
+
def check_default_timestamp(cls, data: dict):
|
|
193
|
+
cls._using_default_timestamp = "timestamp" not in data
|
|
194
|
+
return data
|
|
195
|
+
|
|
196
|
+
@field_validator("timestamp", mode="before")
|
|
197
|
+
@classmethod
|
|
198
|
+
def check_custom_timestamp(cls, v, info):
|
|
199
|
+
from_model_validate = (info.context or {}).get("from_model_validate", False)
|
|
200
|
+
using_default_timestamp = cls._using_default_timestamp
|
|
201
|
+
custom_constructor_timestamp_allowed = cls._ALLOW_CUSTOM_TIMESTAMP
|
|
202
|
+
|
|
203
|
+
if not using_default_timestamp and not custom_constructor_timestamp_allowed and not from_model_validate:
|
|
204
|
+
raise ValueError("timestamp must not be set manually")
|
|
205
|
+
return v
|
|
206
|
+
|
|
178
207
|
|
|
179
208
|
class Direction(StrEnum):
|
|
180
209
|
UNKNOWN = ""
|
|
@@ -16,25 +16,23 @@ class PresenceMedia(OpenModuleModel):
|
|
|
16
16
|
class PresenceBaseData(OpenModuleModel):
|
|
17
17
|
vehicle_id: int
|
|
18
18
|
source: str
|
|
19
|
-
present_area_name: str = Field(
|
|
19
|
+
present_area_name: str = Field(
|
|
20
|
+
validation_alias="present-area-name",
|
|
21
|
+
serialization_alias="present-area-name",
|
|
22
|
+
)
|
|
20
23
|
last_update: Datetime
|
|
21
24
|
gateway: Gateway
|
|
22
25
|
medium: PresenceMedia
|
|
23
26
|
make_model: MakeModel | None = None
|
|
24
27
|
all_ids: PresenceAllIds
|
|
25
28
|
enter_direction: EnterDirection = EnterDirection.unknown
|
|
26
|
-
enter_time: Datetime
|
|
29
|
+
enter_time: Datetime | None = None
|
|
27
30
|
|
|
28
31
|
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"""for backward compatibility, set enter_time based on vehicle_id"""
|
|
34
|
-
if values_dict.get("enter_time") is None:
|
|
35
|
-
utc_datetime = datetime.datetime.fromtimestamp(values_dict["vehicle_id"] / 1e9, datetime.timezone.utc)
|
|
36
|
-
values_dict["enter_time"] = utc_datetime # timestamp from vehicle_id
|
|
37
|
-
return values_dict
|
|
33
|
+
def model_post_init(self, *_) -> None:
|
|
34
|
+
if self.enter_time is None:
|
|
35
|
+
self.enter_time = datetime.datetime.fromtimestamp(self.vehicle_id / 1e9, datetime.timezone.utc)
|
|
38
36
|
|
|
39
37
|
|
|
40
38
|
class PresenceBaseMessage(PresenceBaseData, ZMQMessage):
|
|
@@ -44,21 +42,34 @@ class PresenceBaseMessage(PresenceBaseData, ZMQMessage):
|
|
|
44
42
|
class PresenceBackwardMessage(PresenceBaseMessage):
|
|
45
43
|
type: str = "backward"
|
|
46
44
|
unsure: bool = False
|
|
47
|
-
leave_time: Datetime = Field(
|
|
45
|
+
leave_time: Datetime = Field(
|
|
46
|
+
validation_alias="leave-time",
|
|
47
|
+
serialization_alias="leave-time",
|
|
48
|
+
)
|
|
48
49
|
bidirectional_inverse: bool = False
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
class PresenceForwardMessage(PresenceBaseMessage):
|
|
52
53
|
type: str = "forward"
|
|
53
54
|
unsure: bool = False
|
|
54
|
-
leave_time: Datetime = Field(
|
|
55
|
+
leave_time: Datetime = Field(
|
|
56
|
+
validation_alias="leave-time",
|
|
57
|
+
serialization_alias="leave-time",
|
|
58
|
+
)
|
|
55
59
|
bidirectional_inverse: bool = False
|
|
56
60
|
|
|
57
61
|
|
|
58
62
|
class PresenceLeaveMessage(PresenceBaseMessage):
|
|
59
63
|
type: str = "leave"
|
|
60
|
-
num_presents: int = Field(
|
|
61
|
-
|
|
64
|
+
num_presents: int = Field(
|
|
65
|
+
default=0,
|
|
66
|
+
validation_alias="num-presents",
|
|
67
|
+
serialization_alias="num-presents",
|
|
68
|
+
)
|
|
69
|
+
leave_time: Datetime = Field(
|
|
70
|
+
validation_alias="leave-time",
|
|
71
|
+
serialization_alias="leave-time",
|
|
72
|
+
)
|
|
62
73
|
|
|
63
74
|
@model_validator(mode="before")
|
|
64
75
|
@classmethod
|
|
@@ -2,10 +2,10 @@ import logging
|
|
|
2
2
|
import threading
|
|
3
3
|
import warnings
|
|
4
4
|
from collections import namedtuple
|
|
5
|
-
from typing import Callable, TypeVar
|
|
5
|
+
from typing import Callable, TypeVar, Any
|
|
6
6
|
|
|
7
7
|
import zmq
|
|
8
|
-
from pydantic import ValidationError
|
|
8
|
+
from pydantic import ValidationError
|
|
9
9
|
from sentry_sdk.utils import qualname_from_function
|
|
10
10
|
|
|
11
11
|
from openmodule import sentry
|
|
@@ -16,14 +16,22 @@ from openmodule.messaging import get_sub_socket, receive_message_from_socket
|
|
|
16
16
|
from openmodule.models.rpc import RPCResponse, RPCRequest, RPCServerError
|
|
17
17
|
from openmodule.rpc.common import channel_to_request_topic, channel_to_response_topic
|
|
18
18
|
from openmodule.threading import get_thread_wrapper
|
|
19
|
-
from openmodule.utils.schema import Schema
|
|
20
19
|
|
|
21
20
|
CallbackEntry = namedtuple("CallbackEntry", ["timestamp", "result"])
|
|
22
21
|
HandlerEntry = namedtuple("HandlerEntry", ["request_class", "response_class", "handler"])
|
|
23
22
|
|
|
23
|
+
FilterFunction = Callable[[Any, Any, Any], bool]
|
|
24
|
+
"""
|
|
25
|
+
Each filter function must have 3 parameters:
|
|
26
|
+
- request: OpenModuleModel | None
|
|
27
|
+
- message: RPCRequest | None
|
|
28
|
+
- handler: Callable[[OpenModuleModel, dict[str, Any]], OpenModuleModel | dict[str, Any]] | None
|
|
29
|
+
(NOTE: Use 'None' if the parameter is ignored.)
|
|
30
|
+
"""
|
|
31
|
+
|
|
24
32
|
|
|
25
33
|
def gateway_filter(gate=None, direction=None):
|
|
26
|
-
def _filter(request,
|
|
34
|
+
def _filter(request, _message, _handler):
|
|
27
35
|
gateway = request.get("gateway")
|
|
28
36
|
if not gateway:
|
|
29
37
|
return False
|
|
@@ -34,14 +42,14 @@ def gateway_filter(gate=None, direction=None):
|
|
|
34
42
|
|
|
35
43
|
|
|
36
44
|
class Filter:
|
|
37
|
-
def __init__(self, filter, channel, type):
|
|
45
|
+
def __init__(self, filter: FilterFunction, channel, type):
|
|
38
46
|
self.filter = filter
|
|
39
47
|
self.channel = channel
|
|
40
48
|
self.type = type
|
|
41
49
|
|
|
42
50
|
def check(self, request, message, handler, channel):
|
|
43
51
|
if (self.channel is None or channel == self.channel) and (self.type is None or self.type == message.type):
|
|
44
|
-
if self.filter(request
|
|
52
|
+
if self.filter(request, message, handler):
|
|
45
53
|
return True
|
|
46
54
|
return False
|
|
47
55
|
# filter does not apply
|
|
@@ -51,7 +59,6 @@ class Filter:
|
|
|
51
59
|
RequestType = TypeVar("RequestType")
|
|
52
60
|
ResponseType = TypeVar("ResponseType")
|
|
53
61
|
|
|
54
|
-
|
|
55
62
|
class RPCServer(object):
|
|
56
63
|
def __init__(self, context, config=None, *, filter_resource=True, executor=None):
|
|
57
64
|
"""
|
|
@@ -89,12 +96,12 @@ class RPCServer(object):
|
|
|
89
96
|
"This is ok for debugging / testing containers, but must not happen in production."
|
|
90
97
|
)
|
|
91
98
|
|
|
92
|
-
def add_filter(self, filter:
|
|
99
|
+
def add_filter(self, filter: FilterFunction, channel=None, type=None):
|
|
93
100
|
"""
|
|
94
|
-
:param filter: filter function with parameters (
|
|
95
|
-
request: OpenModuleModel
|
|
96
|
-
message: RPCRequest
|
|
97
|
-
handler: Callable[[OpenModuleModel, dict[str, Any]], OpenModuleModel | dict[str, Any]]
|
|
101
|
+
:param filter: filter function with parameters (arguments in order, use 'None' if ignored):
|
|
102
|
+
- request: OpenModuleModel | None
|
|
103
|
+
- message: RPCRequest | None
|
|
104
|
+
- handler: Callable[[OpenModuleModel, dict[str, Any]], OpenModuleModel | dict[str, Any]] | None
|
|
98
105
|
:param channel: specific channel to filter or all channels if None
|
|
99
106
|
:param type: specific type to filter or all types if None
|
|
100
107
|
"""
|
|
@@ -111,8 +118,7 @@ class RPCServer(object):
|
|
|
111
118
|
def register_handler(self, channel: str, type: str,
|
|
112
119
|
request_class: type[RequestType],
|
|
113
120
|
response_class: type[ResponseType],
|
|
114
|
-
handler: Callable[[RequestType,
|
|
115
|
-
register_schema=True):
|
|
121
|
+
handler: Callable[[RequestType, RPCRequest], ResponseType | dict | None]):
|
|
116
122
|
"""
|
|
117
123
|
:param channel: rpc channel you want to subscribe to, patterns/wildcards are not supported
|
|
118
124
|
:param type: the request type
|
|
@@ -122,7 +128,6 @@ class RPCServer(object):
|
|
|
122
128
|
if the handler returns a dict which contains the key "status",
|
|
123
129
|
then the value of "status" will be used as the rpc response status.
|
|
124
130
|
There are no other reserved keys except "status".
|
|
125
|
-
:param register_schema: register the schema and models of this rpc during testing to create an automatic doc
|
|
126
131
|
"""
|
|
127
132
|
if (channel, type) in self.handlers:
|
|
128
133
|
raise ValueError(f"handler for {channel}:{type} is already registered")
|
|
@@ -132,17 +137,18 @@ class RPCServer(object):
|
|
|
132
137
|
traced_handler = sentry.trace(f"rpc_handler.{qualname_from_function(handler)}")(handler)
|
|
133
138
|
self.handlers[(channel, type)] = HandlerEntry(request_class, response_class, traced_handler)
|
|
134
139
|
self.registered_channels = set(x[0] for x in self.handlers.keys())
|
|
135
|
-
if
|
|
136
|
-
|
|
140
|
+
if settings.TESTING and hasattr(handler, "__global__") and "/tests/" not in handler.__globals__['__file__']:
|
|
141
|
+
# only doc string necessary for functions or lambdas (so not for mocks) outside of tests folder
|
|
142
|
+
assert handler.__doc__, f"You need to describe the RPC handler {handler} with a doc string"
|
|
137
143
|
|
|
138
144
|
def _channel_from_topic(self, topic: str) -> str:
|
|
139
145
|
return topic.split("-", 2)[-1]
|
|
140
146
|
|
|
141
|
-
def should_process_message(self,
|
|
147
|
+
def should_process_message(self, *args):
|
|
142
148
|
if self.filters:
|
|
143
149
|
result = True
|
|
144
150
|
for filter in self.filters:
|
|
145
|
-
ok = filter.check(
|
|
151
|
+
ok = filter.check(*args)
|
|
146
152
|
if ok:
|
|
147
153
|
return True
|
|
148
154
|
# filter applies and is not ok
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
from pydantic import field_validator, Field
|
|
2
|
+
from pydantic import TypeAdapter
|
|
2
3
|
|
|
3
4
|
from openmodule.models.base import OpenModuleModel
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class Replacement(OpenModuleModel):
|
|
7
|
-
c_from: str = Field(
|
|
8
|
-
|
|
8
|
+
c_from: str = Field(
|
|
9
|
+
validation_alias="from",
|
|
10
|
+
serialization_alias="from",
|
|
11
|
+
)
|
|
12
|
+
c_to: str = Field(
|
|
13
|
+
validation_alias="to",
|
|
14
|
+
serialization_alias="to",
|
|
15
|
+
)
|
|
9
16
|
|
|
10
17
|
@field_validator("c_from")
|
|
11
18
|
@classmethod
|
|
@@ -48,7 +55,9 @@ class CharsetConverter:
|
|
|
48
55
|
def _build_charset(allowed: str, replacements: list | tuple) -> Charset:
|
|
49
56
|
return Charset(
|
|
50
57
|
allowed=allowed,
|
|
51
|
-
replacements=(
|
|
58
|
+
replacements=TypeAdapter(list[Replacement]).validate_python(
|
|
59
|
+
{"from": f, "to": t} for f, t in replacements
|
|
60
|
+
)
|
|
52
61
|
)
|
|
53
62
|
|
|
54
63
|
|