tissuebox 2.0.1__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.
tissuebox/__init__.py ADDED
@@ -0,0 +1,312 @@
1
+ from tissuebox.basic import array, boolean, complex_number, dictionary, integer, null, numeric, string, required
2
+ from tissuebox.helpers import exists, kgattr, sattr
3
+
4
+
5
+ class SchemaError(BaseException):
6
+ pass
7
+
8
+
9
+ def sort_unique(l):
10
+ l[:] = sorted(set(l))
11
+
12
+
13
+ def normalise(schema, start=None):
14
+ if start is None:
15
+ start = []
16
+
17
+ if type(schema) in [list, tuple, set]:
18
+ for s in schema:
19
+ normalise(s, start + [s])
20
+
21
+ if type(schema) is dict:
22
+ if "*" in schema and len(schema) > 1:
23
+ raise SchemaError(
24
+ "Can't normalise {} as it contains more keys {} than expected".format(start + ["*"], [k for k in schema.keys() if k != "*"])
25
+ )
26
+
27
+ # Track array vs dict keys
28
+ array_keys = set()
29
+ dict_keys = set()
30
+
31
+ # First detect any conflicts
32
+ for k in schema.keys():
33
+ if "." not in k:
34
+ continue
35
+
36
+ parts = k.split(".")
37
+ base_key = parts[0]
38
+
39
+ if base_key.startswith("[") and base_key.endswith("]"):
40
+ array_keys.add(base_key[1:-1]) # Remove brackets
41
+ else:
42
+ dict_keys.add(base_key)
43
+
44
+ # Check for conflicts
45
+ conflicts = array_keys.intersection(dict_keys)
46
+ if conflicts:
47
+ raise SchemaError("Ambiguous schema: '{}' is used both as array and dict pattern".format(list(conflicts)[0]))
48
+
49
+ # First pass - identify array patterns and group their fields
50
+ array_fields = {} # Will store array_key -> fields mapping
51
+
52
+ for k in list(schema.keys()):
53
+ if "." not in k:
54
+ continue
55
+
56
+ parts = k.split(".")
57
+ if not parts[0].startswith("["):
58
+ # Handle regular dot notation
59
+ try:
60
+ sch = schema
61
+ sofar = []
62
+ for s in parts[:-1]:
63
+ if s in sch:
64
+ sch = sch[s]
65
+ else:
66
+ sch[s] = {}
67
+ sch = sch[s]
68
+ sofar.append(s)
69
+ sch[parts[-1]] = schema[k]
70
+ del schema[k]
71
+ except TypeError:
72
+ raise SchemaError("Can't normalise {} as it conflicts with existing structure".format(k))
73
+ continue
74
+
75
+ # Get the first array key
76
+ array_key = parts[0][1:-1] # Remove brackets
77
+
78
+ if array_key not in array_fields:
79
+ array_fields[array_key] = {}
80
+
81
+ # Remove the first array part and store remaining path
82
+ remaining_path = ".".join(parts[1:])
83
+ array_fields[array_key][remaining_path] = schema[k]
84
+ del schema[k]
85
+
86
+ # Second pass - process each array pattern
87
+ for array_key, fields in array_fields.items():
88
+ if array_key not in schema:
89
+ schema[array_key] = [{}]
90
+
91
+ # Group nested array fields
92
+ nested_arrays = {}
93
+ regular_fields = {}
94
+
95
+ for field_path, value in fields.items():
96
+ if "." not in field_path:
97
+ regular_fields[field_path] = value
98
+ continue
99
+
100
+ parts = field_path.split(".")
101
+ if parts[0].startswith("["):
102
+ nested_key = parts[0][1:-1]
103
+ if nested_key not in nested_arrays:
104
+ nested_arrays[nested_key] = {}
105
+ nested_arrays[nested_key][".".join(parts[1:])] = value
106
+ else:
107
+ regular_fields[field_path] = value
108
+
109
+ # Add regular fields to array schema
110
+ for field, value in regular_fields.items():
111
+ if "." in field:
112
+ parts = field.split(".")
113
+ current = schema[array_key][0]
114
+ for part in parts[:-1]:
115
+ if part not in current:
116
+ current[part] = {}
117
+ current = current[part]
118
+ current[parts[-1]] = value
119
+ else:
120
+ schema[array_key][0][field] = value
121
+
122
+ # Process nested arrays recursively
123
+ for nested_key, nested_fields in nested_arrays.items():
124
+ nested_schema = {"[" + nested_key + "]." + k: v for k, v in nested_fields.items()}
125
+ normalised = normalise(nested_schema)
126
+ schema[array_key][0].update(normalised)
127
+
128
+ return schema
129
+
130
+
131
+ primitives = {
132
+ int: integer,
133
+ str: string,
134
+ bool: boolean,
135
+ float: numeric,
136
+ list: array,
137
+ set: array,
138
+ tuple: array,
139
+ dict: dictionary,
140
+ None: null,
141
+ complex: complex_number,
142
+ }
143
+
144
+
145
+ def decorate(payload):
146
+ # Decorate the payload, i.e if string add quotations, if list add brackets
147
+ if type(payload) is str:
148
+ return "'{}'".format(payload)
149
+ return payload
150
+
151
+
152
+ def msg(schema):
153
+ # Returns human-friendly validation error message for schema type using .msg attribute (e.g. "must be integer", "must be null", etc.)
154
+ if schema is None:
155
+ return "null"
156
+ if is_primitive_value(schema):
157
+ return str(schema)
158
+ if is_primitive_type(schema):
159
+ schema = primitives[schema]
160
+ return schema.msg
161
+
162
+
163
+ def is_primitive_value(schema):
164
+ global primitives
165
+ return type(schema) in primitives
166
+
167
+
168
+ def is_primitive_type(schema):
169
+ global primitives
170
+ return schema in primitives
171
+
172
+
173
+ def is_valid_schema(schema):
174
+ global primitives
175
+
176
+ # Handle collections
177
+ if type(schema) in (set, list, tuple):
178
+ return all([is_valid_schema(s) for s in schema])
179
+
180
+ # Handle dictionaries
181
+ if type(schema) is dict:
182
+ # If dict has "*", it should be the only key
183
+ if "*" in schema and len(schema) > 1:
184
+ return False
185
+ # Recursively validate all values in dict
186
+ return all(is_valid_schema(v) for v in schema.values())
187
+
188
+ # Handle primitives and tissue functions
189
+ if type(schema) in primitives:
190
+ return True
191
+
192
+ if schema in primitives:
193
+ return True
194
+
195
+ if callable(schema) and getattr(schema, "msg", None):
196
+ return True
197
+
198
+ return False
199
+
200
+
201
+ def validate(schema, payload, errors=None):
202
+ if errors is None:
203
+ errors = []
204
+
205
+ if not is_valid_schema(schema):
206
+ raise SchemaError("Schema is invalid, Use SchemaInspector to debug the schema")
207
+
208
+ normalise(schema)
209
+
210
+ if type(schema) is dict:
211
+ if type(payload) is not dict:
212
+ errors.append("must be dict")
213
+ return False
214
+
215
+ # Handle wildcard schema
216
+ if "*" in schema:
217
+ wildcard_schema = schema["*"]
218
+ for key, value in payload.items():
219
+ E = []
220
+ validate(wildcard_schema, value, E)
221
+ for e in E:
222
+ errors.append("['{}']{}".format(key, e))
223
+ sort_unique(errors)
224
+ return not errors
225
+
226
+ # First check for required fields
227
+ for k, v in schema.items():
228
+ if type(k) is str:
229
+ validator = v[0] if type(v) is tuple else v
230
+ if validator == required and k not in payload:
231
+ errors.append("['{}'] is required".format(k))
232
+
233
+ # Then validate present fields
234
+ for k in schema:
235
+ if type(k) is str and k in payload:
236
+ E = []
237
+ validator = schema[k]
238
+
239
+ # If it's a tuple of (required, type), use the type
240
+ if type(validator) is tuple and validator[0] == required:
241
+ validator = validator[1]
242
+
243
+ # Skip validation if it's just required
244
+ if validator != required:
245
+ validate(validator, payload.get(k), E)
246
+ for e in E:
247
+ errors.append("['{}']{}".format(k, e))
248
+
249
+ sort_unique(errors)
250
+ return not errors
251
+
252
+ elif type(schema) is list:
253
+ if type(payload) is not list:
254
+ errors.append("{} must be list".format(payload))
255
+ return False
256
+
257
+ if len(schema) > 1:
258
+ schema = [set(schema)]
259
+
260
+ for i, p in enumerate(payload):
261
+ E = []
262
+ validate(schema[0], p, E)
263
+ for e in E:
264
+ errors.append("[{}]{}".format(i, e))
265
+ return not errors
266
+
267
+ elif type(schema) is set:
268
+ schema = list(schema)
269
+ if not any([validate(s, payload) for s in schema]):
270
+ if len(schema) > 1:
271
+ labels = sorted([msg(s) for s in schema])
272
+ errors.append(" must be either {} or {} (but {})".format(", ".join(labels[:-1]), labels[-1], payload))
273
+ else:
274
+ errors.append("{} must be {}".format(payload, msg(schema[0])))
275
+ sort_unique(errors)
276
+ return not errors
277
+
278
+ elif type(schema) is tuple:
279
+ # If first element is required, validate rest
280
+ if len(schema) > 0 and schema[0] == required:
281
+ if len(schema) > 1:
282
+ return validate(schema[1], payload, errors)
283
+ return True
284
+
285
+ # Otherwise validate all
286
+ all_valid = True
287
+ for s in schema:
288
+ if not validate(s, payload):
289
+ errors.append("{} must be {}".format(payload, msg(s)))
290
+ all_valid = False
291
+ sort_unique(errors)
292
+ return all_valid
293
+
294
+ else:
295
+ if schema in primitives:
296
+ schema = primitives[schema]
297
+
298
+ # Just return True if schema is required
299
+ if schema == required:
300
+ return True
301
+
302
+ result = False
303
+ if callable(schema):
304
+ result = schema(payload)
305
+ elif is_primitive_value(schema):
306
+ result = schema == payload
307
+
308
+ if not result:
309
+ errors.append(" must be {} (but {})".format(msg(schema), decorate(payload)))
310
+
311
+ sort_unique(errors)
312
+ return not errors
tissuebox/basic.py ADDED
@@ -0,0 +1,548 @@
1
+ import re
2
+ from datetime import datetime
3
+ from decimal import Decimal
4
+ from ipaddress import ip_address
5
+ from urllib.parse import urlparse
6
+
7
+
8
+ # Existing core type validators
9
+ def integer(x):
10
+ if isinstance(x, bool):
11
+ return False
12
+ return isinstance(x, int)
13
+
14
+
15
+ integer.msg = "integer"
16
+
17
+
18
+ def numeric(x):
19
+ if isinstance(x, bool):
20
+ return False
21
+ return isinstance(x, (int, float, Decimal))
22
+
23
+
24
+ numeric.msg = "numeric"
25
+
26
+
27
+ def complex_number(x):
28
+ return isinstance(x, complex)
29
+
30
+
31
+ complex_number.msg = "complex number"
32
+
33
+
34
+ def string(x):
35
+ return isinstance(x, str)
36
+
37
+
38
+ string.msg = "string"
39
+
40
+
41
+ def array(x):
42
+ return isinstance(x, list)
43
+
44
+
45
+ array.msg = "list"
46
+
47
+
48
+ def dictionary(x):
49
+ return isinstance(x, dict)
50
+
51
+
52
+ dictionary.msg = "dictionary"
53
+
54
+
55
+ def boolean(x):
56
+ return isinstance(x, bool)
57
+
58
+
59
+ boolean.msg = "boolean"
60
+
61
+
62
+ def null(x):
63
+ return x is None
64
+
65
+
66
+ null.msg = "null"
67
+
68
+
69
+ # Enhanced string validators
70
+ def length(min_len=None, max_len=None):
71
+ """Validate string length is within range"""
72
+
73
+ def validator(x):
74
+ if not isinstance(x, str):
75
+ return False
76
+ if min_len is not None and len(x) < min_len:
77
+ return False
78
+ if max_len is not None and len(x) > max_len:
79
+ return False
80
+ return True
81
+
82
+ msg_parts = []
83
+ if min_len is not None:
84
+ msg_parts.append(f"at least {min_len} characters")
85
+ if max_len is not None:
86
+ msg_parts.append(f"at most {max_len} characters")
87
+
88
+ validator.msg = f"string with {' and '.join(msg_parts)}"
89
+ return validator
90
+
91
+
92
+ def pattern(regex, flags=0):
93
+ """Validate string matches regex pattern"""
94
+
95
+ def validator(x):
96
+ if not isinstance(x, str):
97
+ return False
98
+ return bool(re.match(regex, x, flags))
99
+
100
+ validator.msg = f"string matching pattern {regex}"
101
+ return validator
102
+
103
+
104
+ def alpha():
105
+ """Validate string contains only letters"""
106
+
107
+ def validator(x):
108
+ if not isinstance(x, str):
109
+ return False
110
+ return x.isalpha()
111
+
112
+ validator.msg = "string containing only letters"
113
+ return validator
114
+
115
+
116
+ def alphanumeric():
117
+ """Validate string contains only letters and numbers"""
118
+
119
+ def validator(x):
120
+ if not isinstance(x, str):
121
+ return False
122
+ return x.isalnum()
123
+
124
+ validator.msg = "string containing only letters and numbers"
125
+ return validator
126
+
127
+
128
+ def lowercase():
129
+ """Validate string is lowercase"""
130
+
131
+ def validator(x):
132
+ if not isinstance(x, str):
133
+ return False
134
+ return x.islower()
135
+
136
+ validator.msg = "lowercase string"
137
+ return validator
138
+
139
+
140
+ def uppercase():
141
+ """Validate string is uppercase"""
142
+
143
+ def validator(x):
144
+ if not isinstance(x, str):
145
+ return False
146
+ return x.isupper()
147
+
148
+ validator.msg = "uppercase string"
149
+ return validator
150
+
151
+
152
+ # Number validators
153
+ def positive(x):
154
+ """Validate number is positive"""
155
+ if not numeric(x):
156
+ return False
157
+ return x > 0
158
+
159
+
160
+ positive.msg = "positive number"
161
+
162
+
163
+ def negative(x):
164
+ """Validate number is negative"""
165
+ if not numeric(x):
166
+ return False
167
+ return x < 0
168
+
169
+
170
+ negative.msg = "negative number"
171
+
172
+
173
+ def between(min_val=None, max_val=None):
174
+ """Validate number is within range"""
175
+
176
+ def validator(x):
177
+ if not numeric(x):
178
+ return False
179
+ if min_val is not None and x < min_val:
180
+ return False
181
+ if max_val is not None and x > max_val:
182
+ return False
183
+ return True
184
+
185
+ msg_parts = []
186
+ if min_val is not None:
187
+ msg_parts.append(f"greater than or equal to {min_val}")
188
+ if max_val is not None:
189
+ msg_parts.append(f"less than or equal to {max_val}")
190
+
191
+ validator.msg = f"number {' and '.join(msg_parts)}"
192
+ return validator
193
+
194
+
195
+ def even(x):
196
+ """Validate number is even"""
197
+ if not integer(x):
198
+ return False
199
+ return x % 2 == 0
200
+
201
+
202
+ even.msg = "even number"
203
+
204
+
205
+ def odd(x):
206
+ """Validate number is odd"""
207
+ if not integer(x):
208
+ return False
209
+ return x % 2 != 0
210
+
211
+
212
+ odd.msg = "odd number"
213
+
214
+
215
+ # Date and time validators
216
+ def iso_date(x):
217
+ """Validate string is ISO format date"""
218
+ if not isinstance(x, str):
219
+ return False
220
+ try:
221
+ datetime.fromisoformat(x)
222
+ return True
223
+ except ValueError:
224
+ return False
225
+
226
+
227
+ iso_date.msg = "ISO format date string (YYYY-MM-DD)"
228
+
229
+
230
+ def future_date():
231
+ """Validate date is in the future"""
232
+
233
+ def validator(x):
234
+ if not isinstance(x, str):
235
+ return False
236
+ try:
237
+ date = datetime.fromisoformat(x)
238
+ return date > datetime.now()
239
+ except ValueError:
240
+ return False
241
+
242
+ validator.msg = "future date"
243
+ return validator
244
+
245
+
246
+ def past_date():
247
+ """Validate date is in the past"""
248
+
249
+ def validator(x):
250
+ if not isinstance(x, str):
251
+ return False
252
+ try:
253
+ date = datetime.fromisoformat(x)
254
+ return date < datetime.now()
255
+ except ValueError:
256
+ return False
257
+
258
+ validator.msg = "past date"
259
+ return validator
260
+
261
+
262
+ # Array validators
263
+ def min_length(min_len):
264
+ """Validate array has minimum length"""
265
+
266
+ def validator(x):
267
+ if not array(x):
268
+ return False
269
+ return len(x) >= min_len
270
+
271
+ validator.msg = f"array with at least {min_len} items"
272
+ return validator
273
+
274
+
275
+ def max_length(max_len):
276
+ """Validate array has maximum length"""
277
+
278
+ def validator(x):
279
+ if not array(x):
280
+ return False
281
+ return len(x) <= max_len
282
+
283
+ validator.msg = f"array with at most {max_len} items"
284
+ return validator
285
+
286
+
287
+ # Web-related validators
288
+ def email(x):
289
+ """Validate email address"""
290
+ if not isinstance(x, str):
291
+ return False
292
+ pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
293
+ return bool(re.match(pattern, x))
294
+
295
+
296
+ email.msg = "valid email address"
297
+
298
+
299
+ def url(x):
300
+ """Validate URL"""
301
+ if not isinstance(x, str):
302
+ return False
303
+ try:
304
+ result = urlparse(x)
305
+ return all([result.scheme, result.netloc])
306
+ except ValueError:
307
+ return False
308
+
309
+
310
+ url.msg = "valid URL"
311
+
312
+
313
+ def ip_address_str(x):
314
+ """Validate IP address (v4 or v6)"""
315
+ if not isinstance(x, str):
316
+ return False
317
+ try:
318
+ ip_address(x)
319
+ return True
320
+ except ValueError:
321
+ return False
322
+
323
+
324
+ ip_address_str.msg = "valid IP address"
325
+
326
+
327
+ def ipv4(x):
328
+ """Validate IPv4 address"""
329
+ if not isinstance(x, str):
330
+ return False
331
+ try:
332
+ addr = ip_address(x)
333
+ return addr.version == 4
334
+ except ValueError:
335
+ return False
336
+
337
+
338
+ ipv4.msg = "valid IPv4 address"
339
+
340
+
341
+ def ipv6(x):
342
+ """Validate IPv6 address"""
343
+ if not isinstance(x, str):
344
+ return False
345
+ try:
346
+ addr = ip_address(x)
347
+ return addr.version == 6
348
+ except ValueError:
349
+ return False
350
+
351
+
352
+ ipv6.msg = "valid IPv6 address"
353
+
354
+
355
+ def hostname(x):
356
+ """Validate hostname"""
357
+ if not isinstance(x, str):
358
+ return False
359
+ if len(x) > 255:
360
+ return False
361
+ pattern = r"^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
362
+ return bool(re.match(pattern, x))
363
+
364
+
365
+ hostname.msg = "valid hostname"
366
+
367
+
368
+ # Specialized string validators
369
+ def uuid4(x):
370
+ """Validate UUID v4"""
371
+ if not isinstance(x, str):
372
+ return False
373
+ pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
374
+ return bool(re.match(pattern, x, re.I))
375
+
376
+
377
+ uuid4.msg = "valid UUID v4"
378
+
379
+
380
+ def slug(x):
381
+ """Validate slug (URL-friendly string)"""
382
+ if not isinstance(x, str):
383
+ return False
384
+ pattern = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
385
+ return bool(re.match(pattern, x))
386
+
387
+
388
+ slug.msg = "valid slug (lowercase letters, numbers, hyphens)"
389
+
390
+
391
+ def hex_color(x):
392
+ """Validate hex color code"""
393
+ if not isinstance(x, str):
394
+ return False
395
+ pattern = r"^#(?:[0-9a-fA-F]{3}){1,2}$"
396
+ return bool(re.match(pattern, x))
397
+
398
+
399
+ hex_color.msg = "valid hex color code"
400
+
401
+
402
+ def credit_card(x):
403
+ """Validate credit card number using Luhn algorithm"""
404
+ if not isinstance(x, str):
405
+ return False
406
+
407
+ # Remove spaces and hyphens
408
+ x = x.replace(" ", "").replace("-", "")
409
+
410
+ if not x.isdigit():
411
+ return False
412
+
413
+ # Luhn algorithm
414
+ digits = [int(d) for d in x]
415
+ for i in range(len(digits) - 2, -1, -2):
416
+ digits[i] *= 2
417
+ if digits[i] > 9:
418
+ digits[i] -= 9
419
+
420
+ return sum(digits) % 10 == 0
421
+
422
+
423
+ credit_card.msg = "valid credit card number"
424
+
425
+
426
+ def phone(country_code=None):
427
+ """Validate phone number"""
428
+
429
+ def validator(x):
430
+ if not isinstance(x, str):
431
+ return False
432
+
433
+ # Remove common separators
434
+ x = re.sub(r"[\s\-\(\)]", "", x)
435
+
436
+ if country_code == "US":
437
+ pattern = r"^\+?1?\d{10}$"
438
+ else:
439
+ # Generic international format
440
+ pattern = r"^\+?[1-9]\d{1,14}$"
441
+
442
+ return bool(re.match(pattern, x))
443
+
444
+ validator.msg = "valid phone number"
445
+ if country_code:
446
+ validator.msg += f" ({country_code} format)"
447
+ return validator
448
+
449
+
450
+ def strong_password(x):
451
+ min_length = 8
452
+
453
+ if not isinstance(x, str):
454
+ return False
455
+
456
+ if len(x) < min_length:
457
+ return False
458
+
459
+ # Check for at least one uppercase, lowercase, and digit
460
+ if not (any(c.isupper() for c in x) and any(c.islower() for c in x) and any(c.isdigit() for c in x)):
461
+ return False
462
+
463
+ return True
464
+
465
+
466
+ strong_password.msg = "password with minimum length 8, mixed case, numbers and special characters"
467
+
468
+
469
+ # Data format validators
470
+ def json_string(x):
471
+ """Validate JSON string"""
472
+ if not isinstance(x, str):
473
+ return False
474
+ try:
475
+ import json
476
+
477
+ json.loads(x)
478
+ return True
479
+ except ValueError:
480
+ return False
481
+
482
+
483
+ json_string.msg = "valid JSON string"
484
+
485
+
486
+ def base64(x):
487
+ """Validate base64 string"""
488
+ if not isinstance(x, str):
489
+ return False
490
+ pattern = r"^[A-Za-z0-9+/]*={0,2}$"
491
+ return bool(re.match(pattern, x))
492
+
493
+
494
+ base64.msg = "valid base64 string"
495
+
496
+
497
+ # Geographic validators
498
+ def latitude(x):
499
+ """Validate latitude"""
500
+ if not numeric(x):
501
+ return False
502
+ return -90 <= float(x) <= 90
503
+
504
+
505
+ latitude.msg = "valid latitude (-90 to 90)"
506
+
507
+
508
+ def longitude(x):
509
+ """Validate longitude"""
510
+ if not numeric(x):
511
+ return False
512
+ return -180 <= float(x) <= 180
513
+
514
+
515
+ longitude.msg = "valid longitude (-180 to 180)"
516
+
517
+
518
+ # Required field validator (existing)
519
+ def required(x):
520
+ """Required field validator - always returns True as it just checks presence"""
521
+ return True
522
+
523
+
524
+ required.msg = "required"
525
+
526
+
527
+ def lt(n):
528
+ def lt(x):
529
+ return x < n
530
+
531
+ lt.msg = f"less than {n}"
532
+ return lt
533
+
534
+
535
+ def gt(n):
536
+ def gt(x):
537
+ return x < n
538
+
539
+ gt.msg = f"greater than {n}"
540
+ return lt
541
+
542
+
543
+ def divisible(n):
544
+ def divisible(x):
545
+ return numeric(x) and numeric(n) and x % n == 0
546
+
547
+ divisible.msg = f"multiple of {n}"
548
+ return divisible
tissuebox/helpers.py ADDED
@@ -0,0 +1,33 @@
1
+ import re
2
+
3
+
4
+ def exists(d, attrs):
5
+ try:
6
+ for at in attrs:
7
+ d = d[at]
8
+ return True
9
+ except (KeyError, TypeError):
10
+ return False
11
+
12
+
13
+ def sattr(d, *attrs):
14
+ try:
15
+ for attr in attrs[:-2]:
16
+ if type(d.get(attr)) is not dict:
17
+ d[attr] = {}
18
+ # raise Exception("Overriding Attempt")
19
+ d = d[attr]
20
+ d[attrs[-2]] = attrs[-1]
21
+ except IndexError:
22
+ print()
23
+
24
+
25
+ def kgattr(d, sofar, *attrs):
26
+ # gattr but very strict, tries to go nested, upon KeyError return the sofar list.
27
+ for at in attrs:
28
+ sofar.append(at)
29
+ d = d[at]
30
+
31
+
32
+ def regex_in(r, l):
33
+ return bool(list(filter(re.compile(r).match, l)))
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.1
2
+ Name: tissuebox
3
+ Version: 2.0.1
4
+ Summary: Tissuebox :: Pythonic payload validator
5
+ Home-page: https://github.com/n3h3m/tissuebox.git
6
+ Author: nehemiah
7
+ Author-email: nehemiah.jacob@gmail.com
8
+ Description-Content-Type: text/x-rst
@@ -0,0 +1,7 @@
1
+ tissuebox/__init__.py,sha256=_J6gsxKl4FOdkVBx7uL-4BcobprQAJSgMRMgUIuki3Q,9868
2
+ tissuebox/basic.py,sha256=cSjEe212tFP9tmnr8Tofy5y1aY0Bo_lQhtaYYtl98Ks,10883
3
+ tissuebox/helpers.py,sha256=uxy4lXYgnsgUD6MbfSvPMuf0VgDuzbnPk91f3MdbxuU,715
4
+ tissuebox-2.0.1.dist-info/METADATA,sha256=FW61bfyNjLRMTo56S18vzlwj7UrAlAaZ88-CpyRgewg,245
5
+ tissuebox-2.0.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
6
+ tissuebox-2.0.1.dist-info/top_level.txt,sha256=5jrIChUwF7yCKAWFnbzybiDZD8HRXPKPMK84p14aj_4,10
7
+ tissuebox-2.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.6.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ tissuebox