holado 0.4.1__py3-none-any.whl → 0.5.2__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.

Files changed (93) hide show
  1. holado/common/context/session_context.py +6 -0
  2. holado/common/handlers/object.py +6 -0
  3. {holado-0.4.1.dist-info → holado-0.5.2.dist-info}/METADATA +2 -1
  4. {holado-0.4.1.dist-info → holado-0.5.2.dist-info}/RECORD +93 -43
  5. holado_ais/ais/ais_messages.py +44 -15
  6. holado_ais/ais/patch_pyais.py +47 -176
  7. holado_ais/tests/behave/steps/ais/ais_messages_steps.py +2 -6
  8. holado_core/common/resource/persisted_data_manager.py +4 -5
  9. holado_core/common/resource/resource_manager.py +4 -18
  10. holado_core/common/tools/converters/converter.py +14 -3
  11. holado_core/common/tools/tools.py +9 -2
  12. holado_core/tools/abstracts/blocking_command_service.py +9 -1
  13. holado_data/data/generator/generator_manager.py +1 -13
  14. holado_db/tools/db/clients/base/db_audit.py +94 -0
  15. holado_db/tools/db/clients/base/db_client.py +145 -59
  16. holado_db/tools/db/clients/postgresql/postgresql_audit.py +75 -0
  17. holado_db/tools/db/clients/postgresql/postgresql_client.py +4 -0
  18. holado_db/tools/db/clients/sqlite/sqlite_audit.py +70 -0
  19. holado_db/tools/db/clients/sqlite/sqlite_client.py +4 -0
  20. holado_django/__init__.py +31 -0
  21. holado_django/server/HOWTO.txt +27 -0
  22. holado_django/server/django_projects/rest_api/db.sqlite3 +0 -0
  23. holado_django/server/django_projects/rest_api/manage.py +22 -0
  24. holado_django/server/django_projects/rest_api/rest_api/__init__.py +0 -0
  25. holado_django/server/django_projects/rest_api/rest_api/application/__init__.py +0 -0
  26. holado_django/server/django_projects/rest_api/rest_api/application/admin.py +3 -0
  27. holado_django/server/django_projects/rest_api/rest_api/application/apps.py +9 -0
  28. holado_django/server/django_projects/rest_api/rest_api/application/migrations/__init__.py +0 -0
  29. holado_django/server/django_projects/rest_api/rest_api/application/models.py +3 -0
  30. holado_django/server/django_projects/rest_api/rest_api/application/tests.py +3 -0
  31. holado_django/server/django_projects/rest_api/rest_api/application/views.py +6 -0
  32. holado_django/server/django_projects/rest_api/rest_api/asgi.py +16 -0
  33. holado_django/server/django_projects/rest_api/rest_api/settings.py +130 -0
  34. holado_django/server/django_projects/rest_api/rest_api/urls.py +35 -0
  35. holado_django/server/django_projects/rest_api/rest_api/wsgi.py +16 -0
  36. holado_django/server/django_server.py +110 -0
  37. holado_django/server/grpc_django_server.py +57 -0
  38. holado_django/server/patch_djangogrpcframework.py +46 -0
  39. holado_django/tests/behave/steps/__init__.py +16 -0
  40. holado_django/tests/behave/steps/django_server_steps.py +83 -0
  41. holado_grpc/tests/behave/steps/private/api/grpc_steps.py +9 -9
  42. holado_logging/common/logging/holado_logger.py +1 -6
  43. holado_multitask/multitasking/multitask_manager.py +36 -10
  44. holado_multitask/multithreading/thread.py +13 -7
  45. holado_multitask/multithreading/timer.py +3 -0
  46. holado_python/common/tools/datetime.py +31 -13
  47. holado_python/standard_library/ssl/resources/certificates/tcpbin.crt +16 -16
  48. holado_python/standard_library/ssl/resources/certificates/tcpbin.key +26 -26
  49. holado_rabbitmq/tools/rabbitmq/rabbitmq_blocking_client.py +24 -2
  50. holado_rabbitmq/tools/rabbitmq/rabbitmq_client.py +2 -2
  51. holado_report/report/builders/failure_report_builder.py +129 -0
  52. holado_report/report/report_manager.py +4 -0
  53. holado_rest/api/rest/TODO.txt +1 -1
  54. holado_rest/api/rest/rest_client.py +19 -7
  55. holado_rest/tests/behave/steps/api/rest_client_steps.py +1 -2
  56. holado_rest/tests/behave/steps/private/api/rest_steps.py +11 -13
  57. holado_system/system/command/command.py +31 -9
  58. holado_system/system/global_system.py +3 -3
  59. test_holado/environment.py +6 -4
  60. test_holado/features/NonReg/api/REST.feature +7 -2
  61. test_holado/features/NonReg/api/gRPC.feature +0 -6
  62. test_holado/features/NonReg/holado_ais/message_types/type-10.feature +38 -0
  63. test_holado/features/NonReg/holado_ais/message_types/type-12.feature +37 -0
  64. test_holado/features/NonReg/holado_ais/message_types/type-14.feature +36 -0
  65. test_holado/features/NonReg/holado_ais/message_types/type-15.feature +36 -0
  66. test_holado/features/NonReg/holado_ais/message_types/type-16.feature +38 -0
  67. test_holado/features/NonReg/holado_ais/message_types/type-17.feature +46 -0
  68. test_holado/features/NonReg/holado_ais/message_types/type-18.feature +37 -0
  69. test_holado/features/NonReg/holado_ais/message_types/type-19.feature +38 -0
  70. test_holado/features/NonReg/holado_ais/message_types/type-1_2_3.feature +42 -0
  71. test_holado/features/NonReg/holado_ais/message_types/type-20.feature +38 -0
  72. test_holado/features/NonReg/holado_ais/message_types/type-21.feature +37 -0
  73. test_holado/features/NonReg/holado_ais/message_types/type-22.feature +84 -0
  74. test_holado/features/NonReg/holado_ais/message_types/type-23.feature +49 -0
  75. test_holado/features/NonReg/holado_ais/message_types/type-24.feature +72 -0
  76. test_holado/features/NonReg/holado_ais/message_types/type-25.feature +143 -0
  77. test_holado/features/NonReg/holado_ais/message_types/type-26.feature +144 -0
  78. test_holado/features/NonReg/holado_ais/message_types/type-27.feature +36 -0
  79. test_holado/features/NonReg/holado_ais/message_types/type-4_11.feature +39 -0
  80. test_holado/features/NonReg/holado_ais/message_types/type-5.feature +33 -0
  81. test_holado/features/NonReg/holado_ais/message_types/type-6.feature +37 -0
  82. test_holado/features/NonReg/holado_ais/message_types/type-7_13.feature +43 -0
  83. test_holado/features/NonReg/holado_ais/message_types/type-8.feature +37 -0
  84. test_holado/features/NonReg/holado_ais/message_types/type-9.feature +37 -0
  85. test_holado/initialize_holado.py +62 -0
  86. test_holado/logging.conf +3 -0
  87. test_holado/tools/django/api_grpc/manage.py +2 -0
  88. test_holado/tools/django/api_grpc/patch_djangogrpcframework.py +42 -0
  89. test_holado/tools/django/api_rest/db.sqlite3 +0 -0
  90. {holado-0.4.1.dist-info → holado-0.5.2.dist-info}/WHEEL +0 -0
  91. {holado-0.4.1.dist-info → holado-0.5.2.dist-info}/licenses/LICENSE +0 -0
  92. /holado_helper/holado_module_template/{test → tests}/behave/steps/__init__.py +0 -0
  93. /holado_helper/holado_module_template/{test → tests}/behave/steps/private/__init__.py +0 -0
