vortexpy 4.0.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. vortex/DataWrapTuple.py +27 -0
  2. vortex/DeferUtil.py +224 -0
  3. vortex/Jsonable.py +304 -0
  4. vortex/Payload.py +115 -0
  5. vortex/PayloadEndpoint.py +216 -0
  6. vortex/PayloadEndpointTest.py +164 -0
  7. vortex/PayloadEnvelope.py +166 -0
  8. vortex/PayloadFilterKeys.py +15 -0
  9. vortex/PayloadIO.py +152 -0
  10. vortex/PayloadPriority.py +11 -0
  11. vortex/PayloadResponse.py +173 -0
  12. vortex/PayloadResponseTest.py +39 -0
  13. vortex/PayloadTest.py +136 -0
  14. vortex/PerformTestActionTuple.py +30 -0
  15. vortex/SerialiseUtil.py +273 -0
  16. vortex/TestTuple.py +61 -0
  17. vortex/Tuple.py +968 -0
  18. vortex/TupleAction.py +72 -0
  19. vortex/TupleActionVortex.py +5 -0
  20. vortex/TupleDataForTest.py +25 -0
  21. vortex/TuplePerfTest.py +71 -0
  22. vortex/TuplePolymorphicTupleTypeFieldArgTest.py +74 -0
  23. vortex/TupleRestfulTest.py +140 -0
  24. vortex/TupleSelector.py +105 -0
  25. vortex/TupleTest.py +127 -0
  26. vortex/VortexABC.py +55 -0
  27. vortex/VortexClientHttp.py +308 -0
  28. vortex/VortexClientHttpTest.py +71 -0
  29. vortex/VortexClientTcp.py +355 -0
  30. vortex/VortexClientWebsocketFactory.py +383 -0
  31. vortex/VortexConnectionABC.py +88 -0
  32. vortex/VortexFactory.py +932 -0
  33. vortex/VortexPayloadProtocol.py +180 -0
  34. vortex/VortexServer.py +367 -0
  35. vortex/VortexServerConnection.py +127 -0
  36. vortex/VortexServerHttpResource.py +193 -0
  37. vortex/VortexServerTcp.py +104 -0
  38. vortex/VortexServerWebsocket.py +287 -0
  39. vortex/VortexTcpMemoryLeakPayloadTest.py +611 -0
  40. vortex/VortexTcpMemoryLeakRawTest.py +87 -0
  41. vortex/VortexTest.py +22 -0
  42. vortex/VortexUtil.py +314 -0
  43. vortex/VortexWritePushProducer.py +321 -0
  44. vortex/__init__.py +10 -0
  45. vortex/data_loader/TupleDataLiveLockManager.py +383 -0
  46. vortex/data_loader/TupleDataLoader.py +326 -0
  47. vortex/data_loader/TupleDataLoaderDelegate.py +90 -0
  48. vortex/data_loader/TupleDataLoaderTupleABC.py +10 -0
  49. vortex/data_loader/TupleDataLoaderTuples.py +53 -0
  50. vortex/data_loader/__init__.py +0 -0
  51. vortex/event_bus/TupleChangeEventABC.py +15 -0
  52. vortex/event_bus/TupleChangeEventBus.py +192 -0
  53. vortex/event_bus/TupleChangeEventBusObserverABC.py +11 -0
  54. vortex/event_bus/__init__.py +0 -0
  55. vortex/handler/TupleActionProcessor.py +270 -0
  56. vortex/handler/TupleActionProcessorProxy.py +218 -0
  57. vortex/handler/TupleActionProcessorTestHandler.py +27 -0
  58. vortex/handler/TupleDataActionClient.py +67 -0
  59. vortex/handler/TupleDataObservableCache.py +201 -0
  60. vortex/handler/TupleDataObservableHandler.py +371 -0
  61. vortex/handler/TupleDataObservableProxyHandler.py +340 -0
  62. vortex/handler/TupleDataObservableTestHandler.py +60 -0
  63. vortex/handler/TupleDataObserverClient.py +158 -0
  64. vortex/handler/VortexJSTupleLoaderTestHandler.py +42 -0
  65. vortex/handler/__init__.py +0 -0
  66. vortex/restful/GzippedDataHttpClient.py +170 -0
  67. vortex/restful/GzippedDataHttpClientTest.py +103 -0
  68. vortex/restful/GzippedPayloadHttpClient.py +181 -0
  69. vortex/restful/RestfulHttpClient.py +107 -0
  70. vortex/restful/RestfulHttpClientTest.py +60 -0
  71. vortex/restful/RestfulResource.py +153 -0
  72. vortex/restful/RestfulResourceTest.py +100 -0
  73. vortex/restful/__init__.py +0 -0
  74. vortex/rpc/RPC.py +446 -0
  75. vortex/rpc/RPCTest.py +12 -0
  76. vortex/rpc/__init__.py +0 -0
  77. vortex/sqla_orm/OrmCrudHandler.py +431 -0
  78. vortex/sqla_orm/TupleProviderForDb.py +42 -0
  79. vortex/sqla_orm/VortexTupleJson.py +24 -0
  80. vortex/sqla_orm/__init__.py +0 -0
  81. vortex/storage/TupleStorageSqlite.py +359 -0
  82. vortex/storage/__init__.py +0 -0
  83. vortexpy-4.0.23.dist-info/LICENSE +21 -0
  84. vortexpy-4.0.23.dist-info/METADATA +43 -0
  85. vortexpy-4.0.23.dist-info/RECORD +87 -0
  86. vortexpy-4.0.23.dist-info/WHEEL +5 -0
  87. vortexpy-4.0.23.dist-info/top_level.txt +1 -0
