holado 0.4.2__py3-none-any.whl → 0.5.3__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.
Potentially problematic release.
This version of holado might be problematic. Click here for more details.
- holado/common/context/session_context.py +6 -0
- {holado-0.4.2.dist-info → holado-0.5.3.dist-info}/METADATA +2 -2
- {holado-0.4.2.dist-info → holado-0.5.3.dist-info}/RECORD +57 -28
- holado_ais/ais/ais_messages.py +44 -15
- holado_ais/ais/patch_pyais.py +47 -176
- holado_ais/tests/behave/steps/ais/ais_messages_steps.py +2 -6
- holado_core/common/resource/persisted_data_manager.py +4 -5
- holado_core/common/resource/resource_manager.py +4 -18
- holado_core/common/tools/converters/converter.py +14 -3
- holado_core/common/tools/tools.py +9 -2
- holado_data/data/generator/generator_manager.py +1 -13
- holado_db/tools/db/clients/base/db_audit.py +94 -0
- holado_db/tools/db/clients/base/db_client.py +145 -59
- holado_db/tools/db/clients/postgresql/postgresql_audit.py +75 -0
- holado_db/tools/db/clients/postgresql/postgresql_client.py +4 -0
- holado_db/tools/db/clients/sqlite/sqlite_audit.py +70 -0
- holado_db/tools/db/clients/sqlite/sqlite_client.py +4 -0
- holado_django/server/patch_djangogrpcframework.py +46 -0
- holado_logging/common/logging/holado_logger.py +1 -6
- holado_multitask/multithreading/thread.py +13 -7
- holado_multitask/multithreading/timer.py +3 -0
- holado_python/common/tools/datetime.py +31 -13
- holado_python/standard_library/ssl/resources/certificates/tcpbin.crt +16 -16
- holado_python/standard_library/ssl/resources/certificates/tcpbin.key +26 -26
- holado_rabbitmq/tools/rabbitmq/rabbitmq_blocking_client.py +24 -2
- holado_rabbitmq/tools/rabbitmq/rabbitmq_client.py +2 -2
- holado_report/report/builders/failure_report_builder.py +129 -0
- holado_report/report/report_manager.py +4 -0
- holado_rest/api/rest/rest_client.py +19 -7
- holado_rest/tests/behave/steps/api/rest_client_steps.py +1 -2
- test_holado/features/NonReg/holado_ais/message_types/type-10.feature +38 -0
- test_holado/features/NonReg/holado_ais/message_types/type-12.feature +37 -0
- test_holado/features/NonReg/holado_ais/message_types/type-14.feature +36 -0
- test_holado/features/NonReg/holado_ais/message_types/type-15.feature +36 -0
- test_holado/features/NonReg/holado_ais/message_types/type-16.feature +38 -0
- test_holado/features/NonReg/holado_ais/message_types/type-17.feature +46 -0
- test_holado/features/NonReg/holado_ais/message_types/type-18.feature +37 -0
- test_holado/features/NonReg/holado_ais/message_types/type-19.feature +38 -0
- test_holado/features/NonReg/holado_ais/message_types/type-1_2_3.feature +42 -0
- test_holado/features/NonReg/holado_ais/message_types/type-20.feature +38 -0
- test_holado/features/NonReg/holado_ais/message_types/type-21.feature +37 -0
- test_holado/features/NonReg/holado_ais/message_types/type-22.feature +84 -0
- test_holado/features/NonReg/holado_ais/message_types/type-23.feature +49 -0
- test_holado/features/NonReg/holado_ais/message_types/type-24.feature +72 -0
- test_holado/features/NonReg/holado_ais/message_types/type-25.feature +143 -0
- test_holado/features/NonReg/holado_ais/message_types/type-26.feature +144 -0
- test_holado/features/NonReg/holado_ais/message_types/type-27.feature +36 -0
- test_holado/features/NonReg/holado_ais/message_types/type-4_11.feature +39 -0
- test_holado/features/NonReg/holado_ais/message_types/type-5.feature +33 -0
- test_holado/features/NonReg/holado_ais/message_types/type-6.feature +37 -0
- test_holado/features/NonReg/holado_ais/message_types/type-7_13.feature +43 -0
- test_holado/features/NonReg/holado_ais/message_types/type-8.feature +37 -0
- test_holado/features/NonReg/holado_ais/message_types/type-9.feature +37 -0
- test_holado/tools/django/api_grpc/manage.py +2 -0
- test_holado/tools/django/api_grpc/patch_djangogrpcframework.py +42 -0
- {holado-0.4.2.dist-info → holado-0.5.3.dist-info}/WHEEL +0 -0
- {holado-0.4.2.dist-info → holado-0.5.3.dist-info}/licenses/LICENSE +0 -0
holado_ais/ais/patch_pyais.py
CHANGED
|
@@ -25,112 +25,23 @@ from bitarray import bitarray
|
|
|
25
25
|
import attr
|
|
26
26
|
import typing
|
|
27
27
|
import pyais
|
|
28
|
-
from pyais.messages import Payload, bit_field, from_mmsi, NMEA_VALUE, CommunicationStateMixin,
|
|
29
|
-
from_lat_lon, to_lat_lon, from_10th, to_10th, from_lat_lon_600, to_lat_lon_600
|
|
30
|
-
|
|
28
|
+
from pyais.messages import Payload, bit_field, from_mmsi, NMEA_VALUE, CommunicationStateMixin, from_speed, to_speed,\
|
|
29
|
+
from_lat_lon, to_lat_lon, from_10th, to_10th, from_lat_lon_600, to_lat_lon_600,\
|
|
30
|
+
from_turn, to_turn, ANY_MESSAGE
|
|
31
|
+
from pyais.util import get_int, from_bytes_signed, from_bytes, bits2bytes, chunks, str_to_bin, bytes2bits
|
|
31
32
|
from pyais.exceptions import InvalidDataTypeException
|
|
32
|
-
from pyais.
|
|
33
|
-
|
|
34
|
-
from pyais.constants import NavigationStatus, TurnRate, ManeuverIndicator, EpfdType, ShipType, NavAid, StationType, TransmitMode, StationIntervals
|
|
33
|
+
from pyais.constants import NavigationStatus, EpfdType, ShipType, NavAid, StationType, TransmitMode, StationIntervals,\
|
|
34
|
+
TurnRate, ManeuverIndicator
|
|
35
35
|
import functools
|
|
36
36
|
|
|
37
37
|
logger = logging.getLogger(__name__)
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
# Note: parameter seq_id is just added in some methods of pyais.encode module
|
|
43
|
-
|
|
44
|
-
def HA_ais_to_nmea_0183(payload: str, ais_talker_id: str, radio_channel: str, fill_bits: int, seq_id: str = None) -> AIS_SENTENCES:
|
|
45
|
-
"""
|
|
46
|
-
Splits the AIS payload into sentences, ASCII encodes the payload, creates
|
|
47
|
-
and sends the relevant NMEA 0183 sentences. Messages have a maximum length
|
|
48
|
-
of 82 characters, including the $ or ! starting character and the ending <LF>.
|
|
49
|
-
|
|
50
|
-
HINT:
|
|
51
|
-
This method takes care of splitting large payloads (larger than 60 characters)
|
|
52
|
-
into multiple sentences. With a total of 80 maximum chars excluding end of line
|
|
53
|
-
per sentence, and 20 chars head + tail in the nmea 0183 carrier protocol, 60
|
|
54
|
-
chars remain for the actual payload.
|
|
55
|
-
|
|
56
|
-
@param payload: Armored AIs payload.
|
|
57
|
-
@param ais_talker_id: AIS talker ID (AIVDO or AIVDM)
|
|
58
|
-
@param radio_channel: Radio channel (either A or B)
|
|
59
|
-
@param fill_bits: The number of fill bits requires to pad the data payload to a 6 bit boundary.
|
|
60
|
-
@return: A list of relevant AIS sentences.
|
|
61
|
-
"""
|
|
62
|
-
messages = []
|
|
63
|
-
max_len = 60
|
|
64
|
-
frag_cnt = math.ceil(len(payload) / max_len)
|
|
65
|
-
if seq_id is None:
|
|
66
|
-
seq_id = '0' if frag_cnt > 1 else ''
|
|
67
|
-
elif frag_cnt == 1:
|
|
68
|
-
seq_id = ''
|
|
69
|
-
|
|
70
|
-
if len(ais_talker_id) != 5:
|
|
71
|
-
raise ValueError("AIS talker is must have exactly 6 characters. E.g. AIVDO")
|
|
72
|
-
|
|
73
|
-
if len(radio_channel) != 1:
|
|
74
|
-
raise ValueError("Radio channel must be a single character")
|
|
75
|
-
|
|
76
|
-
for frag_num, chunk in enumerate(chunks(payload, max_len), start=1):
|
|
77
|
-
tpl = "!{},{},{},{},{},{},{}*{:02X}"
|
|
78
|
-
fill_bits_frag = fill_bits if frag_num == frag_cnt else 0 # Make sure we set fill bits only for last fragment
|
|
79
|
-
dummy_message = tpl.format(ais_talker_id, frag_cnt, frag_num, seq_id, radio_channel, chunk, fill_bits_frag, 0)
|
|
80
|
-
checksum = compute_checksum(dummy_message)
|
|
81
|
-
msg = tpl.format(ais_talker_id, frag_cnt, frag_num, seq_id, radio_channel, chunk, fill_bits_frag, checksum)
|
|
82
|
-
messages.append(msg)
|
|
83
|
-
|
|
84
|
-
return messages
|
|
85
|
-
|
|
86
|
-
pyais.ais_to_nmea_0183 = HA_ais_to_nmea_0183
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def HA_encode_dict(data: DATA_DICT, talker_id: str = "AIVDO", radio_channel: str = "A", seq_id: str = None) -> AIS_SENTENCES:
|
|
90
|
-
"""
|
|
91
|
-
Takes a dictionary of data and some NMEA specific kwargs and returns the NMEA 0183 encoded AIS sentence.
|
|
92
|
-
|
|
93
|
-
Notes:
|
|
94
|
-
- the data dict should also contain the AIS message type (1-27) under the `type` key.
|
|
95
|
-
- different messages take different keywords. Refer to the payload classes above to get a glimpse
|
|
96
|
-
on what fields each AIS message can take.
|
|
97
|
-
|
|
98
|
-
@param data: The AIS data as a dictionary.
|
|
99
|
-
@param talker_id: AIS packets have the introducer "AIVDM" or "AIVDO";
|
|
100
|
-
AIVDM packets are reports from other ships and AIVDO packets are reports from your own ship.
|
|
101
|
-
@param radio_channel: The radio channel. Can be either 'A' (default) or 'B'.
|
|
102
|
-
@return: NMEA 0183 encoded AIS sentences.
|
|
103
|
-
|
|
104
|
-
"""
|
|
105
|
-
if talker_id not in ("AIVDM", "AIVDO"):
|
|
106
|
-
raise ValueError("talker_id must be any of ['AIVDM', 'AIVDO']")
|
|
107
|
-
|
|
108
|
-
if radio_channel not in ('A', 'B'):
|
|
109
|
-
raise ValueError("radio_channel must be any of ['A', 'B']")
|
|
110
|
-
|
|
111
|
-
ais_type = get_ais_type(data)
|
|
112
|
-
payload = data_to_payload(ais_type, data)
|
|
113
|
-
armored_payload, fill_bits = payload.encode()
|
|
114
|
-
return pyais.ais_to_nmea_0183(armored_payload, talker_id, radio_channel, fill_bits, seq_id=seq_id)
|
|
115
|
-
|
|
116
|
-
pyais.encode_dict = HA_encode_dict
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def HA_encode_msg(msg: Payload, talker_id: str = "AIVDO", radio_channel: str = "A", seq_id: str = None) -> AIS_SENTENCES:
|
|
120
|
-
if talker_id not in ("AIVDM", "AIVDO"):
|
|
121
|
-
raise ValueError("talker_id must be any of ['AIVDM', 'AIVDO']")
|
|
122
|
-
|
|
123
|
-
if radio_channel not in ('A', 'B'):
|
|
124
|
-
raise ValueError("radio_channel must be any of ['A', 'B']")
|
|
125
|
-
|
|
126
|
-
armored_payload, fill_bits = msg.encode()
|
|
127
|
-
return pyais.ais_to_nmea_0183(armored_payload, talker_id, radio_channel, fill_bits, seq_id=seq_id)
|
|
128
|
-
|
|
129
|
-
pyais.encode_msg = HA_encode_msg
|
|
130
|
-
|
|
41
|
+
### Fix conversion of message types to/from bitarray
|
|
131
42
|
|
|
132
43
|
# All @ at end of string are considered padding.
|
|
133
|
-
# Note: in pyais implementation, decode is stopped at first
|
|
44
|
+
# Note: in pyais implementation, decode is stopped at first @, which can be a regular field information, and following regular characters are skipped.
|
|
134
45
|
def HA_decode_bin_as_ascii6(bit_arr: bitarray) -> str:
|
|
135
46
|
"""
|
|
136
47
|
Decode binary data as 6 bit ASCII.
|
|
@@ -166,7 +77,7 @@ def HA_int_to_bin(val: typing.Union[int, bool], width: int, signed: bool = True)
|
|
|
166
77
|
Compared to pyais implementation, the behaviour is changed if the value is too great to fit into
|
|
167
78
|
`width` bits:
|
|
168
79
|
- in pyais, the maximum possible number that still fits is used,
|
|
169
|
-
- in this implementation,
|
|
80
|
+
- in this implementation, a ValueError is raised.
|
|
170
81
|
|
|
171
82
|
@param val: Any integer or boolean value.
|
|
172
83
|
@param width: The bit width. If less than width bits are required, leading zeros are added.
|
|
@@ -191,52 +102,11 @@ pyais.util.int_to_bin = HA_int_to_bin
|
|
|
191
102
|
pyais.messages.int_to_bin = HA_int_to_bin
|
|
192
103
|
|
|
193
104
|
|
|
194
|
-
# Add spare field management
|
|
195
|
-
|
|
196
|
-
def bit_field(width: int, d_type: typing.Type[typing.Any],
|
|
197
|
-
from_converter: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None,
|
|
198
|
-
to_converter: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None,
|
|
199
|
-
default: typing.Optional[typing.Any] = None,
|
|
200
|
-
signed: bool = False,
|
|
201
|
-
variable_length: bool = False,
|
|
202
|
-
is_spare: bool = False,
|
|
203
|
-
**kwargs: typing.Any) -> typing.Any:
|
|
204
|
-
"""Same as pyais bit_field function, but adds a metadata 'is_spare' at True.
|
|
205
|
-
Metadata 'is_spare' is used to know which fields must not be exported when converting message to dict or json.
|
|
206
|
-
"""
|
|
207
|
-
return attr.ib(
|
|
208
|
-
metadata={
|
|
209
|
-
'width': width,
|
|
210
|
-
'd_type': d_type,
|
|
211
|
-
'from_converter': from_converter,
|
|
212
|
-
'to_converter': to_converter,
|
|
213
|
-
'signed': signed,
|
|
214
|
-
'default': default,
|
|
215
|
-
'variable_length': variable_length,
|
|
216
|
-
'is_spare': is_spare
|
|
217
|
-
},
|
|
218
|
-
**kwargs
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
pyais.messages.bit_field = bit_field
|
|
222
|
-
|
|
223
|
-
|
|
224
105
|
@attr.s(slots=True)
|
|
225
106
|
class HAPayload(Payload):
|
|
226
|
-
"""Payload class with
|
|
107
|
+
"""Payload class with fix in to/from bitarray conversion.
|
|
227
108
|
"""
|
|
228
109
|
|
|
229
|
-
def asdict(self, enum_as_int: bool = False, with_spare: bool = False) -> typing.Dict[str, typing.Optional[NMEA_VALUE]]:
|
|
230
|
-
res = super().asdict(enum_as_int)
|
|
231
|
-
|
|
232
|
-
# Remove spare fields
|
|
233
|
-
if not with_spare:
|
|
234
|
-
for field in self.fields():
|
|
235
|
-
if 'is_spare' in field.metadata and field.metadata['is_spare']:
|
|
236
|
-
del res[field.name]
|
|
237
|
-
|
|
238
|
-
return res
|
|
239
|
-
|
|
240
110
|
@classmethod
|
|
241
111
|
def from_bitarray(cls, bit_arr: bitarray) -> "ANY_MESSAGE":
|
|
242
112
|
cur: int = 0
|
|
@@ -367,7 +237,7 @@ class HAPayload(Payload):
|
|
|
367
237
|
|
|
368
238
|
|
|
369
239
|
# Fix message types:
|
|
370
|
-
# -
|
|
240
|
+
# - Use fixes of Payload implemented in HAPayload, in all message types
|
|
371
241
|
# - type 21: exclude full_name property from asdict result
|
|
372
242
|
# - type 26: fix create and from_bitarray methods
|
|
373
243
|
|
|
@@ -736,6 +606,38 @@ pyais.messages.MessageType17 = HAMessageType17
|
|
|
736
606
|
pyais.messages.MSG_CLASS[17] = pyais.messages.MessageType17
|
|
737
607
|
|
|
738
608
|
|
|
609
|
+
@attr.s(slots=True)
|
|
610
|
+
class HAMessageType18(HAPayload, CommunicationStateMixin):
|
|
611
|
+
"""
|
|
612
|
+
Standard Class B CS Position Report
|
|
613
|
+
Src: https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_18_standard_class_b_cs_position_report
|
|
614
|
+
"""
|
|
615
|
+
msg_type = bit_field(6, int, default=18, signed=False)
|
|
616
|
+
repeat = bit_field(2, int, default=0, signed=False)
|
|
617
|
+
mmsi = bit_field(30, int, from_converter=from_mmsi)
|
|
618
|
+
|
|
619
|
+
reserved_1 = bit_field(8, int, default=0, signed=False)
|
|
620
|
+
speed = bit_field(10, float, from_converter=from_speed, to_converter=to_speed, default=0, signed=False)
|
|
621
|
+
accuracy = bit_field(1, bool, default=0, signed=False)
|
|
622
|
+
lon = bit_field(28, float, from_converter=from_lat_lon, to_converter=to_lat_lon, signed=True, default=0)
|
|
623
|
+
lat = bit_field(27, float, from_converter=from_lat_lon, to_converter=to_lat_lon, signed=True, default=0)
|
|
624
|
+
course = bit_field(12, float, from_converter=from_10th, to_converter=to_10th, default=0, signed=False)
|
|
625
|
+
heading = bit_field(9, int, default=0, signed=False)
|
|
626
|
+
second = bit_field(6, int, default=0, signed=False)
|
|
627
|
+
reserved_2 = bit_field(2, int, default=0, signed=False)
|
|
628
|
+
cs = bit_field(1, bool, default=0, signed=False)
|
|
629
|
+
display = bit_field(1, bool, default=0)
|
|
630
|
+
dsc = bit_field(1, bool, default=0)
|
|
631
|
+
band = bit_field(1, bool, default=0)
|
|
632
|
+
msg22 = bit_field(1, bool, default=0)
|
|
633
|
+
assigned = bit_field(1, bool, default=0)
|
|
634
|
+
raim = bit_field(1, bool, default=0)
|
|
635
|
+
radio = bit_field(20, int, default=0)
|
|
636
|
+
|
|
637
|
+
pyais.messages.MessageType18 = HAMessageType18
|
|
638
|
+
pyais.messages.MSG_CLASS[18] = pyais.messages.MessageType18
|
|
639
|
+
|
|
640
|
+
|
|
739
641
|
@attr.s(slots=True)
|
|
740
642
|
class HAMessageType19(HAPayload):
|
|
741
643
|
"""
|
|
@@ -1064,7 +966,7 @@ class HAMessageType24PartA(HAPayload):
|
|
|
1064
966
|
mmsi = bit_field(30, int, from_converter=from_mmsi)
|
|
1065
967
|
|
|
1066
968
|
# partno = bit_field(2, int, default=0, signed=False)
|
|
1067
|
-
reserved = bit_field(1, int, default=0, signed=False)
|
|
969
|
+
reserved = bit_field(1, int, default=0, signed=False, is_spare=True)
|
|
1068
970
|
partno = bit_field(1, int, default=0, signed=False)
|
|
1069
971
|
shipname = bit_field(120, str, default='')
|
|
1070
972
|
spare_1 = bit_field(8, bytes, default=b'', is_spare=True)
|
|
@@ -1078,7 +980,9 @@ class HAMessageType24PartB(HAPayload):
|
|
|
1078
980
|
repeat = bit_field(2, int, default=0, signed=False)
|
|
1079
981
|
mmsi = bit_field(30, int, from_converter=from_mmsi)
|
|
1080
982
|
|
|
1081
|
-
partno = bit_field(2, int, default=0, signed=False)
|
|
983
|
+
# partno = bit_field(2, int, default=0, signed=False)
|
|
984
|
+
reserved = bit_field(1, int, default=0, signed=False, is_spare=True)
|
|
985
|
+
partno = bit_field(1, int, default=0, signed=False)
|
|
1082
986
|
ship_type = bit_field(8, int, default=0, signed=False)
|
|
1083
987
|
vendorid = bit_field(18, str, default='', signed=False)
|
|
1084
988
|
model = bit_field(4, int, default=0, signed=False)
|
|
@@ -1266,38 +1170,5 @@ pyais.messages.MessageType27 = HAMessageType27
|
|
|
1266
1170
|
pyais.messages.MSG_CLASS[27] = pyais.messages.MessageType27
|
|
1267
1171
|
|
|
1268
1172
|
|
|
1269
|
-
# Manage tag blocks
|
|
1270
|
-
|
|
1271
|
-
class HATagBlock(TagBlock):
|
|
1272
|
-
def __init__(self, raw: bytes = None) -> None:
|
|
1273
|
-
super().__init__(raw)
|
|
1274
|
-
|
|
1275
|
-
def to_raw(self):
|
|
1276
|
-
fields = []
|
|
1277
|
-
if self._group is not None:
|
|
1278
|
-
fields.append(f"g:{self._group}")
|
|
1279
|
-
if self._source_station is not None:
|
|
1280
|
-
fields.append(f"s:{self._source_station}")
|
|
1281
|
-
if self._receiver_timestamp is not None:
|
|
1282
|
-
fields.append(f"c:{self._receiver_timestamp}")
|
|
1283
|
-
if self._destination_station is not None:
|
|
1284
|
-
fields.append(f"d:{self._destination_station}")
|
|
1285
|
-
if self._line_count is not None:
|
|
1286
|
-
fields.append(f"n:{self._line_count}")
|
|
1287
|
-
if self._relative_time is not None:
|
|
1288
|
-
fields.append(f"r:{self._relative_time}")
|
|
1289
|
-
if self._text is not None:
|
|
1290
|
-
fields.append(f"t:{self._text}")
|
|
1291
|
-
|
|
1292
|
-
payload_str = ','.join(fields)
|
|
1293
|
-
payload = payload_str.encode()
|
|
1294
|
-
|
|
1295
|
-
chk_int = checksum(payload)
|
|
1296
|
-
chk = f"{chk_int:02X}".encode()
|
|
1297
|
-
|
|
1298
|
-
return payload + ASTERISK + chk
|
|
1299
|
-
|
|
1300
|
-
pyais.messages.TagBlock = HATagBlock
|
|
1301
|
-
|
|
1302
1173
|
|
|
1303
1174
|
|
|
@@ -56,7 +56,7 @@ if AISMessages.is_available():
|
|
|
56
56
|
Encode an AIS message to NMEA format
|
|
57
57
|
"""
|
|
58
58
|
var_name = StepTools.evaluate_variable_name(var_name)
|
|
59
|
-
table = BehaveStepTools.
|
|
59
|
+
table = BehaveStepTools.get_step_table(context)
|
|
60
60
|
|
|
61
61
|
res = __get_ais_messages().encode(table)
|
|
62
62
|
|
|
@@ -69,7 +69,7 @@ if AISMessages.is_available():
|
|
|
69
69
|
"""
|
|
70
70
|
var_name = StepTools.evaluate_variable_name(var_name)
|
|
71
71
|
msg = StepTools.evaluate_scenario_parameter(message)
|
|
72
|
-
table = BehaveStepTools.
|
|
72
|
+
table = BehaveStepTools.get_step_table(context)
|
|
73
73
|
|
|
74
74
|
if table is not None:
|
|
75
75
|
kwargs = ValueTableConverter.convert_table_with_header_to_dict(table)
|
|
@@ -101,12 +101,8 @@ if AISMessages.is_available():
|
|
|
101
101
|
var_name = StepTools.evaluate_variable_name(var_name)
|
|
102
102
|
msg_type = StepTools.evaluate_scenario_parameter(ais_message_type)
|
|
103
103
|
|
|
104
|
-
if isinstance(msg_type, str):
|
|
105
|
-
msg_type = AISMessageType[msg_type].value
|
|
106
|
-
|
|
107
104
|
if hasattr(context, 'table') and context.table is not None:
|
|
108
105
|
table = BehaveStepTools.convert_step_table_2_value_table_with_header(context.table)
|
|
109
|
-
table.add_column(name='msg_type', cells_content=[msg_type])
|
|
110
106
|
else:
|
|
111
107
|
table = {}
|
|
112
108
|
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
import logging
|
|
15
15
|
from holado_core.common.exceptions.technical_exception import TechnicalException
|
|
16
16
|
from holado_core.common.tools.tools import Tools
|
|
17
|
-
from holado_core.common.tables.converters.table_converter import TableConverter
|
|
18
17
|
import copy
|
|
19
18
|
from holado.common.handlers.undefined import undefined_argument
|
|
20
19
|
|
|
@@ -26,11 +25,12 @@ class PersistedDataManager():
|
|
|
26
25
|
"""
|
|
27
26
|
Manage data persisted in a dedicated table.
|
|
28
27
|
"""
|
|
29
|
-
def __init__(self, data_name, table_name, table_sql_create, db_name="default"):
|
|
28
|
+
def __init__(self, data_name, table_name, table_sql_create, db_name="default", do_audit=False):
|
|
30
29
|
self.__data_name = data_name
|
|
31
30
|
self.__table_name = table_name
|
|
32
31
|
self.__table_sql_create = table_sql_create
|
|
33
32
|
self.__db_name = db_name
|
|
33
|
+
self.__do_audit = do_audit
|
|
34
34
|
|
|
35
35
|
self.__resource_manager = None
|
|
36
36
|
|
|
@@ -51,9 +51,8 @@ class PersistedDataManager():
|
|
|
51
51
|
self.__resource_manager.delete_data_table(self.__table_name, db_name=self.__db_name, raise_if_not_exist=True, do_commit=True)
|
|
52
52
|
|
|
53
53
|
# Create table
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
raise TechnicalException(f"Failed to create table '{self.__table_name}' in DB '{self.__db_name}'")
|
|
54
|
+
# Note: method create_data_table raise an exception if it doesn't succeed to create the table
|
|
55
|
+
self.__resource_manager.create_data_table(self.__table_name, self.__table_sql_create, db_name=self.__db_name, raise_if_exist=True, do_commit=True, do_audit=self.__do_audit)
|
|
57
56
|
|
|
58
57
|
def has_persisted_data(self, filter_data=None, filter_compare_data=None):
|
|
59
58
|
return self.__resource_manager.has_persisted_data(self.__table_name, where_data=filter_data, where_compare_data=filter_compare_data, db_name=self.__db_name)
|
|
@@ -84,24 +84,13 @@ class ResourceManager():
|
|
|
84
84
|
client = self.get_persistent_db_client(db_name)
|
|
85
85
|
return client.exist_table(table_name)
|
|
86
86
|
|
|
87
|
-
def create_data_table(self, table_name, create_sql, db_name="default", raise_if_exist=False, do_commit=True):
|
|
87
|
+
def create_data_table(self, table_name, create_sql, db_name="default", raise_if_exist=False, do_commit=True, do_audit=False):
|
|
88
88
|
client = self.get_persistent_db_client(db_name)
|
|
89
|
-
|
|
90
|
-
client.execute(create_sql, do_commit=do_commit)
|
|
91
|
-
if not client.exist_table(table_name):
|
|
92
|
-
raise TechnicalException(f"Failed to create table '{table_name}' with SQL request [{create_sql}]")
|
|
93
|
-
elif raise_if_exist:
|
|
94
|
-
raise FunctionalException(f"Table '{table_name}' already exists")
|
|
89
|
+
client.create_table(table_name, create_sql, raise_if_exist=raise_if_exist, do_commit=do_commit, do_audit=do_audit)
|
|
95
90
|
|
|
96
91
|
def delete_data_table(self, table_name, db_name="default", raise_if_not_exist=False, do_commit=True):
|
|
97
92
|
client = self.get_persistent_db_client(db_name)
|
|
98
|
-
|
|
99
|
-
sql = f"drop table {table_name};"
|
|
100
|
-
client.execute(sql, do_commit=do_commit)
|
|
101
|
-
if client.exist_table(table_name):
|
|
102
|
-
raise TechnicalException(f"Failed to delete table '{table_name}' with SQL request [{sql}]")
|
|
103
|
-
elif raise_if_not_exist:
|
|
104
|
-
raise FunctionalException(f"Table '{table_name}' doesn't exist")
|
|
93
|
+
client.drop_table(table_name, raise_if_not_exist=raise_if_not_exist, do_commit=do_commit)
|
|
105
94
|
|
|
106
95
|
def check_data_table_schema(self, table_name, create_sql, db_name="default"):
|
|
107
96
|
client = self.get_persistent_db_client(db_name)
|
|
@@ -114,10 +103,7 @@ class ResourceManager():
|
|
|
114
103
|
|
|
115
104
|
def count_persisted_data(self, table_name, where_data: dict=None, where_compare_data: list=None, db_name="default"):
|
|
116
105
|
client = self.get_persistent_db_client(db_name)
|
|
117
|
-
|
|
118
|
-
if Tools.do_log(logger, logging.DEBUG):
|
|
119
|
-
logger.debug(f"result: {Tools.represent_object(result)}")
|
|
120
|
-
return result[0][0].content
|
|
106
|
+
return client.count(table_name, where_data=where_data, where_compare_data=where_compare_data)
|
|
121
107
|
|
|
122
108
|
def has_persisted_data(self, table_name, where_data: dict=None, where_compare_data: list=None, db_name="default"):
|
|
123
109
|
count = self.count_persisted_data(table_name, where_data=where_data, where_compare_data=where_compare_data, db_name=db_name)
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
#################################################
|
|
12
12
|
|
|
13
13
|
from holado_core.common.exceptions.technical_exception import TechnicalException
|
|
14
|
+
import io
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
|
|
@@ -53,10 +54,15 @@ class Converter(object):
|
|
|
53
54
|
|
|
54
55
|
@classmethod
|
|
55
56
|
def to_list(cls, value):
|
|
56
|
-
if
|
|
57
|
+
if value is None:
|
|
58
|
+
return None
|
|
59
|
+
elif isinstance(value, list):
|
|
57
60
|
return value
|
|
58
|
-
|
|
61
|
+
elif cls.is_iterable(value):
|
|
59
62
|
return list(value)
|
|
63
|
+
else:
|
|
64
|
+
from holado_python.standard_library.typing import Typing
|
|
65
|
+
raise TechnicalException(f"Unmanaged convertion to list of object of type {Typing.get_object_class_fullname(value)}")
|
|
60
66
|
|
|
61
67
|
@classmethod
|
|
62
68
|
def is_integer(cls, value):
|
|
@@ -103,5 +109,10 @@ class Converter(object):
|
|
|
103
109
|
return False
|
|
104
110
|
else:
|
|
105
111
|
return True
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def is_file_like(cls, obj):
|
|
115
|
+
return isinstance(obj, io.TextIOBase) or isinstance(obj, io.BufferedIOBase) or isinstance(obj, io.RawIOBase) or isinstance(obj, io.IOBase)
|
|
116
|
+
|
|
117
|
+
|
|
106
118
|
|
|
107
|
-
|
|
@@ -18,6 +18,7 @@ from holado_core.common.handlers.enums import AccessType
|
|
|
18
18
|
import logging
|
|
19
19
|
import time
|
|
20
20
|
from holado_python.standard_library.typing import Typing
|
|
21
|
+
from holado.common.handlers.undefined import default_value
|
|
21
22
|
|
|
22
23
|
logger = logging.getLogger(__name__)
|
|
23
24
|
|
|
@@ -47,8 +48,14 @@ class Tools(object):
|
|
|
47
48
|
return ("\n" + ind_str).join(lines)
|
|
48
49
|
|
|
49
50
|
@classmethod
|
|
50
|
-
def truncate_text(cls, text, length
|
|
51
|
-
|
|
51
|
+
def truncate_text(cls, text, length=Config.message_truncate_length, truncated_suffix=default_value, is_length_with_suffix=False, is_suffix_with_truncated_length=True):
|
|
52
|
+
"""Truncate text if needed.
|
|
53
|
+
Default truncate suffix is '[...(xxx)]' if is_suffix_with_truncated_length=True else '[...]'
|
|
54
|
+
Note: length can be None, meaning no truncate to do
|
|
55
|
+
"""
|
|
56
|
+
if length is not None and len(text) > length:
|
|
57
|
+
if truncated_suffix is default_value:
|
|
58
|
+
truncated_suffix = f"[...({len(text)-length})]" if is_suffix_with_truncated_length else "[...]"
|
|
52
59
|
if truncated_suffix:
|
|
53
60
|
if is_length_with_suffix:
|
|
54
61
|
return text[0 : length - len(truncated_suffix)] + truncated_suffix
|
|
@@ -12,8 +12,6 @@
|
|
|
12
12
|
#################################################
|
|
13
13
|
|
|
14
14
|
import logging
|
|
15
|
-
from holado_core.common.tools.converters.converter import Converter
|
|
16
|
-
from holado_data.data.generator.base import BaseGenerator
|
|
17
15
|
|
|
18
16
|
logger = logging.getLogger(__name__)
|
|
19
17
|
|
|
@@ -22,17 +20,7 @@ class GeneratorManager(object):
|
|
|
22
20
|
"""
|
|
23
21
|
Manage generators.
|
|
24
22
|
"""
|
|
25
|
-
|
|
26
|
-
@classmethod
|
|
27
|
-
def convert_to_list(cls, generator_or_list):
|
|
28
|
-
if generator_or_list is None:
|
|
29
|
-
return None
|
|
30
|
-
elif Converter.is_list(generator_or_list):
|
|
31
|
-
return generator_or_list
|
|
32
|
-
elif isinstance(generator_or_list, Generator):
|
|
33
|
-
return Converter.to_list(generator_or_list)
|
|
34
|
-
else:
|
|
35
|
-
raise TechnicalException(f"Unexpected generator (or list) type '{Typing.get_object_class_fullname(generator_or_list)}'")
|
|
23
|
+
pass
|
|
36
24
|
|
|
37
25
|
|
|
38
26
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
|
|
2
|
+
#################################################
|
|
3
|
+
# HolAdo (Holistic Automation do)
|
|
4
|
+
#
|
|
5
|
+
# (C) Copyright 2023 by Eric Klumpp
|
|
6
|
+
#
|
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
8
|
+
#
|
|
9
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
10
|
+
|
|
11
|
+
# The Software is provided “as is”, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the Software.
|
|
12
|
+
#################################################
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from holado.common.handlers.object import Object
|
|
16
|
+
import abc
|
|
17
|
+
from holado.common.handlers.undefined import default_value, any_value
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DBAuditManager(Object):
|
|
23
|
+
"""Base class for audit managers.
|
|
24
|
+
"""
|
|
25
|
+
__metaclass__ = abc.ABCMeta
|
|
26
|
+
|
|
27
|
+
def __init__(self, name, db_client):
|
|
28
|
+
super().__init__(name)
|
|
29
|
+
|
|
30
|
+
self.__db_client = db_client
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def _db_client(self):
|
|
34
|
+
return self.__db_client
|
|
35
|
+
|
|
36
|
+
def audit_table(self, table_name, operation_types=any_value, **kwargs):
|
|
37
|
+
raise NotImplementedError()
|
|
38
|
+
|
|
39
|
+
def _get_audit_table_name(self, audit_table_name=default_value):
|
|
40
|
+
if audit_table_name is default_value:
|
|
41
|
+
return "_audit"
|
|
42
|
+
else:
|
|
43
|
+
return audit_table_name
|
|
44
|
+
|
|
45
|
+
def _get_audit_table_sql_create(self, audit_table_name=default_value):
|
|
46
|
+
raise NotImplementedError()
|
|
47
|
+
|
|
48
|
+
def ensure_audit_table_exists(self, audit_table_name=default_value):
|
|
49
|
+
audit_table_name = self._get_audit_table_name(audit_table_name)
|
|
50
|
+
create_sql = self._get_audit_table_sql_create(audit_table_name=audit_table_name)
|
|
51
|
+
self._db_client.create_table(audit_table_name, create_sql, raise_if_exist=False, do_commit=True, do_audit=False)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TriggerAuditManager(DBAuditManager):
|
|
56
|
+
def __init__(self, name, db_client):
|
|
57
|
+
super().__init__(name, db_client)
|
|
58
|
+
|
|
59
|
+
def audit_table(self, table_name, operation_types=any_value, audit_table_name=default_value, **kwargs):
|
|
60
|
+
self.ensure_audit_table_exists(audit_table_name)
|
|
61
|
+
|
|
62
|
+
if operation_types is any_value:
|
|
63
|
+
operation_types = ['insert', 'update', 'delete']
|
|
64
|
+
else:
|
|
65
|
+
operation_types = [ot.lower() for ot in operation_types]
|
|
66
|
+
|
|
67
|
+
for op_type in operation_types:
|
|
68
|
+
self.drop_trigger_of_audit_table_operation(table_name, op_type, audit_table_name, **kwargs)
|
|
69
|
+
self.create_trigger_to_audit_table_operation(table_name, op_type, audit_table_name, **kwargs)
|
|
70
|
+
|
|
71
|
+
def get_trigger_name(self, table_name, operation_type):
|
|
72
|
+
return f"audit_{table_name}_on_{operation_type}"
|
|
73
|
+
|
|
74
|
+
def drop_trigger_of_audit_table_operation(self, table_name, operation_type, audit_table_name, **kwargs):
|
|
75
|
+
drop_sql = self._get_drop_trigger_sql_of_audit_table_operation(table_name, operation_type, audit_table_name)
|
|
76
|
+
|
|
77
|
+
do_commit = kwargs.pop('do_commit', True)
|
|
78
|
+
self._db_client.execute(drop_sql, do_commit=do_commit, **kwargs)
|
|
79
|
+
|
|
80
|
+
def _get_drop_trigger_sql_of_audit_table_operation(self, table_name, operation_type):
|
|
81
|
+
raise NotImplementedError()
|
|
82
|
+
|
|
83
|
+
def create_trigger_to_audit_table_operation(self, table_name, operation_type, audit_table_name, **kwargs):
|
|
84
|
+
create_sql = self._get_create_trigger_sql_to_audit_table_operation(table_name, operation_type, audit_table_name)
|
|
85
|
+
|
|
86
|
+
do_commit = kwargs.pop('do_commit', True)
|
|
87
|
+
self._db_client.execute(create_sql, do_commit=do_commit, **kwargs)
|
|
88
|
+
|
|
89
|
+
def _get_create_trigger_sql_to_audit_table_operation(self, table_name, operation_type):
|
|
90
|
+
raise NotImplementedError()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|