@@ -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, TagBlock, ASTERISK, from_turn, to_turn, 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 pyais.util import get_int, from_bytes_signed, from_bytes, bits2bytes, checksum, chunks, compute_checksum, str_to_bin, bytes2bits
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.encode import AIS_SENTENCES, DATA_DICT, get_ais_type, data_to_payload
33
- import math
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
- # Add possibility to specify the seq_id in AIS sentence
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, an exception is raised.
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 spare fields management.
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
- # - almost all types with new spare management
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.convert_step_table_2_value_table_with_header(context.table)
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.convert_step_table_2_value_table_with_header(context.table)
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
- 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)
55
- if not self.__resource_manager.has_data_table(self.__table_name, db_name=self.__db_name):
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
- if not client.exist_table(table_name):
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
- if client.exist_table(table_name):
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
- result = client.select(table_name, where_data=where_data, where_compare_data=where_compare_data, sql_return="count(*)")
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 isinstance(value, list):
57
+ if value is None:
58
+ return None
59
+ elif isinstance(value, list):
57
60
  return value
58
- else:
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 = Config.message_truncate_length, truncated_suffix = "[...]", is_length_with_suffix=False):
51
- if len(text) > length:
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
@@ -30,6 +30,10 @@ class BlockingCommandService(Service):
30
30
  self.__cmd_str = cmd
31
31
  self.__command = Command(self.__cmd_str, **kwargs)
32
32
 
33
+ @property
34
+ def internal_command(self):
35
+ return self.__command
36
+
33
37
  @property
34
38
  def status(self):
35
39
  if self.__command is not None:
@@ -50,7 +54,11 @@ class BlockingCommandService(Service):
50
54
  logger.debug(f"Stopping service '{self.name}'")
51
55
  status = self.__command.state
52
56
  if status == CommandStates.Running:
53
- self.__command.terminate()
57
+ # self.__command.terminate()
58
+ self.__command.stop()
54
59
  self.__command.join()
55
60
  else:
56
61
  raise FunctionalException(f"Service '{self.name}' is not running (status: {status.name}")
62
+
63
+ def join(self, timeout=None):
64
+ self.__command.join(timeout)
@@ -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
+