vortex/Tuple.py ADDED
@@ -0,0 +1,968 @@
1
+ """
2
+ * Created by Synerty Pty Ltd
3
+ *
4
+ * This software is open source, the MIT license applies.
5
+ *
6
+ * Website : http://www.synerty.com
7
+ * Support : support@synerty.com
8
+ """
9
+
10
+ import inspect
11
+ import json
12
+ import logging
13
+ import re
14
+ from abc import abstractmethod, ABC
15
+ from copy import deepcopy
16
+ from datetime import datetime
17
+ from typing import Dict, Optional, Annotated, Any
18
+ from typing import List
19
+ from typing import Type
20
+ from typing import get_args
21
+ from typing import get_origin
22
+ from typing import get_type_hints
23
+
24
+ from sqlalchemy.orm.attributes import InstrumentedAttribute
25
+ from sqlalchemy.orm.properties import RelationshipProperty
26
+ from sqlalchemy.sql.schema import Sequence
27
+
28
+ from vortex.SerialiseUtil import ISO8601_REGEXP
29
+ from .Jsonable import Jsonable
30
+ from .SerialiseUtil import ISO8601
31
+ from .SerialiseUtil import T_RAPUI_TUPLE
32
+ from .SerialiseUtil import WKBElement
33
+ from .SerialiseUtil import convertFromWkbElement
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ TUPLE_TYPES: list[Type["Tuple"]] = []
38
+ TUPLE_TYPES_BY_NAME: Dict[str, Type["Tuple"]] = {}
39
+ TUPLE_TYPES_BY_SHORT_NAME: Dict[str, Type["Tuple"]] = {}
40
+
41
+ JSON_EXCLUDE = "jsonExclude"
42
+
43
+
44
+ def registeredTupleNames():
45
+ return list(TUPLE_TYPES_BY_NAME.keys())
46
+
47
+
48
+ def tupleForTupleName(tupleName):
49
+ return TUPLE_TYPES_BY_NAME[tupleName]
50
+
51
+
52
+ def getSqlaRelationshipFieldNames(cls):
53
+ objDict = _getTupleInternalDict(cls)
54
+
55
+ # Tell the tuple fields of their variable name
56
+ fields = []
57
+ for name, value in objDict.items():
58
+ if isinstance(value, InstrumentedAttribute) and isinstance(
59
+ value.comparator.prop, RelationshipProperty
60
+ ):
61
+ fields.append(value)
62
+
63
+ return fields
64
+
65
+
66
+ def addTupleType(cls):
67
+ if cls.tupleName() is None:
68
+ raise Exception(
69
+ "You must set __tupleType__ to a unique string for this tuple"
70
+ )
71
+
72
+ if not cls.tupleName():
73
+ cls.__tupleType__ = "%s.%s" % (cls.__module__, cls.__name__)
74
+
75
+ tupleType = cls.tupleName()
76
+ tupleTypeShort = cls.__tupleTypeShort__
77
+ if tupleType in TUPLE_TYPES_BY_NAME or tupleType is None:
78
+ raise Exception(
79
+ "Tuple name is None or is already in registered.\n"
80
+ "Tuple name is %s" % tupleType
81
+ )
82
+
83
+ if tupleTypeShort:
84
+ if tupleTypeShort in TUPLE_TYPES_BY_SHORT_NAME:
85
+ raise Exception(
86
+ "Tuple short name is already registered.\n"
87
+ "Tuple short name is %s" % tupleTypeShort
88
+ )
89
+
90
+ TUPLE_TYPES_BY_SHORT_NAME[tupleTypeShort] = cls
91
+
92
+ # Setup the lookups
93
+ TUPLE_TYPES.append(cls)
94
+ TUPLE_TYPES_BY_NAME[tupleType] = cls
95
+
96
+ def underscoreException():
97
+ raise Exception(
98
+ "TupleFields can not start with an underscore. "
99
+ "They can potentially clash with inner tuple workings"
100
+ " if they do."
101
+ )
102
+
103
+ # This is required because SQLAlchemy seems to alter the inhertance.
104
+ # OR __dict__ just doesn't contian attributes defined in the base classes
105
+ # If we inherit from a tuple that has a tuple field, that field should be included
106
+ def getBaseClassTupleFields(cls, objDict):
107
+ # Skip these types
108
+ if cls in (object, Tuple, Jsonable):
109
+ return
110
+
111
+ for name, value in cls.__dict__.items():
112
+ # SQLAlchemy takes care of it's own fields.
113
+ if isinstance(value, TupleField):
114
+ objDict[name] = value
115
+
116
+ objDict = {}
117
+ for baseCls in reversed(inspect.getmro(cls)[1:]): # The first item is us
118
+ getBaseClassTupleFields(baseCls, objDict)
119
+ objDict.update(cls.__dict__)
120
+
121
+ # Tell the tuple fields of their variable name
122
+ fields = []
123
+ shortFieldNames = set()
124
+ for name, value in objDict.items():
125
+ if isinstance(value, TupleField):
126
+ shortName = value.shortName if value.shortName else name
127
+
128
+ elif isinstance(value, InstrumentedAttribute):
129
+ if isinstance(value.comparator.prop, RelationshipProperty):
130
+ continue
131
+
132
+ shortName = value.doc if value.doc else name
133
+
134
+ else:
135
+ continue
136
+
137
+ if name.startswith("_"):
138
+ underscoreException()
139
+
140
+ if shortName.startswith("_"):
141
+ underscoreException()
142
+
143
+ if isinstance(value, InstrumentedAttribute):
144
+ assert value.name is not None, "InstrumentedAttribute.name is None"
145
+ else:
146
+ value.name = name
147
+
148
+ fields.append(name)
149
+
150
+ if shortName != JSON_EXCLUDE:
151
+ if shortName in shortFieldNames:
152
+ raise Exception(
153
+ "TupleField %s short name %s is already registered"
154
+ % (name, shortName)
155
+ )
156
+
157
+ shortFieldNames.add(shortName)
158
+
159
+ hasSlots = len(getattr(cls, "__slots__", ())) > 0
160
+ hasFieldNames = cls.__fieldNames__ is not None
161
+
162
+ if hasSlots and hasFieldNames:
163
+ raise Exception(
164
+ "Only one of __slots__ or __fieldNames__ can be defined"
165
+ " but not both"
166
+ )
167
+
168
+ # If field names already exist, then work off these
169
+ if hasFieldNames:
170
+ fields = list(set(fields + list(cls.__fieldNames__)))
171
+
172
+ # Just check that the field names are defined.
173
+ for fieldName in fields:
174
+ if not hasattr(cls, fieldName):
175
+ raise Exception(
176
+ "Tuple %s doesn't have field %s" % (tupleType, fieldName)
177
+ )
178
+
179
+ # If field names already exist, then work off these
180
+ if hasSlots:
181
+ fields = list(set(fields + list(cls.__slots__)))
182
+
183
+ # Sort and set the field names
184
+ fields.sort()
185
+ cls.__fieldNames__ = fields
186
+
187
+ __mapShortFieldNames(cls)
188
+
189
+ return cls
190
+
191
+
192
+ def _getTupleInternalDict(cls):
193
+ """Get Tuple Internal Dict
194
+
195
+ This is required because SQLAlchemy seems to alter the inhertance.
196
+ OR __dict__ just doesn't contian attributes defined in the base classes
197
+ If we inherit from a tuple that has a tuple field, that field should be included
198
+ """
199
+
200
+ def getBaseClassTupleFields(cls, objDict):
201
+ # Skip these types
202
+ if cls in (object, Tuple, Jsonable):
203
+ return
204
+
205
+ for name, value in cls.__dict__.items():
206
+ # SQLAlchemy takes care of it's own fields.
207
+ if isinstance(value, TupleField):
208
+ objDict[name] = value
209
+
210
+ objDict = {}
211
+ for baseCls in reversed(inspect.getmro(cls)[1:]): # The first item is us
212
+ getBaseClassTupleFields(baseCls, objDict)
213
+ objDict.update(cls.__dict__)
214
+ return objDict
215
+
216
+
217
+ def __mapShortFieldNames(cls):
218
+ fieldMap = {}
219
+ for normalName in cls.__fieldNames__:
220
+ field = getattr(cls, normalName, None)
221
+ if not field:
222
+ continue
223
+
224
+ if isinstance(field, TupleField):
225
+ if field.jsonExclude:
226
+ continue
227
+
228
+ shortName = field.shortName if field.shortName else field.name
229
+
230
+ elif isinstance(field, InstrumentedAttribute):
231
+ if isinstance(field.comparator.prop, RelationshipProperty):
232
+ continue
233
+
234
+ shortName = field.doc if field.doc else field.name
235
+
236
+ else:
237
+ continue
238
+
239
+ # Underscore means skip
240
+ if shortName == JSON_EXCLUDE:
241
+ continue
242
+
243
+ fieldMap[shortName] = normalName
244
+
245
+ cls.__shortFieldNamesMap__ = fieldMap
246
+
247
+
248
+ def removeTuplesForPackage(packageName):
249
+ tupleNames = [
250
+ cls.tupleName()
251
+ for cls in TUPLE_TYPES
252
+ if cls.__name__.startswith("%s." % packageName)
253
+ ]
254
+
255
+ removeTuplesForTupleNames(tupleNames)
256
+
257
+
258
+ def removeTuplesForTupleNames(tupleNames):
259
+ global TUPLE_TYPES, TUPLE_TYPES_BY_NAME, TUPLE_TYPES_BY_SHORT_NAME
260
+
261
+ tupleNames = set(tupleNames)
262
+
263
+ def filt(cls):
264
+ return cls.tupleName() not in tupleNames
265
+
266
+ tupleShortNames = [
267
+ cls.__tupleTypeShort__ for cls in TUPLE_TYPES if filt(cls)
268
+ ]
269
+
270
+ # Remove from tuple types
271
+ TUPLE_TYPES = list(filter(filt, TUPLE_TYPES))
272
+
273
+ # Remove from tuple types by name
274
+ for tupleName in tupleNames:
275
+ if tupleName in TUPLE_TYPES_BY_NAME:
276
+ del TUPLE_TYPES_BY_NAME[tupleName]
277
+
278
+ # Remove from tuple types by name
279
+ for tupleShortName in tupleShortNames:
280
+ if tupleShortName in TUPLE_TYPES_BY_SHORT_NAME:
281
+ del TUPLE_TYPES_BY_SHORT_NAME[tupleShortName]
282
+
283
+
284
+ class TupleField:
285
+ class _Map:
286
+ pass
287
+
288
+ def __init__(
289
+ self,
290
+ defaultValue=None,
291
+ typingType=None,
292
+ comment="",
293
+ shortName=None,
294
+ jsonExclude=False,
295
+ ):
296
+ self.name = None
297
+ self.shortName = shortName
298
+ self.defaultValue = defaultValue
299
+ self.typingType = typingType
300
+ self.comment = comment
301
+ self.jsonExclude = jsonExclude
302
+
303
+
304
+ class PolymorphicTupleTypeFieldArg:
305
+ def __init__(self, fieldName: str):
306
+ self.fieldName = fieldName
307
+
308
+
309
+ class _TupleToSqlaJsonMixin:
310
+ def tupleToSqlaBulkInsertDict(self, includeNulls=True):
311
+ insertDict = {}
312
+
313
+ for field in self.__class__.__dict__.values():
314
+ if not isinstance(field, InstrumentedAttribute):
315
+ continue
316
+
317
+ if isinstance(field.comparator.prop, RelationshipProperty):
318
+ continue
319
+
320
+ def convert(value):
321
+ if isinstance(value, WKBElement):
322
+ return value.desc
323
+
324
+ else:
325
+ return value
326
+
327
+ value = getattr(self, field.name)
328
+ if includeNulls or value is not None:
329
+ insertDict[field.name] = convert(value)
330
+
331
+ return insertDict
332
+
333
+
334
+ class TupleFieldValidatorABC(ABC):
335
+ """Tuple Value Validator
336
+
337
+ This class can be used to validate value ranges within a vortex.Tuple field
338
+
339
+ """
340
+
341
+ @abstractmethod
342
+ def validate(self, fieldName: str, value):
343
+ """Validate
344
+
345
+ This method is called to validate the value.
346
+ If the value is invalid a ValueError will be raised.
347
+
348
+ :param fieldName: The name of the field being validated
349
+ :param value: The value to be checked
350
+ """
351
+
352
+
353
+ class IntTupleFieldValidator(TupleFieldValidatorABC):
354
+ def __init__(self, low: Optional[int], high: Optional[int]):
355
+ self._low = low
356
+ self._high = high
357
+
358
+ def validate(self, fieldName: str, value):
359
+ if self._low is not None and value < self._low:
360
+ raise ValueError(
361
+ f"Field {fieldName}," f" Value {value} is less than {self._low}"
362
+ )
363
+
364
+ if self._high is not None and self._high < value:
365
+ raise ValueError(
366
+ f"Field {fieldName},"
367
+ f" Value {value} is greater than {self._high}"
368
+ )
369
+
370
+
371
+ def getConcreteClass(TupleClass, jsonDict):
372
+ # Create concrete class for Tuple
373
+ if not hasattr(TupleClass, "__tupleArgs__"):
374
+ return TupleClass
375
+
376
+ polyType = [
377
+ p
378
+ for p in TupleClass.__tupleArgs__
379
+ if isinstance(p, PolymorphicTupleTypeFieldArg)
380
+ ]
381
+ if not polyType:
382
+ return TupleClass
383
+
384
+ fieldName = polyType[0].fieldName
385
+ ConcreteTupleClass = TUPLE_TYPES_BY_NAME[jsonDict[fieldName]]
386
+
387
+ if not issubclass(ConcreteTupleClass, TupleClass):
388
+ raise TypeError(
389
+ f"{ConcreteTupleClass.__name__} is not derived"
390
+ f" from {TupleClass.__name__}"
391
+ )
392
+
393
+ return ConcreteTupleClass
394
+
395
+
396
+ class _TupleToPlainJsonMixin:
397
+ def tupleToSmallJsonDict(self, includeNones=True, includeFalse=True):
398
+ if not self.__shortFieldNamesMap__:
399
+ raise Exception(
400
+ "Tuple %s has no shortFieldNames defined" % self.tupleType()
401
+ )
402
+
403
+ return self.__tupleToJsonDict(
404
+ includeNones=includeNones,
405
+ includeFalse=includeFalse,
406
+ useShortNames=True,
407
+ )
408
+
409
+ def tupleToRestfulJsonDict(self, includeNones=True, includeFalse=True):
410
+ return self.__tupleToJsonDict(
411
+ includeNones=includeNones,
412
+ includeFalse=includeFalse,
413
+ useShortNames=False,
414
+ )
415
+
416
+ def __tupleToJsonDict(
417
+ self, includeNones=True, includeFalse=True, useShortNames=True
418
+ ):
419
+ if useShortNames:
420
+ json = {
421
+ "_tt": (
422
+ self.__tupleTypeShort__
423
+ if self.__tupleTypeShort__
424
+ else self.__tupleType__
425
+ )
426
+ }
427
+ else:
428
+ json = {}
429
+
430
+ def convert(value):
431
+ if isinstance(value, list):
432
+ return [convert(v) for v in value]
433
+
434
+ elif isinstance(value, dict):
435
+ return {convert(k): convert(i) for k, i in value.items()}
436
+
437
+ elif isinstance(value, Tuple):
438
+ if useShortNames:
439
+ return value.tupleToSmallJsonDict()
440
+ return value.tupleToRestfulJsonDict()
441
+
442
+ elif isinstance(value, WKBElement):
443
+ return convertFromWkbElement(value)
444
+
445
+ elif isinstance(value, TupleField):
446
+ return None
447
+
448
+ elif isinstance(value, datetime):
449
+ return value.strftime(ISO8601)
450
+
451
+ else:
452
+ return value
453
+
454
+ for shortName, normalName in self.__shortFieldNamesMap__.items():
455
+ value = convert(getattr(self, normalName))
456
+ if value is None and not includeNones:
457
+ continue
458
+ if value is False and not includeFalse:
459
+ continue
460
+ if useShortNames:
461
+ json[shortName] = value
462
+ else:
463
+ json[normalName] = value
464
+
465
+ return json
466
+
467
+ @staticmethod
468
+ def smallJsonDictToTuple(jsonDict: dict) -> "Tuple":
469
+ tupleShortType = jsonDict.get("_tt")
470
+ if not tupleShortType:
471
+ raise Exception(
472
+ "Tuple.smallJsonDictToTuple: jsonDict has no _tt field"
473
+ )
474
+
475
+ Tuple_ = TUPLE_TYPES_BY_SHORT_NAME.get(tupleShortType)
476
+ if not Tuple_:
477
+ raise Exception(
478
+ "Tuple.smallJsonDictToTuple: %s is not a registered tuple type"
479
+ % tupleShortType
480
+ )
481
+
482
+ if not Tuple_.__shortFieldNamesMap__:
483
+ raise Exception(
484
+ "Tuple %s has no shortFieldNames defined" % Tuple.tupleType()
485
+ )
486
+
487
+ def convert(value):
488
+ if value in (None, ""):
489
+ return value
490
+
491
+ if isinstance(value, list):
492
+ return [convert(v) for v in value]
493
+
494
+ if isinstance(value, dict):
495
+ if value.get("_tt", None):
496
+ return Tuple.smallJsonDictToTuple(value)
497
+ return {convert(k): convert(i) for k, i in value.items()}
498
+
499
+ # elif isinstance(value, WKBElement):
500
+ # return convertFromWkbElement(value)
501
+
502
+ if isinstance(value, str) and re.match(ISO8601_REGEXP, value):
503
+ return datetime.strptime(value, ISO8601)
504
+
505
+ return value
506
+
507
+ newTuple = Tuple_()
508
+
509
+ for shortName, normalName in Tuple_.__shortFieldNamesMap__.items():
510
+ setattr(newTuple, normalName, convert(jsonDict.get(shortName)))
511
+
512
+ return newTuple
513
+
514
+ @staticmethod
515
+ def restfulJsonDictToTupleWithValidation(
516
+ jsonDict: dict, TupleClass: Type["Tuple"]
517
+ ) -> "Tuple":
518
+ """Restful JsonDict to Tuple With Validation
519
+
520
+ This method uses the typings on the fields in a tuple to validate the
521
+ types of data
522
+ """
523
+
524
+ if not get_type_hints(TupleClass):
525
+ raise TypeError(f"{TupleClass} has no annotations")
526
+
527
+ # Create concrete class for Tuple
528
+ TupleClass = getConcreteClass(TupleClass, jsonDict)
529
+
530
+ typeHints = get_type_hints(TupleClass)
531
+ extraTypeHits = get_type_hints(TupleClass, include_extras=True)
532
+
533
+ def getAndCheckTypeHint(fieldName):
534
+ typeHint = typeHints.get(fieldName, None)
535
+
536
+ if not typeHint:
537
+ raise TypeError(
538
+ f"Type hint for {TupleClass} field {fieldName} is falsy"
539
+ )
540
+
541
+ # Check some type hints
542
+ if get_origin(typeHint) is dict:
543
+ keyHint, _ = get_args(typeHint)
544
+ if keyHint is not str:
545
+ raise TypeError(
546
+ f"Type hint for {TupleClass}"
547
+ f" field {fieldName}"
548
+ f" must have a str type for the key,"
549
+ f" got {keyHint}"
550
+ )
551
+
552
+ return typeHint
553
+
554
+ def getExtraTypeHint(fieldName):
555
+ extraTypeHit = extraTypeHits.get(fieldName, None)
556
+ return extraTypeHit
557
+
558
+ def getFieldValidatorFromExtraHint(extraTypeHint):
559
+ origin = get_origin(extraTypeHint)
560
+
561
+ # Check if the type hint is an Annotated
562
+ if origin is not Annotated:
563
+ return None
564
+
565
+ for hint in get_args(extraTypeHint):
566
+ if isinstance(hint, TupleFieldValidatorABC):
567
+ return hint
568
+
569
+ return None
570
+
571
+ def raiseIfNotType(
572
+ fieldName,
573
+ typeHint,
574
+ value,
575
+ validator: Optional[TupleFieldValidatorABC] = None,
576
+ ):
577
+ # if the field type is of the type typeHint, then we are good.
578
+ if not get_origin(typeHint) and isinstance(value, typeHint):
579
+ if validator:
580
+ validator.validate(fieldName, value)
581
+ return
582
+
583
+ # if the field type is of the type typeHint, then we are good.
584
+ if isinstance(value, list) and get_origin(typeHint) is list:
585
+ return
586
+
587
+ # if the field type is of the type typeHint, then we are good.
588
+ if isinstance(value, dict):
589
+ if get_origin(typeHint) is dict:
590
+ return
591
+
592
+ # If the type is a tuple and the value is a dict, that's a pass
593
+ if hasattr(typeHint, "__tupleType__"):
594
+ return
595
+
596
+ # handle the the Optional[fieldType] or Union case
597
+ if type(value) in get_args(typeHint):
598
+ return
599
+
600
+ # Handle the None and Optional[fieldType] case
601
+ if value is None and type(None) in get_args(typeHint):
602
+ return
603
+
604
+ raise TypeError(
605
+ f"Parsing {TupleClass}, field {fieldName}"
606
+ f" is not of type {typeHint}, got {type(value)} instead"
607
+ )
608
+
609
+ def convert(fieldName: str, value):
610
+ typeHint = getAndCheckTypeHint(fieldName)
611
+ extraTypeHint = getExtraTypeHint(fieldName)
612
+ validator = getFieldValidatorFromExtraHint(extraTypeHint)
613
+
614
+ # If a value is not provided, but is required
615
+ # Get the tupleField,
616
+ # if it has and has a defaultValue
617
+ # Then assign the default value
618
+ if value is None and not (type(None) in get_args(typeHint)):
619
+ tupleField = getattr(TupleClass, fieldName, None)
620
+ if (
621
+ isinstance(tupleField, TupleField)
622
+ and tupleField.defaultValue is not None
623
+ ):
624
+ value = tupleField.defaultValue
625
+
626
+ # Cleanup, this is a long function
627
+ del tupleField
628
+
629
+ if (
630
+ isinstance(value, str)
631
+ and datetime is typeHint
632
+ and re.match(ISO8601_REGEXP, value)
633
+ ):
634
+ return datetime.strptime(value, ISO8601)
635
+
636
+ raiseIfNotType(fieldName, typeHint, value, validator)
637
+
638
+ if value in (None, ""):
639
+ return value
640
+
641
+ if isinstance(value, list):
642
+ listItemTypeHint = get_args(typeHint)[0]
643
+ listItemTypeValidator = getFieldValidatorFromExtraHint(
644
+ get_args(extraTypeHint)[0]
645
+ )
646
+ data = []
647
+ if hasattr(listItemTypeHint, "__tupleType__"):
648
+ for item in value:
649
+ if not isinstance(item, dict):
650
+ raise TypeError(
651
+ f"Parsing {TupleClass}, field {fieldName}"
652
+ f" list item is not a json object to convert to "
653
+ f" {listItemTypeHint}, got {type(value)} instead"
654
+ )
655
+
656
+ data.append(
657
+ TupleClass.restfulJsonDictToTupleWithValidation(
658
+ item, listItemTypeHint
659
+ )
660
+ )
661
+
662
+ else:
663
+ for item in value:
664
+ raiseIfNotType(
665
+ fieldName,
666
+ listItemTypeHint,
667
+ item,
668
+ listItemTypeValidator,
669
+ )
670
+ data.append(item)
671
+
672
+ return data
673
+
674
+ if isinstance(value, dict):
675
+ # If this is just a single child tuple, import it
676
+ if hasattr(typeHint, "__tupleType__"):
677
+ return TupleClass.restfulJsonDictToTupleWithValidation(
678
+ value, typeHint
679
+ )
680
+
681
+ if not get_args(typeHint):
682
+ raise Exception(
683
+ "Typing should use `Dict[<type>,<type>]`," " not `dict`"
684
+ )
685
+
686
+ keyTypeHint, valueTypeHint = get_args(typeHint)
687
+
688
+ data = {}
689
+ if hasattr(valueTypeHint, "__tupleType__"):
690
+ for key, item in value.items():
691
+ raiseIfNotType(fieldName, keyTypeHint, key)
692
+ if not isinstance(item, dict):
693
+ raise TypeError(
694
+ f"Parsing {TupleClass}, field {fieldName}"
695
+ f" list item is not a json object to convert to "
696
+ f" {valueTypeHint}, got {type(value)} instead"
697
+ )
698
+
699
+ data[key] = (
700
+ TupleClass.restfulJsonDictToTupleWithValidation(
701
+ item, valueTypeHint
702
+ )
703
+ )
704
+
705
+ else:
706
+ keyTypeExtraHint, valueTypeExtraHint = get_args(
707
+ extraTypeHint
708
+ )
709
+ keyTypeValidator = getFieldValidatorFromExtraHint(
710
+ keyTypeExtraHint
711
+ )
712
+ valueTypeValidator = getFieldValidatorFromExtraHint(
713
+ valueTypeExtraHint
714
+ )
715
+
716
+ logger.debug(
717
+ f"typing is not checked for key 'key' in value '{value}'"
718
+ )
719
+ for key, item in value.items():
720
+ # raiseIfNotType(
721
+ # fieldName, keyTypeHint, key, keyTypeValidator
722
+ # )
723
+ # raiseIfNotType(
724
+ # fieldName, valueTypeHint, item, valueTypeValidator
725
+ # )
726
+
727
+ # this should be in SerialiseUtil.py
728
+ if isinstance(item, str) and re.match(
729
+ ISO8601_REGEXP, item
730
+ ):
731
+ item = datetime.strptime(item, ISO8601)
732
+
733
+ data[key] = item # No change
734
+
735
+ return data
736
+
737
+ # elif isinstance(value, WKBElement):
738
+ # return convertFromWkbElement(value)
739
+
740
+ return value
741
+
742
+ newTuple = TupleClass()
743
+
744
+ for fieldName in TupleClass.__fieldNames__:
745
+ setattr(
746
+ newTuple, fieldName, convert(fieldName, jsonDict.get(fieldName))
747
+ )
748
+
749
+ return newTuple
750
+
751
+
752
+ class Tuple(Jsonable, _TupleToPlainJsonMixin, _TupleToSqlaJsonMixin):
753
+ """Tuple
754
+
755
+ This class provides rich serialisation / deserialisation support.
756
+
757
+ The advantage over pure JSON is as follows:
758
+
759
+ * Data structures are restored into full Tuple classes, allowing the use of
760
+ properties, and methods on the tuples.
761
+
762
+ * Datetimes and ints are serialised and deserialsied
763
+
764
+ """
765
+
766
+ #: Tuple Type, EG com.synerty.rapui.UnitTestTuple
767
+ __tupleType__: str = None
768
+ __tupleTypeShort__ = None
769
+ __tupleArgs__ = () # Empty tuple
770
+ __fieldNames__ = None
771
+ __shortFieldNamesMap__ = None
772
+ __rapuiSerialiseType__ = T_RAPUI_TUPLE
773
+
774
+ def __init__(self, **kwargs):
775
+ if self.__fieldNames__ is None:
776
+ raise Exception("This tuple is missing the @addTupleType decorator")
777
+
778
+ Jsonable.__init__(self)
779
+
780
+ # If we're using slots, then don't use tuple fields (there arn't any anyway)
781
+ if getattr(self.__class__, "__slots__", None):
782
+ # noinspection PyTypeChecker
783
+ for key in self.__class__.__slots__:
784
+ if key in kwargs:
785
+ setattr(self, key, kwargs.pop(key))
786
+ else:
787
+ setattr(self, key, None)
788
+
789
+ if kwargs:
790
+ raise KeyError(
791
+ "kwargs %s were passed, but tuple %s has no such fields"
792
+ % (", ".join(kwargs), self.__tupleType__)
793
+ )
794
+
795
+ return
796
+
797
+ # Reset all the tuples.
798
+ # We never want TupleField in an instance
799
+ for name in self.__fieldNames__:
800
+ tupleField = getattr(self.__class__, name)
801
+
802
+ if isinstance(tupleField, TupleField):
803
+ setattr(self, name, deepcopy(tupleField.defaultValue))
804
+
805
+ elif isinstance(tupleField, InstrumentedAttribute):
806
+ default = (
807
+ self.__table__.c[name].default
808
+ if name in self.__table__.c
809
+ else None
810
+ )
811
+
812
+ assign = (
813
+ default is not None
814
+ and getattr(self, name) is None
815
+ and not isinstance(default, Sequence)
816
+ )
817
+ if assign:
818
+ setattr(self, name, deepcopy(default.arg))
819
+
820
+ # It's faster to add these at the end, rather than have logic to add one or
821
+ # the other
822
+ for key, val in kwargs.items():
823
+ if not hasattr(self, key):
824
+ raise KeyError(
825
+ "kwarg %s was passed, but tuple %s has no such TupleField"
826
+ % (key, self.__tupleType__)
827
+ )
828
+ setattr(self, key, val)
829
+
830
+ # Handle setting any polymorphic field types
831
+ for arg in self.__tupleArgs__:
832
+ if isinstance(arg, PolymorphicTupleTypeFieldArg):
833
+ setattr(self, arg.fieldName, self.__tupleType__)
834
+
835
+ @classmethod
836
+ def tupleFieldNames(cls) -> List[str]:
837
+ return cls.__fieldNames__
838
+
839
+ @classmethod
840
+ def tupleName(cls): # DEPRECIATED
841
+ return cls.__tupleType__
842
+
843
+ @classmethod
844
+ def tupleType(cls):
845
+ return cls.__tupleType__
846
+
847
+ @classmethod
848
+ def isSameTupleType(cls, other):
849
+ if not hasattr(other, "__tupleType__"):
850
+ return False
851
+ return cls.__tupleType__ == other.__tupleType__
852
+
853
+ def tupleClone(self, deep=False):
854
+ clone = self.__class__()
855
+
856
+ def cloneVal(anyVal: Any):
857
+ if isinstance(anyVal, list):
858
+ return [cloneVal(v) for v in anyVal]
859
+ if isinstance(anyVal, Tuple):
860
+ return anyVal.tupleClone()
861
+ return anyVal
862
+
863
+ for name in self.__fieldNames__:
864
+ val = getattr(self, name)
865
+ if deep:
866
+ val = cloneVal(val)
867
+ if val is not None:
868
+ setattr(clone, name, val)
869
+
870
+ return clone
871
+
872
+ def _fromJson(self, jsonStr: str):
873
+ jsonDict = json.loads(jsonStr)
874
+
875
+ assert jsonDict[Jsonable.JSON_CLASS_TYPE] == self.__rapuiSerialiseType__
876
+ return self.fromJsonDict(jsonDict)
877
+
878
+ def _toJson(self) -> str:
879
+ return json.dumps(self.toJsonDict())
880
+
881
+ def __eq__(self, other):
882
+ return id(self) == id(other)
883
+ # if other == None or type(self) != type(other):
884
+ # return False
885
+ #
886
+ # # return id(self) != id(other)
887
+ # for f in self.__fieldNames__:
888
+ # v1 = getattr(self, f, None)
889
+ # v2 = getattr(other, f, None)
890
+ #
891
+ # if isinstance(v1, Tuple):
892
+ # if v1 is not v2:
893
+ # return False
894
+ # elif v1 != v2:
895
+ # return False
896
+ #
897
+ # return True
898
+
899
+ def __ne__(self, other):
900
+ return not self == other
901
+
902
+ def __hash__(self):
903
+ return id(self)
904
+
905
+ def __repr__(self):
906
+ val = lambda name: (
907
+ getattr(self, name) if hasattr(self, name) else "AttributeError"
908
+ )
909
+
910
+ vals = ["type = %s," % self.tupleType()]
911
+ vals.extend(
912
+ ["%s = %s," % (name, val(name)) for name in self.__fieldNames__]
913
+ )
914
+
915
+ return "\n".join(vals)
916
+
917
+
918
+ class TupleHash(object):
919
+ def __init__(self, tupl):
920
+ self.tupl = tupl
921
+ from .Payload import Payload
922
+
923
+ assert tupl is not None
924
+ assert isinstance(tupl, Tuple) or isinstance(tupl, Payload)
925
+
926
+ def _key(self):
927
+ vals = []
928
+ if isinstance(self.tupl, Tuple):
929
+ vals += [self.tupl.tupleName()]
930
+ for name in self.tupl.__fieldNames__:
931
+ val = getattr(self.tupl, name)
932
+ if isinstance(val, dict):
933
+ newItems = []
934
+ for k, v in list(val.items()):
935
+ k = TupleHash(k) if isinstance(k, Tuple) else k
936
+ v = TupleHash(v) if isinstance(v, Tuple) else v
937
+ newItems.append((k, v))
938
+ val = tuple(newItems)
939
+
940
+ elif isinstance(val, (list, set)):
941
+ newItems = [
942
+ TupleHash(v)._key() if isinstance(v, Tuple) else v
943
+ for v in val
944
+ ]
945
+ val = tuple(newItems)
946
+
947
+ elif isinstance(val, Tuple):
948
+ val = TupleHash(val)._key()
949
+ # val = TupleHash(val)
950
+
951
+ vals.append((name, val))
952
+ return tuple(vals)
953
+
954
+ def __eq__(self, other):
955
+ if not isinstance(other, TupleHash):
956
+ return False
957
+ return self._key() == other._key()
958
+
959
+ def __ne__(self, other):
960
+ return not self == other
961
+
962
+ def __hash__(self):
963
+ # This issue is caused by uiData and it's deep structure
964
+ key = self._key()
965
+ try:
966
+ return hash(key)
967
+ except Exception as e:
968
+ raise e