protolizer 1.4.0__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.
protolizer/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from . import fields
2
+ from .exceptions import InvalidDataError, ValidationError
3
+ from .serializer import ListSerializer, Serializer, proto_to_dict, to_protobuf
4
+
5
+ __all__ = [
6
+ "Serializer",
7
+ "ListSerializer",
8
+ "to_protobuf",
9
+ "proto_to_dict",
10
+ "ValidationError",
11
+ "InvalidDataError",
12
+ "fields",
13
+ ] + fields.__all__
@@ -0,0 +1,25 @@
1
+ class ValidationError(Exception):
2
+ """
3
+ Raised when a validation error occurs.
4
+ Note that this exception is only raised when the serializer is in `is_valid()` mode.
5
+ and raise_exception is passed as `True` on the `is_valid()` call.
6
+ """
7
+
8
+ def __init__(self, detail=None):
9
+ self.detail = detail
10
+
11
+
12
+ class InvalidDataError(Exception):
13
+ def __init__(self, field, data=None, expected_type=None, extra=None):
14
+ self.field = field
15
+ self.data = data
16
+ self.expected_type = expected_type
17
+ self.extra = extra
18
+
19
+ def __str__(self):
20
+ text = f"Field {self.field} has invalid data: [{self.data}] => [{type(self.data)}]"
21
+ if self.expected_type:
22
+ text += f", expected type: {self.expected_type}"
23
+ if self.extra:
24
+ text += f", extra: {self.extra}"
25
+ return text
protolizer/fields.py ADDED
@@ -0,0 +1,663 @@
1
+ import base64
2
+ import binascii
3
+ import copy
4
+ import functools
5
+ import inspect
6
+ from collections.abc import Mapping
7
+ from datetime import datetime
8
+ from typing import Any, Dict, List, Optional, Tuple, Union
9
+
10
+ from protolizer.exceptions import InvalidDataError, ValidationError
11
+ from protolizer.helpers import DictMapper
12
+
13
+ __all__ = [
14
+ "Empty",
15
+ "BaseField",
16
+ "BooleanField",
17
+ "CharField",
18
+ "BytesField",
19
+ "IntField",
20
+ "CustomField",
21
+ "DateTimeField",
22
+ "TimestampField",
23
+ "FloatField",
24
+ "EnumField",
25
+ "DictField",
26
+ "ListField",
27
+ "set_value",
28
+ ]
29
+
30
+
31
+ class Empty:
32
+ """
33
+ Empty class to be used as a placeholder for None
34
+ """
35
+
36
+ pass
37
+
38
+
39
+ def is_simple_callable(obj):
40
+ """
41
+ True if the object is a callable that takes no arguments.
42
+ """
43
+ # Bail early since we cannot inspect built-in function signatures.
44
+ if inspect.isbuiltin(obj):
45
+ raise ValueError(
46
+ "Built-in function signatures are not injectable. Wrap the function call in a simple, pure Python function."
47
+ )
48
+
49
+ if not (inspect.isfunction(obj) or inspect.ismethod(obj) or isinstance(obj, functools.partial)):
50
+ return False
51
+
52
+ sig = inspect.signature(obj)
53
+ params = sig.parameters.values()
54
+ return all(
55
+ param.kind == param.VAR_POSITIONAL or param.kind == param.VAR_KEYWORD or param.default != param.empty
56
+ for param in params
57
+ )
58
+
59
+
60
+ def get_attribute(instance: Any, attrs: List[str]) -> Any:
61
+ """
62
+ Similar to Python's built in `getattr(instance, attr)`,
63
+ but takes a list of nested attributes, instead of a single attribute.
64
+
65
+ Also accepts either attribute lookup on objects or dictionary lookups.
66
+ """
67
+ for attr in attrs:
68
+ try:
69
+ if isinstance(instance, Mapping):
70
+ instance = instance[attr]
71
+ else:
72
+ instance = getattr(instance, attr)
73
+ except (IndexError, KeyError, AttributeError):
74
+ return None
75
+ if is_simple_callable(instance):
76
+ try:
77
+ instance = instance()
78
+ except (AttributeError, KeyError) as exc:
79
+ # If we raised an Attribute or KeyError here it'd get treated
80
+ # as an omitted field in `Field.get_attribute()`. Instead, we
81
+ # raise a ValueError to ensure the exception is not masked.
82
+ raise ValueError(
83
+ 'Exception raised in callable attribute "{}"; original exception was: {}'.format(attr, exc)
84
+ )
85
+
86
+ return instance
87
+
88
+
89
+ def set_value(dictionary: Dict[str, Any], keys: List[str], value: Any) -> None:
90
+ """
91
+ Similar to Python's built in `dictionary[key] = value`,
92
+ but takes a list of nested keys instead of a single key.
93
+
94
+ set_value({'a': 1}, [], {'b': 2}) -> {'a': 1, 'b': 2}
95
+ set_value({'a': 1}, ['x'], 2) -> {'a': 1, 'x': 2}
96
+ set_value({'a': 1}, ['x', 'y'], 2) -> {'a': 1, 'x': {'y': 2}}
97
+ """
98
+ if not keys:
99
+ dictionary.update(value)
100
+ return
101
+
102
+ for key in keys[:-1]:
103
+ if key not in dictionary:
104
+ dictionary[key] = {}
105
+ dictionary = dictionary[key]
106
+
107
+ dictionary[keys[-1]] = value
108
+
109
+
110
+ class BaseField(object):
111
+ _auto_creation_counter = 0
112
+ ALLOWED_TYPES = None
113
+ initial = None
114
+
115
+ def __init__(
116
+ self,
117
+ initial: Any = Empty,
118
+ proto_field: Optional[str] = None,
119
+ default: Any = None,
120
+ custom: bool = False,
121
+ context: Any = None,
122
+ read_only: bool = False,
123
+ write_only: bool = False,
124
+ required: bool = False,
125
+ allow_null: bool = True,
126
+ ) -> None:
127
+ """
128
+ Initializes the field.
129
+ :param initial: The initial value.
130
+ :param proto_field: proto field is protobuf message field
131
+ it's used when the input data key is different from the output data key.
132
+ :param default: default value for the field.
133
+ :param custom: whether the field is custom. If True, the field will be filled by the custom method.
134
+ custom method name is get_custom_{field_name}.
135
+ note that if the method is not defined, the field will be filled with None.
136
+ :param context: extra context for the field.
137
+ :param read_only: if True, the field is serialized but not deserialized.
138
+ :param write_only: if True, the field is deserialized but not serialized.
139
+ :param required: if True, the field must be present in input data.
140
+ :param allow_null: if False, None values raise a validation error.
141
+ """
142
+ # Increase the auto creation counter
143
+ self._auto_creation_counter = BaseField._auto_creation_counter
144
+ BaseField._auto_creation_counter += 1
145
+
146
+ self.initial = self.initial if (initial is Empty) else initial
147
+ self.proto_field = proto_field
148
+ self.default = default
149
+ self.custom = custom
150
+ self.context = {} if context is None else context
151
+ self.read_only = read_only
152
+ self.write_only = write_only
153
+ self.required = required
154
+ self.allow_null = allow_null
155
+
156
+ # These are set up by `.bind()` when the field is added to a serializer.
157
+ self.field_name = None
158
+ self.parent = None
159
+ self.source = None
160
+ self.attributes = None
161
+
162
+ self.meta = getattr(self, "Meta", None)
163
+ self.pb = getattr(self.meta, "schema", None) if self.meta else None
164
+
165
+ def bind(self, field_name: str, parent: Any) -> None:
166
+ """
167
+ Initializes the field name and parent for the field instance.
168
+
169
+ :param field_name: The field name.
170
+ :param parent: The parent field.
171
+ :return: None
172
+ """
173
+ self.field_name = field_name
174
+ self.parent = parent
175
+
176
+ # self.source should default to being the same as the field name.
177
+ if self.source is None:
178
+ self.source = field_name
179
+
180
+ if self.source == "*":
181
+ self.attributes = []
182
+ else:
183
+ self.attributes = self.source.split(".")
184
+
185
+ def get_initial(self) -> Any:
186
+ return self.initial() if callable(self.initial) else self.initial
187
+
188
+ def get_value(self, dictionary: Any, instance: Any) -> Any:
189
+ """
190
+ Given the *incoming* dictionary, return the value for this field
191
+ that should be validated and transformed to a native value.
192
+ """
193
+
194
+ # convert the dictionary to mapping to make it easier to work with
195
+ dictionary = DictMapper(dictionary) if isinstance(dictionary, dict) else dictionary
196
+
197
+ if self.custom:
198
+ fill_method = getattr(instance, f"get_custom_{self.field_name}", None)
199
+ return fill_method(dictionary) if fill_method else None
200
+
201
+ return dictionary[self.field_name] if self.field_name in dictionary and not self.custom else Empty
202
+
203
+ def get_attribute(self, instance: Any) -> Any:
204
+ """
205
+ Given the *outgoing* instance, return the value for this field
206
+ that should be serialized.
207
+ """
208
+ if self.custom and self.parent is not None:
209
+ fill_method = getattr(self.parent, f"get_custom_{self.field_name}", None)
210
+ if fill_method:
211
+ dictionary = DictMapper(instance) if isinstance(instance, dict) else instance
212
+ return fill_method(dictionary)
213
+ return None
214
+ return get_attribute(instance, self.attributes)
215
+
216
+ def get_default(self) -> Any:
217
+ """
218
+ Return the default value for this field.
219
+ """
220
+ return self.default
221
+
222
+ def validate_empty_values(self, data: Any) -> Tuple[bool, Any]:
223
+ """
224
+ Validate empty values, and either:
225
+ """
226
+ if data is Empty:
227
+ if self.required:
228
+ raise ValidationError("This field is required.")
229
+ return True, self.get_default()
230
+
231
+ if data is None:
232
+ if not self.allow_null:
233
+ raise ValidationError("This field may not be null.")
234
+ if self.source == "*":
235
+ return False, None
236
+ return True, None
237
+
238
+ return False, data
239
+
240
+ def run_validation(self, data: Any = Empty) -> Any:
241
+ """
242
+ Run default validation on fields.
243
+ """
244
+ (is_empty_value, data) = self.validate_empty_values(data)
245
+ if is_empty_value:
246
+ return data
247
+
248
+ return self.to_internal_value(data)
249
+
250
+ def to_internal_value(self, data: Any) -> Any:
251
+ """
252
+ Given the *incoming* primitive data, return the native value.
253
+ """
254
+ raise NotImplementedError("`to_internal_value()` must be implemented.")
255
+
256
+ def to_representation(self, value: Any) -> Any:
257
+ """
258
+ Given the native value, return the representation of this field
259
+ that should be returned to the user.
260
+ """
261
+ raise NotImplementedError("`to_representation()` must be implemented.")
262
+
263
+ def to_protobuf(self, data: Any) -> Any:
264
+ """
265
+ Given the native value, return the protobuf value.
266
+ """
267
+ raise NotImplementedError("`to_protobuf()` must be implemented.")
268
+
269
+ @property
270
+ def root(self):
271
+ """
272
+ Returns the top-level serializer for this field.
273
+ """
274
+ root = self
275
+ while root.parent is not None:
276
+ root = root.parent
277
+ return root
278
+
279
+ def __new__(cls, *args, **kwargs):
280
+ instance = super().__new__(cls)
281
+ instance._args = args
282
+ instance._kwargs = kwargs
283
+ return instance
284
+
285
+
286
+ class BooleanField(BaseField):
287
+ TRUE_VALUES = ["t", "T", "true", "True", "TRUE", "1", 1, True]
288
+ FALSE_VALUES = ["f", "F", "false", "False", "FALSE", "0", 0, False]
289
+ NULL_VALUES = ["n", "N", "null", "Null", "NULL", "", None]
290
+
291
+ def to_internal_value(self, data):
292
+ if data is not Empty:
293
+ if data in self.TRUE_VALUES:
294
+ return True
295
+ elif data in self.FALSE_VALUES:
296
+ return False
297
+ elif data in self.NULL_VALUES:
298
+ return None
299
+ raise InvalidDataError(field=self.field_name, data=data, expected_type="boolean/None")
300
+ return data
301
+
302
+ def to_representation(self, value):
303
+ if value in self.TRUE_VALUES:
304
+ return True
305
+ elif value in self.FALSE_VALUES:
306
+ return False
307
+ elif value in self.NULL_VALUES:
308
+ return None
309
+ return bool(value)
310
+
311
+ def to_protobuf(self, value):
312
+ _repr = self.to_representation(value)
313
+ if _repr is None:
314
+ return None
315
+ return _repr
316
+
317
+
318
+ class CharField(BaseField):
319
+ """
320
+ A field that validates input as a string.
321
+ """
322
+
323
+ def __init__(self, trim_whitespace: bool = False, **kwargs):
324
+ self.trim_whitespace = trim_whitespace
325
+ super().__init__(**kwargs)
326
+
327
+ def to_internal_value(self, data):
328
+ value = str(data)
329
+ return value.strip() if self.trim_whitespace else value
330
+
331
+ def to_representation(self, value):
332
+ return str(value) if value is not None else None
333
+
334
+ def to_protobuf(self, value):
335
+ _repr = self.to_representation(value)
336
+ if _repr is None:
337
+ return None
338
+ return _repr
339
+
340
+
341
+ class BytesField(BaseField):
342
+ """
343
+ A field that validates input as bytes.
344
+ """
345
+
346
+ def to_internal_value(self, data):
347
+ if isinstance(data, bytes):
348
+ return data
349
+ if isinstance(data, str):
350
+ try:
351
+ return base64.b64decode(data, validate=True)
352
+ except (ValueError, binascii.Error):
353
+ raise InvalidDataError(field=self.field_name, data=data, expected_type="bytes")
354
+ raise InvalidDataError(field=self.field_name, data=data, expected_type="bytes")
355
+
356
+ def to_representation(self, value):
357
+ if value is None:
358
+ return None
359
+ if isinstance(value, bytes):
360
+ return base64.b64encode(value).decode("ascii")
361
+ return value
362
+
363
+ def to_protobuf(self, value):
364
+ _repr = self.to_representation(value)
365
+ if _repr is None:
366
+ return None
367
+ return _repr
368
+
369
+
370
+ class IntField(BaseField):
371
+ """
372
+ A field that validates input as an integer.
373
+ """
374
+
375
+ def to_internal_value(self, data):
376
+ try:
377
+ return int(data)
378
+ except (TypeError, ValueError):
379
+ raise InvalidDataError(field=self.field_name, data=data, expected_type="integer")
380
+
381
+ def to_representation(self, value):
382
+ return int(value) if value is not None else None
383
+
384
+ def to_protobuf(self, value):
385
+ _repr = self.to_representation(value)
386
+ if _repr is None:
387
+ return None
388
+ return _repr
389
+
390
+
391
+ class FloatField(BaseField):
392
+ """
393
+ A field that validates input as a float.
394
+ """
395
+
396
+ def to_internal_value(self, data):
397
+ try:
398
+ return float(data)
399
+ except (TypeError, ValueError):
400
+ raise InvalidDataError(field=self.field_name, data=data, expected_type="float")
401
+
402
+ def to_representation(self, value):
403
+ return float(value) if value is not None else None
404
+
405
+ def to_protobuf(self, value):
406
+ _repr = self.to_representation(value)
407
+ if _repr is None:
408
+ return None
409
+ return _repr
410
+
411
+
412
+ class EnumField(BaseField):
413
+ """
414
+ A field that validates input as a protobuf enum value.
415
+ """
416
+
417
+ def __init__(self, enum_class, by_name: bool = False, **kwargs):
418
+ self.enum_class = enum_class
419
+ self.by_name = by_name
420
+ super().__init__(**kwargs)
421
+
422
+ def _coerce_enum_value(self, value):
423
+ if isinstance(value, int):
424
+ return value
425
+ if isinstance(value, str):
426
+ return self.enum_class.Value(value)
427
+ return int(value)
428
+
429
+ def to_internal_value(self, data):
430
+ if isinstance(data, int):
431
+ if data in self.enum_class.values():
432
+ return data
433
+ raise InvalidDataError(field=self.field_name, data=data, expected_type="enum")
434
+ if isinstance(data, str):
435
+ try:
436
+ return self.enum_class.Value(data)
437
+ except ValueError:
438
+ raise InvalidDataError(field=self.field_name, data=data, expected_type="enum")
439
+ raise InvalidDataError(field=self.field_name, data=data, expected_type="enum")
440
+
441
+ def to_representation(self, value):
442
+ if value is None:
443
+ return None
444
+ enum_value = self._coerce_enum_value(value)
445
+ if self.by_name:
446
+ return self.enum_class.Name(enum_value)
447
+ return enum_value
448
+
449
+ def to_protobuf(self, value):
450
+ _repr = self.to_representation(value)
451
+ if _repr is None:
452
+ return None
453
+ return _repr
454
+
455
+ def __deepcopy__(self, memo):
456
+ cls = self.__class__
457
+ result = cls.__new__(cls)
458
+ memo[id(self)] = result
459
+ for key, value in self.__dict__.items():
460
+ if key in ("enum_class", "_args"):
461
+ setattr(result, key, value)
462
+ else:
463
+ setattr(result, key, copy.deepcopy(value, memo))
464
+ return result
465
+
466
+
467
+ class CustomField(BaseField):
468
+ """
469
+ A field that validates input as a custom field.
470
+ custom fields are defined in the model class
471
+ e.g:
472
+ class MyModel(Serializer):
473
+ username = CustomField()
474
+
475
+ def get_custom_username(obj):
476
+ return "John Doe"
477
+ """
478
+
479
+ def __init__(self, child=None):
480
+ self.child = child
481
+ super().__init__(custom=True)
482
+
483
+ def to_internal_value(self, data):
484
+ if data is not Empty:
485
+ if not self.child:
486
+ return data
487
+
488
+ if isinstance(data, list):
489
+ return [self.child.to_internal_value(item) for item in data]
490
+ return self.child.to_internal_value(data)
491
+ return data
492
+
493
+ def to_representation(self, value):
494
+ if not self.child:
495
+ return value
496
+ if isinstance(value, list):
497
+ return [self.child.to_representation(item) for item in value]
498
+ return self.child.to_representation(value)
499
+
500
+ def to_protobuf(self, value):
501
+ _repr = self.to_representation(value)
502
+ if _repr is None:
503
+ return None
504
+ return _repr
505
+
506
+
507
+ class ListField(BaseField):
508
+ """
509
+ A field that validates input as a list.
510
+ """
511
+
512
+ ALLOWED_TYPES = [
513
+ "CharField",
514
+ "BytesField",
515
+ "DateTimeField",
516
+ "IntField",
517
+ "CustomField",
518
+ "BooleanField",
519
+ "FloatField",
520
+ "EnumField",
521
+ ]
522
+
523
+ def __init__(self, child=None, **kwargs):
524
+ self.type = child
525
+ if child and child.__class__.__name__ not in self.ALLOWED_TYPES:
526
+ raise ValueError(
527
+ "type {!r} is not allowed for {!r}. Allowed types are: {}".format(
528
+ child.__class__.__name__, self.__class__.__name__, ", ".join(self.ALLOWED_TYPES)
529
+ )
530
+ )
531
+ super().__init__(**kwargs)
532
+
533
+ def to_internal_value(self, data):
534
+ if data is not Empty:
535
+ if not self.type:
536
+ return data
537
+ return [self.type.to_internal_value(item) for item in data]
538
+ return data
539
+
540
+ def to_representation(self, value):
541
+ if not self.type:
542
+ return value
543
+ return [self.type.to_representation(item) for item in value]
544
+
545
+ def to_protobuf(self, value):
546
+ _repr = self.to_representation(value)
547
+ if _repr is None:
548
+ return None
549
+ return _repr
550
+
551
+
552
+ class DictField(BaseField):
553
+ """
554
+ A field that validates input as a dict.
555
+ """
556
+
557
+ ALLOWED_TYPES = ["ListField"]
558
+
559
+ def __init__(self, child=None, **kwargs):
560
+ self.type = child
561
+ if child and child.__class__.__name__ not in self.ALLOWED_TYPES:
562
+ raise ValueError(
563
+ "type {!r} is not allowed for {!r}. Allowed types are: {}".format(
564
+ child.__class__.__name__, self.__class__.__name__, ", ".join(self.ALLOWED_TYPES)
565
+ )
566
+ )
567
+
568
+ super().__init__(**kwargs)
569
+
570
+ def to_internal_value(self, data):
571
+ if data is not Empty:
572
+ if not self.type:
573
+ return data
574
+ return {key: self.type.to_internal_value(value) for key, value in data.items()}
575
+ return data
576
+
577
+ def to_representation(self, value):
578
+ if not self.type:
579
+ return value
580
+ return {key: self.type.to_representation(item) for key, item in value.items()}
581
+
582
+ def to_protobuf(self, value):
583
+ _repr = self.to_representation(value)
584
+ if _repr is None:
585
+ return None
586
+ return _repr
587
+
588
+
589
+ class DateTimeField(BaseField):
590
+ """
591
+ A field that validates input as a datetime.
592
+ Internal value is always datetime (or None). Representation is string (if format set) or timestamp float.
593
+ """
594
+
595
+ def __init__(self, fmt: Optional[str] = "%Y-%m-%dT%H:%M:%S", **kwargs: Any) -> None:
596
+ self.format = fmt
597
+ super().__init__(**kwargs)
598
+
599
+ def to_internal_value(self, data: Any) -> Optional[datetime]:
600
+ if data is Empty:
601
+ return data
602
+ if isinstance(data, datetime):
603
+ return data
604
+ if self.format:
605
+ try:
606
+ return datetime.strptime(str(data), self.format)
607
+ except ValueError:
608
+ raise InvalidDataError(field=self.field_name, data=data, expected_type="datetime")
609
+ return datetime.fromtimestamp(float(data))
610
+
611
+ def to_representation(self, value: Any) -> Optional[Union[str, float]]:
612
+ if value is None:
613
+ return None
614
+ if isinstance(value, datetime):
615
+ if self.format:
616
+ return value.strftime(self.format)
617
+ return value.timestamp()
618
+ # Legacy: already a string (e.g. from older serialized data)
619
+ return value
620
+
621
+ def to_protobuf(self, value):
622
+ _repr = self.to_representation(value)
623
+ if _repr is None:
624
+ return None
625
+ return _repr
626
+
627
+
628
+ class TimestampField(BaseField):
629
+ """
630
+ A field that validates input as a timestamp.
631
+ """
632
+
633
+ ALLOWED_TYPES = ["IntField", "CharField", "FloatField"]
634
+
635
+ def __init__(self, child=None, **kwargs):
636
+ self.type = IntField() if child is None else child
637
+ if child and child.__class__.__name__ not in self.ALLOWED_TYPES:
638
+ raise ValueError(
639
+ "type {!r} is not allowed for {!r}. Allowed types are: {}".format(
640
+ child.__class__.__name__, self.__class__.__name__, ", ".join(self.ALLOWED_TYPES)
641
+ )
642
+ )
643
+ super().__init__(**kwargs)
644
+
645
+ def to_internal_value(self, data):
646
+ if data is not Empty:
647
+ if isinstance(data, datetime):
648
+ return data.timestamp()
649
+ return self.type.to_internal_value(data)
650
+ return data
651
+
652
+ def to_representation(self, value):
653
+ if value is not None:
654
+ if isinstance(value, datetime):
655
+ return value.timestamp()
656
+ return self.type.to_representation(value)
657
+ return value
658
+
659
+ def to_protobuf(self, value):
660
+ _repr = self.to_representation(value)
661
+ if _repr is None:
662
+ return None
663
+ return _repr