jsonbp 0.9.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.
jsonbp/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+
2
+ import os
3
+
4
+ from . import error_type
5
+ from .parser import load, loads, invalidateCache
6
+ from .exception import SchemaViolation
7
+ from .error import useTranslation
8
+
9
+ def useLanguage(languageCode):
10
+ localizationPath = f'messages.{languageCode}.ini'
11
+
12
+ if not os.path.isfile(localizationPath):
13
+ localizationPath = os.path.join(
14
+ os.path.dirname(__file__),
15
+ "localization",
16
+ localizationPath)
17
+
18
+ useTranslation(localizationPath)
19
+
20
+
21
+ defaultLanguage = "en_US"
22
+ useLanguage(defaultLanguage)
23
+
24
+ __all__ = [
25
+ "load",
26
+ "loads",
27
+ "invalidateCache",
28
+ "SchemaViolation",
29
+ "useLanguage"
30
+ ]
31
+
jsonbp/array.py ADDED
@@ -0,0 +1,39 @@
1
+
2
+ from sys import maxsize
3
+ from .exception import SchemaViolation
4
+ from .field import JsonField
5
+
6
+ class JsonArray(JsonField):
7
+ def __init__(self, baseField):
8
+ JsonField.__init__(self,
9
+ baseField.fieldKind,
10
+ baseField.fieldType)
11
+
12
+ self.maxLength = maxsize
13
+ self.minLength = 0
14
+
15
+
16
+ def applySpec(self, spec, value):
17
+ if not spec in ("minLength", "maxLength"):
18
+ msg = f"Invalid array spec '{spec}'"
19
+ raise SchemaViolation(msg)
20
+
21
+ if value < 0:
22
+ msg = f"Invalid array length: {value}"
23
+ raise SchemaViolation(msg)
24
+
25
+ if "minLength" == spec: self.minLength = value
26
+ if "maxLength" == spec: self.maxLength = value
27
+
28
+ if not (0 <= self.minLength <= self.maxLength and 0 < self.maxLength):
29
+ msg = f"Invalid array length for '{spec}'"
30
+ raise SchemaViolation(msg)
31
+
32
+ #------------------------------------------------------------------------------
33
+
34
+ def makeArray(baseField):
35
+ return JsonArray(baseField)
36
+
37
+ def isArray(decl):
38
+ return isinstance(decl, JsonArray)
39
+
jsonbp/blueprint.py ADDED
@@ -0,0 +1,412 @@
1
+
2
+ import re
3
+ import decimal
4
+ import json
5
+ import uuid
6
+
7
+ from decimal import Decimal
8
+ from datetime import datetime
9
+
10
+ from . import field_type
11
+ from . import error_type
12
+
13
+ from .types import primitive_types
14
+ from .error import createErrorForField, createErrorForNode, createErrorForRoot
15
+ from .array import isArray
16
+
17
+ #-------------------------------------------------------------------------------
18
+
19
+ class taggedNumber:
20
+ def __init__(self, strValue): self.strValue = strValue
21
+ def __str__(self): return self.strValue
22
+ __repr__ = __str__
23
+
24
+ #-------------------------------------------------------------------------------
25
+
26
+ def d_integer(fieldName, value, specs):
27
+ strValue = value.strValue if isinstance(value, taggedNumber) else value
28
+
29
+ if None == value:
30
+ return False, createErrorForField(fieldName,
31
+ error_type.NULL_VALUE)
32
+
33
+ try:
34
+ rawValue = int(strValue)
35
+ if not specs['min'] <= rawValue <= specs['max']:
36
+ return False, createErrorForField(fieldName,
37
+ error_type.OUTSIDE_RANGE, value=rawValue)
38
+
39
+ except Exception as e:
40
+ return False, createErrorForField(fieldName,
41
+ error_type.INTEGER_PARSING, text=strValue)
42
+
43
+ return True, rawValue
44
+
45
+
46
+ def d_float(fieldName, value, specs):
47
+ strValue = value.strValue if isinstance(value, taggedNumber) else value
48
+
49
+ if None == value:
50
+ return False, createErrorForField(fieldName,
51
+ error_type.NULL_VALUE)
52
+
53
+ try:
54
+ sanedValue = strValue.replace('Infinity', 'inf')
55
+ rawValue = float(sanedValue)
56
+
57
+ if not specs['min'] <= rawValue <= specs['max']:
58
+ return False, createErrorForField(fieldName,
59
+ error_type.OUTSIDE_RANGE, value=rawValue)
60
+
61
+ except Exception as e:
62
+ return False, createErrorForField(fieldName,
63
+ error_type.FLOAT_PARSING, text=strValue)
64
+
65
+ return True, rawValue
66
+
67
+
68
+ roundingContext = decimal.Context(rounding=decimal.ROUND_DOWN)
69
+ specialChars = r'.^$*+?|'
70
+
71
+ def d_decimal(fieldName, value, specs):
72
+ strValue = value.strValue if isinstance(value, taggedNumber) else value
73
+
74
+ if None == strValue:
75
+ return False, createErrorForField(fieldName,
76
+ error_type.NULL_VALUE)
77
+
78
+ if not isinstance(strValue, str):
79
+ return False, createErrorForField(fieldName,
80
+ error_type.DECIMAL_PARSING, text=strValue)
81
+
82
+ grpSep = specs['groupSeparator']
83
+ decSep = specs['decimalSeparator']
84
+
85
+ if grpSep in specialChars: grpSep = f'\\{grpSep}'
86
+ if decSep in specialChars: decSep = f'\\{decSep}'
87
+ decimalPattern = f'^[+-]?\\d+({grpSep}\\d+)*({decSep}\\d+)?$'
88
+
89
+ if None == re.match(decimalPattern, strValue):
90
+ return False, createErrorForField(fieldName,
91
+ error_type.DECIMAL_PARSING, text=strValue)
92
+
93
+ try:
94
+ precision = f"1e-{specs['fractionalLength']}"
95
+ sanedStrValue = strValue.replace(specs['groupSeparator'], '')
96
+ sanedStrValue = sanedStrValue.replace(specs['decimalSeparator'], '.')
97
+ rawValue = Decimal(sanedStrValue).quantize(Decimal(precision),
98
+ context=roundingContext)
99
+
100
+ if specs['min'] > rawValue or rawValue > specs['max']:
101
+ return False, createErrorForField(fieldName,
102
+ error_type.OUTSIDE_RANGE, value=rawValue)
103
+
104
+ except Exception as e:
105
+ return False, createErrorForField(fieldName,
106
+ error_type.DECIMAL_PARSING, text=strValue)
107
+
108
+ return True, rawValue
109
+
110
+
111
+ def d_bool(fieldName, value, specs):
112
+ value = value.strValue if isinstance(value, taggedNumber) else value
113
+
114
+ if isinstance(value, bool):
115
+ return True, value
116
+
117
+ if not specs['coerce']:
118
+ return False, createErrorForField(fieldName,
119
+ error_type.INVALID_BOOLEAN, value=value)
120
+
121
+ # coercion attempt
122
+ # check if is 'null' or empty string
123
+ if None == value or 0 == len(value):
124
+ return True, False
125
+
126
+ try:
127
+ rawValue = float(value)
128
+ # check if is effectively zero or NaN
129
+ if 0 == rawValue or rawValue != rawValue:
130
+ return True, False
131
+
132
+ except Exception as e:
133
+ # it was just an attempt, no problem
134
+ pass
135
+
136
+ # if none of the above, then most likely it's a truthy value
137
+ return True, True
138
+
139
+
140
+ def d_datetime(fieldName, value, specs):
141
+ strValue = value.strValue if isinstance(value, taggedNumber) else value
142
+
143
+ try:
144
+ parsed_date = datetime.strptime(strValue, specs['format'])
145
+ return True, parsed_date
146
+
147
+ except ValueError as e:
148
+ return False, createErrorForField(fieldName,
149
+ error_type.INVALID_DATETIME, text=strValue)
150
+
151
+ except TypeError as e:
152
+ return False, createErrorForField(fieldName,
153
+ error_type.INVALID_STRING, text=strValue)
154
+
155
+
156
+ def d_string(fieldName, strValue, specs):
157
+ if not isinstance(strValue, str):
158
+ return False, createErrorForField(fieldName,
159
+ error_type.INVALID_STRING, value=strValue)
160
+
161
+ strLength = len(strValue)
162
+ if not specs['minLength'] <= strLength <= specs['maxLength']:
163
+ return False, createErrorForField(fieldName,
164
+ error_type.INVALID_LENGTH, length=strLength)
165
+
166
+ return True, strValue
167
+
168
+ #-------------------------------------------------------------------------------
169
+ #-------------------------------------------------------------------------------
170
+
171
+ class JsonBlueprint:
172
+ def __init__(self):
173
+ self.uuid = uuid.uuid4()
174
+ self.includes = list()
175
+ self.derived_types = dict()
176
+ self.enums = dict()
177
+ self.nodes = dict()
178
+ self.root = None
179
+
180
+ def __str__(self):
181
+ return (
182
+ f"blueprint: {self.uuid}\n" +
183
+ f"|-> types = {self.derived_types}\n" +
184
+ f"|-> enums = {self.enums}\n" +
185
+ f"|-> nodes = {self.nodes}\n" +
186
+ f"'-> root = {self.root}")
187
+
188
+ #-----------------------------------------------------------------------------
189
+
190
+ def deserialize_field(self, fieldName, fieldType, value):
191
+ if fieldType in primitive_types:
192
+ specs = primitive_types[fieldType]
193
+ baseType = fieldType
194
+
195
+ else:
196
+ specs = self.find_element_declaration(fieldType)
197
+ baseType = specs['__baseType__']
198
+
199
+ deserialize_method = globals()['d_' + baseType]
200
+ return deserialize_method(fieldName, value, specs)
201
+
202
+
203
+ def validate_enum(self, fieldName, enumType, value):
204
+ if None == value:
205
+ return False, createErrorForField(fieldName,
206
+ error_type.NULL_VALUE)
207
+
208
+ if not isinstance(value, str):
209
+ return False, createErrorForField(fieldName,
210
+ error_type.INVALID_ENUM, value=value)
211
+
212
+ possibleValues = self.find_enum_declaration(enumType)
213
+
214
+ if not value in possibleValues:
215
+ return False, createErrorForField(fieldName,
216
+ error_type.UNKNOWN_LITERAL, value=value)
217
+
218
+ return True, value
219
+
220
+
221
+ def validate_array(self, fieldName, jArray, contents):
222
+ if not isinstance(contents, list):
223
+ return False, createErrorForField(fieldName,
224
+ error_type.INVALID_ARRAY)
225
+
226
+ arrayLen = len(contents)
227
+ if not jArray.minLength <= arrayLen <= jArray.maxLength:
228
+ return False, createErrorForField(fieldName,
229
+ error_type.INVALID_LENGTH, length=arrayLen)
230
+
231
+ arrayKind = jArray.fieldKind
232
+ arrayType = jArray.fieldType
233
+
234
+ if arrayKind == field_type.NODE:
235
+ arrayNode = self.find_node_declaration(arrayType)
236
+ for idx, content in enumerate(contents):
237
+ success, processed = self.validate_node(fieldName, arrayNode, content)
238
+
239
+ if not success:
240
+ processed.setAsArrayElement(idx)
241
+ return False, processed
242
+
243
+ contents[idx] = processed
244
+
245
+ return True, contents
246
+
247
+ if arrayKind == field_type.ENUM:
248
+ for idx, value in enumerate(contents):
249
+ success, processed = self.validate_enum(fieldName, arrayType, value)
250
+
251
+ if not success:
252
+ processed.setAsArrayElement(idx)
253
+ return False, processed
254
+
255
+ contents[idx] = processed
256
+
257
+ return True, contents
258
+
259
+ for idx, value in enumerate(contents):
260
+ success, processed = self.deserialize_field(fieldName, arrayType, value)
261
+
262
+ if not success:
263
+ processed.setAsArrayElement(idx)
264
+ return False, processed
265
+
266
+ contents[idx] = processed
267
+
268
+ return True, contents
269
+
270
+
271
+ def validate_node(self, nodeName, node, contents):
272
+ if not isinstance(contents, dict):
273
+ return False, createErrorForNode(nodeName,
274
+ error_type.INVALID_NODE)
275
+
276
+ for fieldName, fieldData in node.items():
277
+ if not fieldName in contents:
278
+ if fieldData.optional: continue
279
+ return False, createErrorForNode(nodeName,
280
+ error_type.MISSING_FIELD, field=fieldName)
281
+
282
+ retrieved = contents[fieldName]
283
+ if isArray(fieldData):
284
+ success, processed = self.validate_array(fieldName, fieldData, retrieved)
285
+ if success: continue
286
+ return success, processed
287
+
288
+ fieldKind = fieldData.fieldKind
289
+ fieldType = fieldData.fieldType
290
+
291
+ if fieldKind == field_type.NODE:
292
+ nodeSpecs = self.find_node_declaration(fieldType)
293
+ success, processed = self.validate_node(fieldName, nodeSpecs, retrieved)
294
+ if not success: return success, processed
295
+ contents[fieldName] = processed
296
+ continue
297
+
298
+ if fieldKind == field_type.ENUM:
299
+ success, processed = self.validate_enum(fieldName, fieldType, retrieved)
300
+ if not success: return success, processed
301
+ contents[fieldName] = processed
302
+ continue
303
+
304
+ success, processed = self.deserialize_field(fieldName, fieldType, retrieved)
305
+ if not success: return False, processed
306
+ contents[fieldName] = processed
307
+
308
+ return True, contents
309
+
310
+
311
+ def validate(self, rootContents):
312
+ if isArray(self.root):
313
+ return self.validate_array(None, self.root, rootContents)
314
+
315
+ rootKind = self.root.fieldKind
316
+ rootType = self.root.fieldType
317
+
318
+ if rootKind == field_type.NODE:
319
+ rootNode = self.find_node_declaration(rootType)
320
+ return self.validate_node(None, rootNode, rootContents)
321
+
322
+ if rootKind == field_type.ENUM:
323
+ rootEnum = self.find_enum_declaration(rootType)
324
+ return self.validate_enum(None, rootType, rootContents)
325
+
326
+ if rootKind == field_type.SIMPLE:
327
+ return self.deserialize_field(None, rootType, rootContents)
328
+
329
+
330
+ def deserialize(self, contents):
331
+ tag_number = lambda x : taggedNumber(x)
332
+ ident = lambda x : x
333
+
334
+ try:
335
+ loaded = json.loads(contents,
336
+ parse_float=tag_number, parse_int=tag_number,
337
+ parse_constant=ident)
338
+
339
+ except json.JSONDecodeError as e:
340
+ return False, createErrorForRoot(error_type.JSON_PARSING,
341
+ line=e.lineno, column=e.colno, message=e.msg)
342
+
343
+ return self.validate(loaded)
344
+
345
+ #----------------------------------------------------------------------------
346
+
347
+ def collectSources(self, collected=None):
348
+ collected = collected if None != collected else set()
349
+ collected.add(self)
350
+
351
+ for blueprint in self.includes:
352
+ blueprint.collectSources(collected)
353
+
354
+ return collected
355
+
356
+
357
+ def collectTypes(self):
358
+ collected = list()
359
+ sources = self.collectSources()
360
+
361
+ for source in sources:
362
+ collected.extend(source.derived_types.keys())
363
+ collected.extend(source.enums.keys())
364
+ collected.extend(source.nodes.keys())
365
+
366
+ return collected
367
+
368
+
369
+ def __hash__(self):
370
+ return hash(self.uuid)
371
+
372
+ #----------------------------------------------------------------------------
373
+
374
+ def find_element_declaration(self, typeName, checked=None):
375
+ if typeName in primitive_types: return primitive_types[typeName]
376
+ if typeName in self.derived_types: return self.derived_types[typeName]
377
+ checked = checked or set()
378
+ checked.add(self)
379
+
380
+ for blueprint in self.includes:
381
+ if not blueprint in checked:
382
+ found = blueprint.find_element_declaration(typeName, checked)
383
+ if None != found: return found
384
+
385
+ return None
386
+
387
+
388
+ def find_node_declaration(self, nodeName, checked=None):
389
+ if nodeName in self.nodes: return self.nodes[nodeName]
390
+ checked = checked or set()
391
+ checked.add(self)
392
+
393
+ for blueprint in self.includes:
394
+ if not blueprint in checked:
395
+ found = blueprint.find_node_declaration(nodeName, checked)
396
+ if None != found: return found
397
+
398
+ return None
399
+
400
+
401
+ def find_enum_declaration(self, enumName, checked=None):
402
+ if enumName in self.enums: return self.enums[enumName]
403
+ checked = checked or set()
404
+ checked.add(self)
405
+
406
+ for blueprint in self.includes:
407
+ if not blueprint in checked:
408
+ found = blueprint.find_enum_declaration(enumName, checked)
409
+ if None != found: return found
410
+
411
+ return None
412
+
jsonbp/declaration.py ADDED
@@ -0,0 +1,14 @@
1
+
2
+ class TypeDeclaration:
3
+ def __init__(self, typeName, specs):
4
+ self.typeName = typeName
5
+ self.specs = specs
6
+
7
+ def isCustomized(self):
8
+ return len(self.specs) > 0
9
+
10
+ #-------------------------------------------------------------------------------
11
+
12
+ def createDeclaration(typeName, specs):
13
+ return TypeDeclaration(typeName, specs)
14
+
jsonbp/error.py ADDED
@@ -0,0 +1,117 @@
1
+
2
+ import configparser
3
+ from . import error_type
4
+
5
+ _error_names = ([ entry
6
+ for entry in dir(error_type)
7
+ if not entry.startswith('_')
8
+ ])
9
+
10
+ _error_codes = dict()
11
+ for error_name in _error_names:
12
+ error_code = getattr(error_type, error_name)
13
+ _error_codes[error_name] = error_code
14
+
15
+ #-------------------------------------------------------------------------------
16
+
17
+ texts = dict()
18
+ possiblePrefixes = {'FIELD', 'NODE', 'ARRAY', 'ROOT'}
19
+ prefixes = dict()
20
+
21
+ #-------------------------------------------------------------------------------
22
+
23
+ def useTranslation(filename):
24
+
25
+ try:
26
+ with open(filename) as fd:
27
+ contents = fd.read()
28
+
29
+ except IOError as e:
30
+ print(f"Unable to read localization file: {e.strerror}")
31
+ return
32
+
33
+ config = configparser.ConfigParser()
34
+ config.read_string(contents)
35
+ sections = config.sections()
36
+
37
+ if 'Messages' in sections:
38
+ section = config['Messages']
39
+ for entry in config.options('Messages'):
40
+ sanedEntry = entry.upper()
41
+
42
+ if sanedEntry in _error_codes:
43
+ index = _error_codes[sanedEntry]
44
+ texts[index] = section[sanedEntry]
45
+
46
+ if 'Prefixes' in sections:
47
+ section = config['Prefixes']
48
+ for entry in config.options('Prefixes'):
49
+ sanedEntry = entry.upper()
50
+
51
+ if sanedEntry in possiblePrefixes:
52
+ prefixValue = section[sanedEntry]
53
+ prefixes[sanedEntry] = prefixValue
54
+
55
+ #-------------------------------------------------------------------------------
56
+
57
+ class _instance:
58
+ def __init__(self, error_id, **context):
59
+ self.error_id = error_id
60
+ self.context = context
61
+ self.prefix = None
62
+ self.assignee = None
63
+ self.index = None
64
+
65
+ def getErrorType(self):
66
+ return self.error_id
67
+
68
+ def setAssignee(self, assigneeType, assigneeName=None):
69
+ self.prefix = assigneeType
70
+ self.assignee = assigneeName
71
+
72
+ def setAsArrayElement(self, arrayIndex):
73
+ self.prefix = "ARRAY"
74
+ self.index = arrayIndex
75
+
76
+ def __str__(self):
77
+ prefixText = ""
78
+ if None != self.prefix:
79
+ prefix = prefixes[self.prefix]
80
+ prefixText = prefix.format(
81
+ assignee=self.assignee,
82
+ index=self.index)
83
+
84
+ msg = texts[self.error_id]
85
+ errorMsg = msg.format(**self.context)
86
+ return " ".join([prefixText, errorMsg])
87
+
88
+ #-------------------------------------------------------------------------------
89
+
90
+ def createErrorForField(fieldName, error_id, **context):
91
+ result = _instance(error_id, **context)
92
+ args = ("FIELD", fieldName) if fieldName != None else ("ROOT",)
93
+ result.setAssignee(*args)
94
+ return result
95
+
96
+
97
+ def createErrorForNode(nodeName, error_id, **context):
98
+ result = _instance(error_id, **context)
99
+ args = ("NODE", nodeName) if nodeName != None else ("ROOT",)
100
+ result.setAssignee(*args)
101
+ return result
102
+
103
+
104
+ def createErrorForRoot(error_id, **context):
105
+ result = _instance(error_id, **context)
106
+ result.setAssignee("ROOT")
107
+ return result
108
+
109
+ #-------------------------------------------------------------------------------
110
+
111
+ __all__ = [
112
+ "useTranslation",
113
+ "createErrorForField",
114
+ "createErrorForNode",
115
+ "createErrorForRoot"
116
+ ]
117
+
jsonbp/error_type.py ADDED
@@ -0,0 +1,17 @@
1
+
2
+ JSON_PARSING = 0 # line, column, message
3
+ NULL_VALUE = 1 #
4
+ INTEGER_PARSING = 2 # text
5
+ FLOAT_PARSING = 3 # text
6
+ DECIMAL_PARSING = 4 # text
7
+ OUTSIDE_RANGE = 5 # value
8
+ INVALID_BOOLEAN = 6 # value
9
+ INVALID_STRING = 7 # value
10
+ INVALID_LENGTH = 8 # length
11
+ INVALID_DATETIME = 9 # text
12
+ UNKNOWN_LITERAL = 10 # text
13
+ INVALID_ENUM = 11 # value
14
+ MISSING_FIELD = 12 # field
15
+ INVALID_ARRAY = 13 #
16
+ INVALID_NODE = 14 #
17
+
jsonbp/exception.py ADDED
@@ -0,0 +1,4 @@
1
+
2
+ class SchemaViolation(Exception):
3
+ pass
4
+
jsonbp/field.py ADDED
@@ -0,0 +1,15 @@
1
+
2
+ class JsonField:
3
+ def __init__(self, fieldKind, fieldType):
4
+ self.fieldKind = fieldKind
5
+ self.fieldType = fieldType
6
+ self.optional = False
7
+
8
+ def setOptional(self):
9
+ self.optional = True
10
+
11
+ #-------------------------------------------------------------------------------
12
+
13
+ def createField(fieldKind, fieldType):
14
+ return JsonField(fieldKind, fieldType)
15
+
jsonbp/field_type.py ADDED
@@ -0,0 +1,5 @@
1
+
2
+ SIMPLE = 0
3
+ ENUM = 1
4
+ NODE = 2
5
+