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.
- vortex/DataWrapTuple.py +27 -0
- vortex/DeferUtil.py +224 -0
- vortex/Jsonable.py +304 -0
- vortex/Payload.py +115 -0
- vortex/PayloadEndpoint.py +216 -0
- vortex/PayloadEndpointTest.py +164 -0
- vortex/PayloadEnvelope.py +166 -0
- vortex/PayloadFilterKeys.py +15 -0
- vortex/PayloadIO.py +152 -0
- vortex/PayloadPriority.py +11 -0
- vortex/PayloadResponse.py +173 -0
- vortex/PayloadResponseTest.py +39 -0
- vortex/PayloadTest.py +136 -0
- vortex/PerformTestActionTuple.py +30 -0
- vortex/SerialiseUtil.py +273 -0
- vortex/TestTuple.py +61 -0
- vortex/Tuple.py +968 -0
- vortex/TupleAction.py +72 -0
- vortex/TupleActionVortex.py +5 -0
- vortex/TupleDataForTest.py +25 -0
- vortex/TuplePerfTest.py +71 -0
- vortex/TuplePolymorphicTupleTypeFieldArgTest.py +74 -0
- vortex/TupleRestfulTest.py +140 -0
- vortex/TupleSelector.py +105 -0
- vortex/TupleTest.py +127 -0
- vortex/VortexABC.py +55 -0
- vortex/VortexClientHttp.py +308 -0
- vortex/VortexClientHttpTest.py +71 -0
- vortex/VortexClientTcp.py +355 -0
- vortex/VortexClientWebsocketFactory.py +383 -0
- vortex/VortexConnectionABC.py +88 -0
- vortex/VortexFactory.py +932 -0
- vortex/VortexPayloadProtocol.py +180 -0
- vortex/VortexServer.py +367 -0
- vortex/VortexServerConnection.py +127 -0
- vortex/VortexServerHttpResource.py +193 -0
- vortex/VortexServerTcp.py +104 -0
- vortex/VortexServerWebsocket.py +287 -0
- vortex/VortexTcpMemoryLeakPayloadTest.py +611 -0
- vortex/VortexTcpMemoryLeakRawTest.py +87 -0
- vortex/VortexTest.py +22 -0
- vortex/VortexUtil.py +314 -0
- vortex/VortexWritePushProducer.py +321 -0
- vortex/__init__.py +10 -0
- vortex/data_loader/TupleDataLiveLockManager.py +383 -0
- vortex/data_loader/TupleDataLoader.py +326 -0
- vortex/data_loader/TupleDataLoaderDelegate.py +90 -0
- vortex/data_loader/TupleDataLoaderTupleABC.py +10 -0
- vortex/data_loader/TupleDataLoaderTuples.py +53 -0
- vortex/data_loader/__init__.py +0 -0
- vortex/event_bus/TupleChangeEventABC.py +15 -0
- vortex/event_bus/TupleChangeEventBus.py +192 -0
- vortex/event_bus/TupleChangeEventBusObserverABC.py +11 -0
- vortex/event_bus/__init__.py +0 -0
- vortex/handler/TupleActionProcessor.py +270 -0
- vortex/handler/TupleActionProcessorProxy.py +218 -0
- vortex/handler/TupleActionProcessorTestHandler.py +27 -0
- vortex/handler/TupleDataActionClient.py +67 -0
- vortex/handler/TupleDataObservableCache.py +201 -0
- vortex/handler/TupleDataObservableHandler.py +371 -0
- vortex/handler/TupleDataObservableProxyHandler.py +340 -0
- vortex/handler/TupleDataObservableTestHandler.py +60 -0
- vortex/handler/TupleDataObserverClient.py +158 -0
- vortex/handler/VortexJSTupleLoaderTestHandler.py +42 -0
- vortex/handler/__init__.py +0 -0
- vortex/restful/GzippedDataHttpClient.py +170 -0
- vortex/restful/GzippedDataHttpClientTest.py +103 -0
- vortex/restful/GzippedPayloadHttpClient.py +181 -0
- vortex/restful/RestfulHttpClient.py +107 -0
- vortex/restful/RestfulHttpClientTest.py +60 -0
- vortex/restful/RestfulResource.py +153 -0
- vortex/restful/RestfulResourceTest.py +100 -0
- vortex/restful/__init__.py +0 -0
- vortex/rpc/RPC.py +446 -0
- vortex/rpc/RPCTest.py +12 -0
- vortex/rpc/__init__.py +0 -0
- vortex/sqla_orm/OrmCrudHandler.py +431 -0
- vortex/sqla_orm/TupleProviderForDb.py +42 -0
- vortex/sqla_orm/VortexTupleJson.py +24 -0
- vortex/sqla_orm/__init__.py +0 -0
- vortex/storage/TupleStorageSqlite.py +359 -0
- vortex/storage/__init__.py +0 -0
- vortexpy-4.0.23.dist-info/LICENSE +21 -0
- vortexpy-4.0.23.dist-info/METADATA +43 -0
- vortexpy-4.0.23.dist-info/RECORD +87 -0
- vortexpy-4.0.23.dist-info/WHEEL +5 -0
- 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
|