1import re
2import fnmatch
3import inspect
4
5from calendar import timegm
6from datetime import date, datetime
7from decimal import Decimal, ROUND_HALF_EVEN
8from email.utils import formatdate
9
10from urllib.parse import urlparse, urlunparse
11
12from flask import url_for, request
13from werkzeug.utils import cached_property
14
15from .inputs import (
16 date_from_iso8601,
17 datetime_from_iso8601,
18 datetime_from_rfc822,
19 boolean,
20)
21from .errors import RestError
22from .marshalling import marshal
23from .utils import camel_to_dash, not_none
24
25
26__all__ = (
27 "Raw",
28 "String",
29 "FormattedString",
30 "Url",
31 "DateTime",
32 "Date",
33 "Boolean",
34 "Integer",
35 "Float",
36 "Arbitrary",
37 "Fixed",
38 "Nested",
39 "List",
40 "ClassName",
41 "Polymorph",
42 "Wildcard",
43 "StringMixin",
44 "MinMaxMixin",
45 "NumberMixin",
46 "MarshallingError",
47)
48
49
50class MarshallingError(RestError):
51 """
52 This is an encapsulating Exception in case of marshalling error.
53 """
54
55 def __init__(self, underlying_exception):
56 # just put the contextual representation of the error to hint on what
57 # went wrong without exposing internals
58 super(MarshallingError, self).__init__(str(underlying_exception))
59
60
61def is_indexable_but_not_string(obj):
62 return not hasattr(obj, "strip") and hasattr(obj, "__iter__")
63
64
65def is_integer_indexable(obj):
66 return isinstance(obj, list) or isinstance(obj, tuple)
67
68
69def get_value(key, obj, default=None):
70 """Helper for pulling a keyed value off various types of objects"""
71 if isinstance(key, int):
72 return _get_value_for_key(key, obj, default)
73 elif callable(key):
74 return key(obj)
75 else:
76 return _get_value_for_keys(key.split("."), obj, default)
77
78
79def _get_value_for_keys(keys, obj, default):
80 if len(keys) == 1:
81 return _get_value_for_key(keys[0], obj, default)
82 else:
83 return _get_value_for_keys(
84 keys[1:], _get_value_for_key(keys[0], obj, default), default
85 )
86
87
88def _get_value_for_key(key, obj, default):
89 if is_indexable_but_not_string(obj):
90 try:
91 return obj[key]
92 except (IndexError, TypeError, KeyError):
93 pass
94 if is_integer_indexable(obj):
95 try:
96 return obj[int(key)]
97 except (IndexError, TypeError, ValueError):
98 pass
99 return getattr(obj, key, default)
100
101
102def to_marshallable_type(obj):
103 """
104 Helper for converting an object to a dictionary only if it is not
105 dictionary already or an indexable object nor a simple type
106 """
107 if obj is None:
108 return None # make it idempotent for None
109
110 if hasattr(obj, "__marshallable__"):
111 return obj.__marshallable__()
112
113 if hasattr(obj, "__getitem__"):
114 return obj # it is indexable it is ok
115
116 return dict(obj.__dict__)
117
118
119class Raw(object):
120 """
121 Raw provides a base field class from which others should extend. It
122 applies no formatting by default, and should only be used in cases where
123 data does not need to be formatted before being serialized. Fields should
124 throw a :class:`MarshallingError` in case of parsing problem.
125
126 :param default: The default value for the field, if no value is
127 specified.
128 :param attribute: If the public facing value differs from the internal
129 value, use this to retrieve a different attribute from the response
130 than the publicly named value.
131 :param str title: The field title (for documentation purpose)
132 :param str description: The field description (for documentation purpose)
133 :param bool required: Is the field required ?
134 :param bool readonly: Is the field read only ? (for documentation purpose)
135 :param example: An optional data example (for documentation purpose)
136 :param callable mask: An optional mask function to be applied to output
137 :param bool nullable: Whether the field accepts null values in input
138 validation. When True, the generated JSON Schema will allow null
139 values for this field during request payload validation.
140 """
141
142 #: The JSON/Swagger schema type
143 __schema_type__ = "object"
144 #: The JSON/Swagger schema format
145 __schema_format__ = None
146 #: An optional JSON/Swagger schema example
147 __schema_example__ = None
148
149 def __init__(
150 self,
151 default=None,
152 attribute=None,
153 title=None,
154 description=None,
155 required=None,
156 readonly=None,
157 example=None,
158 mask=None,
159 nullable=None,
160 **kwargs
161 ):
162 self.attribute = attribute
163 self.default = default
164 self.title = title
165 self.description = description
166 self.required = required
167 self.readonly = readonly
168 self.example = example if example is not None else self.__schema_example__
169 self.mask = mask
170 self.nullable = nullable
171
172 def format(self, value):
173 """
174 Formats a field's value. No-op by default - field classes that
175 modify how the value of existing object keys should be presented should
176 override this and apply the appropriate formatting.
177
178 :param value: The value to format
179 :raises MarshallingError: In case of formatting problem
180
181 Ex::
182
183 class TitleCase(Raw):
184 def format(self, value):
185 return unicode(value).title()
186 """
187 return value
188
189 def output(self, key, obj, **kwargs):
190 """
191 Pulls the value for the given key from the object, applies the
192 field's formatting and returns the result. If the key is not found
193 in the object, returns the default value. Field classes that create
194 values which do not require the existence of the key in the object
195 should override this and return the desired value.
196
197 :raises MarshallingError: In case of formatting problem
198 """
199
200 value = get_value(key if self.attribute is None else self.attribute, obj)
201
202 if value is None:
203 default = self._v("default")
204 return self.format(default) if default else default
205
206 try:
207 data = self.format(value)
208 except MarshallingError as e:
209 msg = 'Unable to marshal field "{0}" value "{1}": {2}'.format(
210 key, value, str(e)
211 )
212 raise MarshallingError(msg)
213 return self.mask.apply(data) if self.mask else data
214
215 def _v(self, key):
216 """Helper for getting a value from attribute allowing callable"""
217 value = getattr(self, key)
218 return value() if callable(value) else value
219
220 @cached_property
221 def __schema__(self):
222 return not_none(self.schema())
223
224 def schema(self):
225 return {
226 "type": self.__schema_type__,
227 "format": self.__schema_format__,
228 "title": self.title,
229 "description": self.description,
230 "readOnly": self.readonly,
231 "default": self._v("default"),
232 "example": self.example,
233 }
234
235
236class Nested(Raw):
237 """
238 Allows you to nest one set of fields inside another.
239 See :ref:`nested-field` for more information
240
241 :param dict model: The model dictionary to nest
242 :param bool allow_null: Whether to return None instead of a dictionary
243 with null keys, if a nested dictionary has all-null keys
244 :param bool skip_none: Optional key will be used to eliminate inner fields
245 which value is None or the inner field's key not
246 exist in data
247 :param kwargs: If ``default`` keyword argument is present, a nested
248 dictionary will be marshaled as its value if nested dictionary is
249 all-null keys (e.g. lets you return an empty JSON object instead of
250 null)
251 """
252
253 __schema_type__ = None
254
255 def __init__(
256 self, model, allow_null=False, skip_none=False, as_list=False, **kwargs
257 ):
258 self.model = model
259 self.as_list = as_list
260 self.allow_null = allow_null
261 self.skip_none = skip_none
262 super(Nested, self).__init__(**kwargs)
263
264 @property
265 def nested(self):
266 return getattr(self.model, "resolved", self.model)
267
268 def output(self, key, obj, ordered=False, **kwargs):
269 value = get_value(key if self.attribute is None else self.attribute, obj)
270 if value is None:
271 if self.allow_null:
272 return None
273 elif self.default is not None:
274 return self.default
275
276 return marshal(value, self.nested, skip_none=self.skip_none, ordered=ordered)
277
278 def schema(self):
279 schema = super(Nested, self).schema()
280 ref = "#/definitions/{0}".format(self.nested.name)
281
282 if self.as_list:
283 schema["type"] = "array"
284 schema["items"] = {"$ref": ref}
285 elif any(schema.values()):
286 # There is already some properties in the schema
287 allOf = schema.get("allOf", [])
288 allOf.append({"$ref": ref})
289 schema["allOf"] = allOf
290 else:
291 schema["$ref"] = ref
292
293 # If nullable is True, wrap using anyOf to permit nulls for input validation
294 if self.nullable:
295 # Remove structural keys that conflict with anyOf composition
296 for key in ("$ref", "allOf", "type", "items"):
297 schema.pop(key, None)
298 # Create anyOf with the original schema and null type
299 anyOf = [{"$ref": ref}]
300 if self.as_list:
301 anyOf = [{"type": "array", "items": {"$ref": ref}}]
302 anyOf.append({"type": "null"})
303 schema["anyOf"] = anyOf
304
305 return schema
306
307 def clone(self, mask=None):
308 kwargs = self.__dict__.copy()
309 model = kwargs.pop("model")
310 if mask:
311 model = mask.apply(model.resolved if hasattr(model, "resolved") else model)
312 return self.__class__(model, **kwargs)
313
314
315class List(Raw):
316 """
317 Field for marshalling lists of other fields.
318
319 See :ref:`list-field` for more information.
320
321 :param cls_or_instance: The field type the list will contain.
322 """
323
324 def __init__(self, cls_or_instance, **kwargs):
325 self.min_items = kwargs.pop("min_items", None)
326 self.max_items = kwargs.pop("max_items", None)
327 self.unique = kwargs.pop("unique", None)
328 super(List, self).__init__(**kwargs)
329 error_msg = "The type of the list elements must be a subclass of fields.Raw"
330 if isinstance(cls_or_instance, type):
331 if not issubclass(cls_or_instance, Raw):
332 raise MarshallingError(error_msg)
333 self.container = cls_or_instance()
334 else:
335 if not isinstance(cls_or_instance, Raw):
336 raise MarshallingError(error_msg)
337 self.container = cls_or_instance
338
339 def format(self, value):
340 # Convert all instances in typed list to container type
341 if isinstance(value, set):
342 value = list(value)
343
344 is_nested = isinstance(self.container, Nested) or type(self.container) is Raw
345
346 def is_attr(val):
347 return self.container.attribute and hasattr(val, self.container.attribute)
348
349 if value is None:
350 return []
351 return [
352 self.container.output(
353 idx,
354 (
355 val
356 if (isinstance(val, dict) or is_attr(val)) and not is_nested
357 else value
358 ),
359 )
360 for idx, val in enumerate(value)
361 ]
362
363 def output(self, key, data, ordered=False, **kwargs):
364 value = get_value(key if self.attribute is None else self.attribute, data)
365 # we cannot really test for external dict behavior
366 if is_indexable_but_not_string(value) and not isinstance(value, dict):
367 return self.format(value)
368
369 if value is None:
370 return self._v("default")
371
372 return [marshal(value, self.container.nested)]
373
374 def schema(self):
375 schema = super(List, self).schema()
376 schema.update(
377 minItems=self._v("min_items"),
378 maxItems=self._v("max_items"),
379 uniqueItems=self._v("unique"),
380 )
381 schema["type"] = "array"
382 schema["items"] = self.container.__schema__
383 return schema
384
385 def clone(self, mask=None):
386 kwargs = self.__dict__.copy()
387 model = kwargs.pop("container")
388 if mask:
389 model = mask.apply(model)
390 return self.__class__(model, **kwargs)
391
392
393class StringMixin(object):
394 __schema_type__ = "string"
395
396 def __init__(self, *args, **kwargs):
397 self.min_length = kwargs.pop("min_length", None)
398 self.max_length = kwargs.pop("max_length", None)
399 self.pattern = kwargs.pop("pattern", None)
400 super(StringMixin, self).__init__(*args, **kwargs)
401
402 def schema(self):
403 schema = super(StringMixin, self).schema()
404 schema.update(
405 minLength=self._v("min_length"),
406 maxLength=self._v("max_length"),
407 pattern=self._v("pattern"),
408 )
409 return schema
410
411
412class MinMaxMixin(object):
413 def __init__(self, *args, **kwargs):
414 self.minimum = kwargs.pop("min", None)
415 self.exclusiveMinimum = kwargs.pop("exclusiveMin", None)
416 self.maximum = kwargs.pop("max", None)
417 self.exclusiveMaximum = kwargs.pop("exclusiveMax", None)
418 super(MinMaxMixin, self).__init__(*args, **kwargs)
419
420 def schema(self):
421 schema = super(MinMaxMixin, self).schema()
422 schema.update(
423 minimum=self._v("minimum"),
424 exclusiveMinimum=self._v("exclusiveMinimum"),
425 maximum=self._v("maximum"),
426 exclusiveMaximum=self._v("exclusiveMaximum"),
427 )
428 return schema
429
430
431class NumberMixin(MinMaxMixin):
432 __schema_type__ = "number"
433
434 def __init__(self, *args, **kwargs):
435 self.multiple = kwargs.pop("multiple", None)
436 super(NumberMixin, self).__init__(*args, **kwargs)
437
438 def schema(self):
439 schema = super(NumberMixin, self).schema()
440 schema.update(multipleOf=self._v("multiple"))
441 return schema
442
443
444class String(StringMixin, Raw):
445 """
446 Marshal a value as a string.
447 """
448
449 def __init__(self, *args, **kwargs):
450 self.enum = kwargs.pop("enum", None)
451 self.discriminator = kwargs.pop("discriminator", None)
452 super(String, self).__init__(*args, **kwargs)
453 self.required = self.discriminator or self.required
454
455 def format(self, value):
456 try:
457 return str(value)
458 except ValueError as ve:
459 raise MarshallingError(ve)
460
461 def schema(self):
462 enum = self._v("enum")
463 schema = super(String, self).schema()
464 if enum:
465 schema.update(enum=enum)
466 if enum and schema["example"] is None:
467 schema["example"] = enum[0]
468 return schema
469
470
471class Integer(NumberMixin, Raw):
472 """
473 Field for outputting an integer value.
474
475 :param int default: The default value for the field, if no value is specified.
476 """
477
478 __schema_type__ = "integer"
479
480 def format(self, value):
481 try:
482 if value is None:
483 return self.default
484 return int(value)
485 except (ValueError, TypeError) as ve:
486 raise MarshallingError(ve)
487
488
489class Float(NumberMixin, Raw):
490 """
491 A double as IEEE-754 double precision.
492
493 ex : 3.141592653589793 3.1415926535897933e-06 3.141592653589793e+24 nan inf -inf
494 """
495
496 def format(self, value):
497 try:
498 if value is None:
499 return self.default
500 return float(value)
501 except (ValueError, TypeError) as ve:
502 raise MarshallingError(ve)
503
504
505class Arbitrary(NumberMixin, Raw):
506 """
507 A floating point number with an arbitrary precision.
508
509 ex: 634271127864378216478362784632784678324.23432
510 """
511
512 def format(self, value):
513 return str(Decimal(value))
514
515
516ZERO = Decimal()
517
518
519class Fixed(NumberMixin, Raw):
520 """
521 A decimal number with a fixed precision.
522 """
523
524 def __init__(self, decimals=5, **kwargs):
525 super(Fixed, self).__init__(**kwargs)
526 self.precision = Decimal("0." + "0" * (decimals - 1) + "1")
527
528 def format(self, value):
529 dvalue = Decimal(value)
530 if not dvalue.is_normal() and dvalue != ZERO:
531 raise MarshallingError("Invalid Fixed precision number.")
532 return str(dvalue.quantize(self.precision, rounding=ROUND_HALF_EVEN))
533
534
535class Boolean(Raw):
536 """
537 Field for outputting a boolean value.
538
539 Empty collections such as ``""``, ``{}``, ``[]``, etc. will be converted to ``False``.
540 """
541
542 __schema_type__ = "boolean"
543
544 def format(self, value):
545 return boolean(value)
546
547
548class DateTime(MinMaxMixin, Raw):
549 """
550 Return a formatted datetime string in UTC. Supported formats are RFC 822 and ISO 8601.
551
552 See :func:`email.utils.formatdate` for more info on the RFC 822 format.
553
554 See :meth:`datetime.datetime.isoformat` for more info on the ISO 8601 format.
555
556 :param str dt_format: ``rfc822`` or ``iso8601``
557 """
558
559 __schema_type__ = "string"
560 __schema_format__ = "date-time"
561
562 def __init__(self, dt_format="iso8601", **kwargs):
563 super(DateTime, self).__init__(**kwargs)
564 self.dt_format = dt_format
565
566 def parse(self, value):
567 if value is None:
568 return None
569 elif isinstance(value, str):
570 parser = (
571 datetime_from_iso8601
572 if self.dt_format == "iso8601"
573 else datetime_from_rfc822
574 )
575 return parser(value)
576 elif isinstance(value, datetime):
577 return value
578 elif isinstance(value, date):
579 return datetime(value.year, value.month, value.day)
580 else:
581 raise ValueError("Unsupported DateTime format")
582
583 def format(self, value):
584 try:
585 value = self.parse(value)
586 if self.dt_format == "iso8601":
587 return self.format_iso8601(value)
588 elif self.dt_format == "rfc822":
589 return self.format_rfc822(value)
590 else:
591 raise MarshallingError("Unsupported date format %s" % self.dt_format)
592 except (AttributeError, ValueError) as e:
593 raise MarshallingError(e)
594
595 def format_rfc822(self, dt):
596 """
597 Turn a datetime object into a formatted date.
598
599 :param datetime dt: The datetime to transform
600 :return: A RFC 822 formatted date string
601 """
602 return formatdate(timegm(dt.utctimetuple()))
603
604 def format_iso8601(self, dt):
605 """
606 Turn a datetime object into an ISO8601 formatted date.
607
608 :param datetime dt: The datetime to transform
609 :return: A ISO 8601 formatted date string
610 """
611 return dt.isoformat()
612
613 def _for_schema(self, name):
614 value = self.parse(self._v(name))
615 return self.format(value) if value else None
616
617 def schema(self):
618 schema = super(DateTime, self).schema()
619 schema["default"] = self._for_schema("default")
620 schema["minimum"] = self._for_schema("minimum")
621 schema["maximum"] = self._for_schema("maximum")
622 return schema
623
624
625class Date(DateTime):
626 """
627 Return a formatted date string in UTC in ISO 8601.
628
629 See :meth:`datetime.date.isoformat` for more info on the ISO 8601 format.
630 """
631
632 __schema_format__ = "date"
633
634 def __init__(self, **kwargs):
635 kwargs.pop("dt_format", None)
636 super(Date, self).__init__(dt_format="iso8601", **kwargs)
637
638 def parse(self, value):
639 if value is None:
640 return None
641 elif isinstance(value, str):
642 return date_from_iso8601(value)
643 elif isinstance(value, datetime):
644 return value.date()
645 elif isinstance(value, date):
646 return value
647 else:
648 raise ValueError("Unsupported Date format")
649
650
651class Url(StringMixin, Raw):
652 """
653 A string representation of a Url
654
655 :param str endpoint: Endpoint name. If endpoint is ``None``, ``request.endpoint`` is used instead
656 :param bool absolute: If ``True``, ensures that the generated urls will have the hostname included
657 :param str scheme: URL scheme specifier (e.g. ``http``, ``https``)
658 """
659
660 def __init__(self, endpoint=None, absolute=False, scheme=None, **kwargs):
661 super(Url, self).__init__(**kwargs)
662 self.endpoint = endpoint
663 self.absolute = absolute
664 self.scheme = scheme
665
666 def output(self, key, obj, **kwargs):
667 try:
668 data = to_marshallable_type(obj)
669 endpoint = self.endpoint if self.endpoint is not None else request.endpoint
670 o = urlparse(url_for(endpoint, _external=self.absolute, **data))
671 if self.absolute:
672 scheme = self.scheme if self.scheme is not None else o.scheme
673 return urlunparse((scheme, o.netloc, o.path, "", "", ""))
674 return urlunparse(("", "", o.path, "", "", ""))
675 except TypeError as te:
676 raise MarshallingError(te)
677
678
679class FormattedString(StringMixin, Raw):
680 """
681 FormattedString is used to interpolate other values from
682 the response into this field. The syntax for the source string is
683 the same as the string :meth:`~str.format` method from the python
684 stdlib.
685
686 Ex::
687
688 fields = {
689 'name': fields.String,
690 'greeting': fields.FormattedString("Hello {name}")
691 }
692 data = {
693 'name': 'Doug',
694 }
695 marshal(data, fields)
696
697 :param str src_str: the string to format with the other values from the response.
698 """
699
700 def __init__(self, src_str, **kwargs):
701 super(FormattedString, self).__init__(**kwargs)
702 self.src_str = str(src_str)
703
704 def output(self, key, obj, **kwargs):
705 try:
706 data = to_marshallable_type(obj)
707 return self.src_str.format(**data)
708 except (TypeError, IndexError) as error:
709 raise MarshallingError(error)
710
711
712class ClassName(String):
713 """
714 Return the serialized object class name as string.
715
716 :param bool dash: If `True`, transform CamelCase to kebab_case.
717 """
718
719 def __init__(self, dash=False, **kwargs):
720 super(ClassName, self).__init__(**kwargs)
721 self.dash = dash
722
723 def output(self, key, obj, **kwargs):
724 classname = obj.__class__.__name__
725 if classname == "dict":
726 return "object"
727 return camel_to_dash(classname) if self.dash else classname
728
729
730class Polymorph(Nested):
731 """
732 A Nested field handling inheritance.
733
734 Allows you to specify a mapping between Python classes and fields specifications.
735
736 .. code-block:: python
737
738 mapping = {
739 Child1: child1_fields,
740 Child2: child2_fields,
741 }
742
743 fields = api.model('Thing', {
744 owner: fields.Polymorph(mapping)
745 })
746
747 :param dict mapping: Maps classes to their model/fields representation
748 """
749
750 def __init__(self, mapping, required=False, **kwargs):
751 self.mapping = mapping
752 parent = self.resolve_ancestor(list(mapping.values()))
753 super(Polymorph, self).__init__(parent, allow_null=not required, **kwargs)
754
755 def output(self, key, obj, ordered=False, **kwargs):
756 # Copied from upstream NestedField
757 value = get_value(key if self.attribute is None else self.attribute, obj)
758 if value is None:
759 if self.allow_null:
760 return None
761 elif self.default is not None:
762 return self.default
763
764 # Handle mappings
765 if not hasattr(value, "__class__"):
766 raise ValueError("Polymorph field only accept class instances")
767
768 candidates = [
769 fields for cls, fields in self.mapping.items() if type(value) == cls
770 ]
771
772 if len(candidates) <= 0:
773 raise ValueError("Unknown class: " + value.__class__.__name__)
774 elif len(candidates) > 1:
775 raise ValueError(
776 "Unable to determine a candidate for: " + value.__class__.__name__
777 )
778 else:
779 return marshal(
780 value, candidates[0].resolved, mask=self.mask, ordered=ordered
781 )
782
783 def resolve_ancestor(self, models):
784 """
785 Resolve the common ancestor for all models.
786
787 Assume there is only one common ancestor.
788 """
789 ancestors = [m.ancestors for m in models]
790 candidates = set.intersection(*ancestors)
791 if len(candidates) != 1:
792 field_names = [f.name for f in models]
793 raise ValueError(
794 "Unable to determine the common ancestor for: " + ", ".join(field_names)
795 )
796
797 parent_name = candidates.pop()
798 return models[0].get_parent(parent_name)
799
800 def clone(self, mask=None):
801 data = self.__dict__.copy()
802 mapping = data.pop("mapping")
803 for field in ("allow_null", "model"):
804 data.pop(field, None)
805
806 data["mask"] = mask
807 return Polymorph(mapping, **data)
808
809
810class Wildcard(Raw):
811 """
812 Field for marshalling list of "unkown" fields.
813
814 :param cls_or_instance: The field type the list will contain.
815 """
816
817 exclude = set()
818 # cache the flat object
819 _flat = None
820 _obj = None
821 _cache = set()
822 _last = None
823
824 def __init__(self, cls_or_instance, **kwargs):
825 super(Wildcard, self).__init__(**kwargs)
826 error_msg = "The type of the wildcard elements must be a subclass of fields.Raw"
827 if isinstance(cls_or_instance, type):
828 if not issubclass(cls_or_instance, Raw):
829 raise MarshallingError(error_msg)
830 self.container = cls_or_instance()
831 else:
832 if not isinstance(cls_or_instance, Raw):
833 raise MarshallingError(error_msg)
834 self.container = cls_or_instance
835
836 def _flatten(self, obj):
837 if obj is None:
838 return None
839 if obj == self._obj and self._flat is not None:
840 return self._flat
841 if isinstance(obj, dict):
842 self._flat = [x for x in obj.items()]
843 else:
844
845 def __match_attributes(attribute):
846 attr_name, attr_obj = attribute
847 if inspect.isroutine(attr_obj) or (
848 attr_name.startswith("__") and attr_name.endswith("__")
849 ):
850 return False
851 return True
852
853 attributes = inspect.getmembers(obj)
854 self._flat = [x for x in attributes if __match_attributes(x)]
855
856 self._cache = set()
857 self._obj = obj
858 return self._flat
859
860 @property
861 def key(self):
862 return self._last
863
864 def reset(self):
865 self.exclude = set()
866 self._flat = None
867 self._obj = None
868 self._cache = set()
869 self._last = None
870
871 def output(self, key, obj, ordered=False):
872 value = None
873 reg = fnmatch.translate(key)
874
875 if self._flatten(obj):
876 while True:
877 try:
878 # we are using pop() so that we don't
879 # loop over the whole object every time dropping the
880 # complexity to O(n)
881 if ordered:
882 # Get first element if respecting order
883 (objkey, val) = self._flat.pop(0)
884 else:
885 # Previous default retained
886 (objkey, val) = self._flat.pop()
887 if (
888 objkey not in self._cache
889 and objkey not in self.exclude
890 and re.match(reg, objkey, re.IGNORECASE)
891 ):
892 value = val
893 self._cache.add(objkey)
894 self._last = objkey
895 break
896 except IndexError:
897 break
898
899 if value is None:
900 if self.default is not None:
901 return self.container.format(self.default)
902 return None
903
904 if isinstance(self.container, Nested):
905 return marshal(
906 value,
907 self.container.nested,
908 skip_none=self.container.skip_none,
909 ordered=ordered,
910 )
911 return self.container.format(value)
912
913 def schema(self):
914 schema = super(Wildcard, self).schema()
915 schema["type"] = "object"
916 schema["additionalProperties"] = self.container.__schema__
917 return schema
918
919 def clone(self):
920 kwargs = self.__dict__.copy()
921 model = kwargs.pop("container")
922 return self.__class__(model, **kwargs)