marshmallow-utils 0.15.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.
Files changed (63) hide show
  1. marshmallow_utils/__init__.py +47 -0
  2. marshmallow_utils/context.py +8 -0
  3. marshmallow_utils/fields/__init__.py +56 -0
  4. marshmallow_utils/fields/babel.py +214 -0
  5. marshmallow_utils/fields/contrib.py +126 -0
  6. marshmallow_utils/fields/edtfdatestring.py +114 -0
  7. marshmallow_utils/fields/generated.py +39 -0
  8. marshmallow_utils/fields/identifier.py +42 -0
  9. marshmallow_utils/fields/isodate.py +34 -0
  10. marshmallow_utils/fields/isolanguage.py +30 -0
  11. marshmallow_utils/fields/links.py +85 -0
  12. marshmallow_utils/fields/nestedattr.py +20 -0
  13. marshmallow_utils/fields/sanitizedhtml.py +38 -0
  14. marshmallow_utils/fields/sanitizedunicode.py +22 -0
  15. marshmallow_utils/fields/strippedhtml.py +21 -0
  16. marshmallow_utils/fields/trimmed.py +15 -0
  17. marshmallow_utils/fields/tzdatetime.py +31 -0
  18. marshmallow_utils/fields/url.py +215 -0
  19. marshmallow_utils/html.py +163 -0
  20. marshmallow_utils/links.py +98 -0
  21. marshmallow_utils/permissions.py +75 -0
  22. marshmallow_utils/schemas/__init__.py +15 -0
  23. marshmallow_utils/schemas/geojson.py +90 -0
  24. marshmallow_utils/schemas/identifier.py +116 -0
  25. marshmallow_utils/translations/ar/LC_MESSAGES/messages.po +28 -0
  26. marshmallow_utils/translations/bg/LC_MESSAGES/messages.po +24 -0
  27. marshmallow_utils/translations/ca/LC_MESSAGES/messages.po +24 -0
  28. marshmallow_utils/translations/cs/LC_MESSAGES/messages.po +30 -0
  29. marshmallow_utils/translations/da/LC_MESSAGES/messages.po +24 -0
  30. marshmallow_utils/translations/de/LC_MESSAGES/messages.po +30 -0
  31. marshmallow_utils/translations/el/LC_MESSAGES/messages.po +24 -0
  32. marshmallow_utils/translations/es/LC_MESSAGES/messages.po +28 -0
  33. marshmallow_utils/translations/et/LC_MESSAGES/messages.po +28 -0
  34. marshmallow_utils/translations/fa/LC_MESSAGES/messages.po +24 -0
  35. marshmallow_utils/translations/fi/LC_MESSAGES/messages.po +30 -0
  36. marshmallow_utils/translations/fr/LC_MESSAGES/messages.po +28 -0
  37. marshmallow_utils/translations/hr/LC_MESSAGES/messages.po +24 -0
  38. marshmallow_utils/translations/hu/LC_MESSAGES/messages.po +29 -0
  39. marshmallow_utils/translations/it/LC_MESSAGES/messages.po +24 -0
  40. marshmallow_utils/translations/ja/LC_MESSAGES/messages.po +24 -0
  41. marshmallow_utils/translations/ka/LC_MESSAGES/messages.po +24 -0
  42. marshmallow_utils/translations/ko/LC_MESSAGES/messages.po +24 -0
  43. marshmallow_utils/translations/lt/LC_MESSAGES/messages.po +24 -0
  44. marshmallow_utils/translations/messages.pot +24 -0
  45. marshmallow_utils/translations/no/LC_MESSAGES/messages.po +24 -0
  46. marshmallow_utils/translations/pl/LC_MESSAGES/messages.po +24 -0
  47. marshmallow_utils/translations/pt/LC_MESSAGES/messages.po +24 -0
  48. marshmallow_utils/translations/ro/LC_MESSAGES/messages.po +29 -0
  49. marshmallow_utils/translations/ru/LC_MESSAGES/messages.po +29 -0
  50. marshmallow_utils/translations/sk/LC_MESSAGES/messages.po +24 -0
  51. marshmallow_utils/translations/sv/LC_MESSAGES/messages.po +28 -0
  52. marshmallow_utils/translations/tr/LC_MESSAGES/messages.po +28 -0
  53. marshmallow_utils/translations/uk/LC_MESSAGES/messages.po +24 -0
  54. marshmallow_utils/translations/zh_CN/LC_MESSAGES/messages.po +28 -0
  55. marshmallow_utils/translations/zh_TW/LC_MESSAGES/messages.po +24 -0
  56. marshmallow_utils/validators/__init__.py +8 -0
  57. marshmallow_utils/validators/oneof.py +50 -0
  58. marshmallow_utils-0.15.1.dist-info/METADATA +62 -0
  59. marshmallow_utils-0.15.1.dist-info/RECORD +63 -0
  60. marshmallow_utils-0.15.1.dist-info/WHEEL +4 -0
  61. marshmallow_utils-0.15.1.dist-info/entry_points.txt +2 -0
  62. marshmallow_utils-0.15.1.dist-info/licenses/AUTHORS.rst +12 -0
  63. marshmallow_utils-0.15.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,47 @@
1
+ # SPDX-FileCopyrightText: 2020-2025 CERN.
2
+ # SPDX-FileCopyrightText: 2024-2026 Graz University of Technology.
3
+ # SPDX-FileCopyrightText: 2025 KTH Royal Institute of Technology.
4
+ # SPDX-FileCopyrightText: 2026 TU Wien.
5
+ # SPDX-License-Identifier: MIT
6
+
7
+ r"""Extras and utilities for Marshmallow.
8
+
9
+ Currently, this library contains a couple of extra fields that helps with
10
+ sanitizing data as shown in the following example:
11
+
12
+ >>> from marshmallow_utils import fields
13
+ >>> from marshmallow import Schema
14
+ >>> class MySchema(Schema):
15
+ ... trim = fields.TrimmedString()
16
+ ... html = fields.SanitizedHTML()
17
+ ... text = fields.SanitizedUnicode()
18
+ ... isodate = fields.ISODateString()
19
+ ...
20
+ >>> data = MySchema().load({
21
+ ... 'trim': ' whitespace ',
22
+ ... 'html': '<script>evil()</script>',
23
+ ... 'text': 'PDF copy/paste\u200b\u000b\u001b\u0018 ',
24
+ ... 'isodate': '1999-10-27',
25
+ ... })
26
+ >>> data['trim']
27
+ 'whitespace'
28
+ >>> data['html']
29
+ 'evil()'
30
+ >>> data['text']
31
+ 'PDF copy/paste'
32
+ >>> data['isodate']
33
+ '1999-10-27'
34
+
35
+ Fields:
36
+
37
+ - :py:class:`~fields.SanitizedUnicode`: Integrates the
38
+ `ftfy <https://pypi.org/project/ftfy/>`_ for fixing broken unicode text.
39
+ - :py:class:`~fields.SanitizedHTML`: Integrates the
40
+ `bleach <https://pypi.org/project/bleach/>`_ for HTML sanitization.
41
+ - :py:class:`~fields.ISODateString`: Integrates the
42
+ `arrow <https://pypi.org/project/arrow/>`_ for date parsing.
43
+ """
44
+
45
+ __version__ = "0.15.1"
46
+
47
+ __all__ = ("__version__",)
@@ -0,0 +1,8 @@
1
+ # SPDX-FileCopyrightText: 2025 Graz University of Technology.
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """Context."""
5
+
6
+ from contextvars import ContextVar
7
+
8
+ context_schema: ContextVar[dict] = ContextVar("context_schema")
@@ -0,0 +1,56 @@
1
+ # SPDX-FileCopyrightText: 2016-2020 CERN.
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """Marshmallow fields."""
5
+
6
+ from .babel import (
7
+ BabelGettextDictField,
8
+ FormatDate,
9
+ FormatDatetime,
10
+ FormatEDTF,
11
+ FormatTime,
12
+ )
13
+ from .contrib import Function, Method
14
+ from .edtfdatestring import EDTFDateString, EDTFDateTimeString, EDTFLevel2DateString
15
+ from .generated import GenFunction, GenMethod
16
+ from .identifier import IdentifierSet, IdentifierValueSet
17
+ from .isodate import ISODateString
18
+ from .isolanguage import ISOLangString
19
+ from .links import Link, Links
20
+ from .nestedattr import NestedAttribute
21
+ from .sanitizedhtml import ALLOWED_HTML_ATTRS, ALLOWED_HTML_TAGS, SanitizedHTML
22
+ from .sanitizedunicode import SanitizedUnicode
23
+ from .strippedhtml import StrippedHTML
24
+ from .trimmed import TrimmedString
25
+ from .tzdatetime import TZDateTime
26
+ from .url import URL
27
+
28
+ __all__ = (
29
+ "ALLOWED_HTML_ATTRS",
30
+ "ALLOWED_HTML_TAGS",
31
+ "BabelGettextDictField",
32
+ "EDTFDateString",
33
+ "EDTFDateTimeString",
34
+ "EDTFLevel2DateString",
35
+ "FormatDate",
36
+ "FormatDatetime",
37
+ "FormatEDTF",
38
+ "FormatTime",
39
+ "Function",
40
+ "GenFunction",
41
+ "GenMethod",
42
+ "IdentifierSet",
43
+ "IdentifierValueSet",
44
+ "ISODateString",
45
+ "ISOLangString",
46
+ "Link",
47
+ "Links",
48
+ "Method",
49
+ "NestedAttribute",
50
+ "SanitizedHTML",
51
+ "SanitizedUnicode",
52
+ "StrippedHTML",
53
+ "TrimmedString",
54
+ "TZDateTime",
55
+ "URL",
56
+ )
@@ -0,0 +1,214 @@
1
+ # SPDX-FileCopyrightText: 2016-2024 CERN.
2
+ # SPDX-FileCopyrightText: 2024-2025 Graz University of Technology.
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ """Localized Extended Date(/Time) Format Level 0 date string field."""
6
+
7
+ import arrow
8
+ from babel import Locale
9
+ from babel.core import negotiate_locale
10
+ from babel.dates import LC_TIME, format_date, format_datetime, format_time
11
+ from babel_edtf import format_edtf
12
+ from marshmallow import fields
13
+
14
+
15
+ class BabelFormatField(fields.String):
16
+ """Base classe for babel date and time formatting fields.
17
+
18
+ The babel format field is used only for dumping.
19
+ """
20
+
21
+ def __init__(self, format="medium", locale=LC_TIME, parse=True, **kwargs):
22
+ """Constructor.
23
+
24
+ :param format: The format to use (either ``short``, ``medium``,
25
+ ``long`` or ``full``).
26
+ :param locale: The current locale or a callable returning the current
27
+ locale.
28
+ """
29
+ self._format = format
30
+ self._locale = locale
31
+ self._parse = parse
32
+ kwargs.setdefault("dump_only", True)
33
+ super().__init__(**kwargs)
34
+
35
+ @property
36
+ def locale(self):
37
+ """Get the locale to use."""
38
+ return self._locale() if callable(self._locale) else self._locale
39
+
40
+ def parse(self, value, as_time=False, as_date=False, as_datetime=False):
41
+ """Parse the value if it's a string."""
42
+ if not self._parse or not isinstance(value, str):
43
+ return value
44
+
45
+ a = arrow.get(value)
46
+ if as_time:
47
+ return a.datetime
48
+ elif as_date:
49
+ return a.date()
50
+ elif as_datetime:
51
+ return a.datetime
52
+ else:
53
+ return a
54
+
55
+ def format_value(self, value):
56
+ """Format a given value using the chosen format function."""
57
+ raise NotImplementedError()
58
+
59
+ def _serialize(self, value, attr, data, **kwargs):
60
+ """Serialize the value."""
61
+ return super()._serialize(self.format_value(value), attr, data, **kwargs)
62
+
63
+
64
+ class FormatDate(BabelFormatField):
65
+ """Format a date object."""
66
+
67
+ def format_value(self, value):
68
+ """Format an EDTF date."""
69
+ return format_date(
70
+ self.parse(value, as_date=True), format=self._format, locale=self.locale
71
+ )
72
+
73
+
74
+ class FormatDatetime(BabelFormatField):
75
+ """Format a datetime object."""
76
+
77
+ def __init__(self, tzinfo=None, **kwargs):
78
+ """Constructor."""
79
+ self._tzinfo = tzinfo
80
+ super().__init__(**kwargs)
81
+
82
+ @property
83
+ def tzinfo(self):
84
+ """Get the timzone to use."""
85
+ return self._tzinfo() if callable(self._tzinfo) else self._tzinfo
86
+
87
+ def format_value(self, value):
88
+ """Format an EDTF date."""
89
+ return format_datetime(
90
+ self.parse(value, as_datetime=True),
91
+ format=self._format,
92
+ tzinfo=self.tzinfo,
93
+ locale=self.locale,
94
+ )
95
+
96
+
97
+ class FormatTime(FormatDatetime):
98
+ """Format a time object."""
99
+
100
+ def format_value(self, value):
101
+ """Format an EDTF date."""
102
+ return format_time(
103
+ self.parse(value, as_time=True),
104
+ format=self._format,
105
+ tzinfo=self.tzinfo,
106
+ locale=self.locale,
107
+ )
108
+
109
+
110
+ class FormatEDTF(BabelFormatField):
111
+ """Format an EDTF-formatted string."""
112
+
113
+ def format_value(self, value):
114
+ """Format an EDTF date."""
115
+ return format_edtf(value, format=self._format, locale=self.locale)
116
+
117
+
118
+ class BabelGettextDictField(fields.String):
119
+ """Translation string field (dump only).
120
+
121
+ This field dumps a translation string as output, by looking up the
122
+ translation in the dictionary provided as input (the message catalog).
123
+
124
+ The lookup is performed via babel's locale negotiation (e.g. en_US will
125
+ also match en).
126
+
127
+ Basically the fields takes a data object like this::
128
+
129
+ {'title': {'en': 'Text', 'da': 'Tekst'}}
130
+
131
+ and dumps this (in case the locale is english)::
132
+
133
+ {'title': 'Text'}
134
+ """
135
+
136
+ default_error_messages = {
137
+ "invalid": "Not a valid dictionary.",
138
+ "missing_locale": "Translation not found for ",
139
+ }
140
+
141
+ def __init__(self, locale, default_locale, **kwargs):
142
+ """Initialize the field.
143
+
144
+ :param locale: The locale to lookup, or a function returning the
145
+ locale.
146
+ :param default_locale: The default locale in case the locale is not
147
+ found. Can be a callable that returns the default locale.
148
+ """
149
+ self._locale = locale
150
+ self._default_locale = default_locale
151
+ kwargs["dump_only"] = True
152
+ super().__init__(**kwargs)
153
+
154
+ @property
155
+ def locale(self):
156
+ """Get the locale to be used."""
157
+ return self._locale() if callable(self._locale) else self._locale
158
+
159
+ @property
160
+ def default_locale(self):
161
+ """Get the default locale to be used."""
162
+ return (
163
+ self._default_locale()
164
+ if callable(self._default_locale)
165
+ else self._default_locale
166
+ )
167
+
168
+ def _serialize(self, value, attr, obj, **kwargs):
169
+ """Serialize the dict into a string.
170
+
171
+ The dict is a message catalog with the keys being locale identifiers
172
+ and the values being the translated string.
173
+ """
174
+ if value is None:
175
+ return None
176
+ if not isinstance(value, dict):
177
+ raise self.make_error("invalid")
178
+ translated_str = gettext_from_dict(value, self.locale, self.default_locale)
179
+ if translated_str is None:
180
+ raise self.make_error("missing_locale")
181
+ return super()._serialize(translated_str, attr, obj, **kwargs)
182
+
183
+
184
+ def gettext_from_dict(catalog, locale, default_locale):
185
+ """Get translation string from a dictionary."""
186
+ # First try with negotiate_locale. Negotiate locale will not properly
187
+ # negotiate e.g "en" when the available locales are "en_GB" and "da", even
188
+ # though "en_GB" could be used.
189
+ selected = negotiate_locale([str(locale)], catalog.keys())
190
+ if selected and selected in catalog:
191
+ return catalog[selected]
192
+
193
+ # In situations where negotiate locale doesn't work, we check if the
194
+ # language itself might be found.
195
+
196
+ # Extract language keys only.
197
+ catalog_langs = {Locale.parse(l).language: l for l in catalog}
198
+ if isinstance(locale, str):
199
+ locale = Locale.parse(locale)
200
+ if locale is not None and locale.language in catalog_langs:
201
+ # If primary language match, use that
202
+ catalog_key = catalog_langs[locale.language]
203
+ if catalog_key in catalog:
204
+ return catalog[catalog_key]
205
+ # If not, use default locale (must be defined it is defined)
206
+ # "en" is set as fallback language.
207
+ out = catalog.get(str(default_locale)) or catalog.get("en")
208
+ if out:
209
+ return out
210
+
211
+ # if all other things didn't worked, we are here, the real and only last
212
+ # option, use the first element in the dictionary. this one here is only
213
+ # used to not break frontend
214
+ return list(catalog.values())[0]
@@ -0,0 +1,126 @@
1
+ # SPDX-FileCopyrightText: 2018-2019 CERN.
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """Contributed Marshmallow fields."""
5
+
6
+ import functools
7
+ from inspect import getfullargspec, isfunction, ismethod
8
+
9
+ from marshmallow import fields, utils
10
+
11
+
12
+ def _get_func_args(func):
13
+ """Get a list of the arguments a function or method has."""
14
+ if isinstance(func, functools.partial):
15
+ return _get_func_args(func.func)
16
+ if isfunction(func) or ismethod(func):
17
+ return list(getfullargspec(func).args)
18
+ if callable(func):
19
+ return list(getfullargspec(func.__call__).args)
20
+
21
+
22
+ class Function(fields.Function):
23
+ """Enhanced marshmallow Function field.
24
+
25
+ The main difference between the original marshmallow.fields.Function is for
26
+ the ``deserialize`` function, which can now also point to a three-argument
27
+ function, with the third argument being the original data that was passed
28
+ to ``Schema.load``. The following example better demonstrates how this
29
+ works:
30
+
31
+ .. code-block:: python
32
+
33
+ def serialize_foo(obj, context):
34
+ return {'serialize_args': {'obj': obj, 'context': context}}
35
+
36
+ def deserialize_foo(value, context, data):
37
+ return {'deserialize_args': {
38
+ 'value': value, 'context': context, 'data': data}}
39
+
40
+ class FooSchema(marshmallow.Schema):
41
+
42
+ foo = Function(serialize_foo, deserialize_foo)
43
+
44
+ FooSchema().dump({'foo': 42})
45
+ {'foo': {
46
+ 'serialize_args': {
47
+ 'obj': {'foo': 42},
48
+ 'context': {} # no context was passed
49
+ }
50
+ }}
51
+
52
+ FooSchema().load({'foo': 42}).data
53
+ {'foo': {
54
+ 'deserialize_args': {
55
+ 'value': 42,
56
+ 'context': {}, # no context was passed
57
+ 'data': {'foo': 42},
58
+ }
59
+ }}
60
+ """
61
+
62
+ def _deserialize(self, value, attr, data, **kwargs):
63
+ if self.deserialize_func:
64
+ return self._call_or_raise(self.deserialize_func, value, attr, data)
65
+ return value
66
+
67
+ def _call_or_raise(self, func, value, attr, data=None):
68
+ func_args_len = len(_get_func_args(func))
69
+ if func_args_len > 2:
70
+ return func(value, self.parent.context, data)
71
+ elif func_args_len > 1:
72
+ return func(value, self.parent.context)
73
+ else:
74
+ return func(value)
75
+
76
+
77
+ class Method(fields.Method):
78
+ """Enhanced marshmallow Method field.
79
+
80
+ The main difference between the original marshmallow.fields.Method is for
81
+ the ``deserialize`` method, which can now also point to a two-argument
82
+ method, with the second argument being the original data that was passed to
83
+ ``Schema.load``. The following example better demonstrates how this works:
84
+
85
+ .. code-block:: python
86
+
87
+ class BarSchema(marshmallow.Schema):
88
+
89
+ bar = Method('serialize_bar', 'deserialize_bar')
90
+
91
+ # Exactly the same behavior as in ``marshmallow.fields.Method``
92
+ def serialize_bar(self, obj):
93
+ return {'serialize_args': {'obj': obj}}
94
+
95
+ def deserialize_bar(self, value, data):
96
+ return {'deserialize_args': {'value': value, 'data': data}}
97
+
98
+ BarSchema().dump({'bar': 42})
99
+ {'bar': {
100
+ 'serialize_args': {
101
+ 'obj': {'bar': 42}
102
+ }
103
+ }}
104
+
105
+ BarSchema().load({'bar': 42})
106
+ {'bar': {
107
+ 'deserialize_args': {
108
+ 'data': {'bar': 42},
109
+ 'value': 42
110
+ }
111
+ }}
112
+ """
113
+
114
+ def _deserialize(self, value, attr, data, **kwargs):
115
+ if self.deserialize_method_name:
116
+ try:
117
+ method = utils.callable_or_raise(
118
+ getattr(self.parent, self.deserialize_method_name, None)
119
+ )
120
+ method_args_len = len(_get_func_args(method))
121
+ if method_args_len > 2:
122
+ return method(value, data)
123
+ return method(value)
124
+ except AttributeError:
125
+ pass
126
+ return value
@@ -0,0 +1,114 @@
1
+ # SPDX-FileCopyrightText: 2016-2020 CERN.
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """Extended Date(/Time) Format Level 0 date string field."""
5
+
6
+ from babel_edtf import parse_edtf
7
+ from edtf import (
8
+ Date,
9
+ DateAndTime,
10
+ Interval,
11
+ Level1Interval,
12
+ Level2Interval,
13
+ OneOfASet,
14
+ UncertainOrApproximate,
15
+ )
16
+ from edtf.parser.grammar import ParseException
17
+ from marshmallow import ValidationError, fields
18
+ from marshmallow.validate import Validator
19
+
20
+
21
+ class EDTFValidator(Validator):
22
+ """EDTF validator."""
23
+
24
+ default_message = "Please provide a valid date or interval."
25
+
26
+ def __init__(
27
+ self,
28
+ types=[Date, DateAndTime, Interval],
29
+ chronological_interval=True,
30
+ error=None,
31
+ ):
32
+ """Constructor.
33
+
34
+ :params types: List of EDTFObject subclasses that you accept. Use
35
+ EDTFObject to accept all levels.
36
+ """
37
+ self._types = types or []
38
+ self._chronological_interval = chronological_interval
39
+ self._error = error or self.default_message
40
+
41
+ def _format_error(self, value, e):
42
+ return self._error.format(input=value, edtf=e)
43
+
44
+ def __call__(self, value):
45
+ """Validate."""
46
+ try:
47
+ e = parse_edtf(value)
48
+ except ParseException:
49
+ raise ValidationError(self._format_error(value, None))
50
+
51
+ if self._types:
52
+ if not any([isinstance(e, t) for t in self._types]):
53
+ raise ValidationError(self._format_error(value, e))
54
+
55
+ if self._chronological_interval:
56
+ # We require intervals to be chronological. EDTF Date and Interval
57
+ # both have same interface and
58
+ # date.lower_strict() <= date.upper_strict() is always True for a
59
+ # Date
60
+ if e.upper_strict() < e.lower_strict():
61
+ raise ValidationError(self._format_error(value, e))
62
+
63
+ return value
64
+
65
+
66
+ class EDTFDateString(fields.Str):
67
+ """
68
+ Extended Date Format Level 0 date string field.
69
+
70
+ A string field which is using the EDTF Validator.
71
+ """
72
+
73
+ def __init__(self, **kwargs):
74
+ """Constructor."""
75
+ kwargs.setdefault("validate", EDTFValidator(types=[Date, Interval]))
76
+ super().__init__(**kwargs)
77
+
78
+
79
+ class EDTFDateTimeString(fields.Str):
80
+ """
81
+ Extended Date(/Time) Format Level 0 date string field.
82
+
83
+ A string field which is using the EDTF Validator.
84
+ """
85
+
86
+ def __init__(self, **kwargs):
87
+ """Constructor."""
88
+ kwargs.setdefault("validate", EDTFValidator())
89
+ super().__init__(**kwargs)
90
+
91
+
92
+ class EDTFLevel2DateString(fields.Str):
93
+ """
94
+ Extended Date Format Level 2 date string field.
95
+
96
+ A string field which is using the EDTF Validator.
97
+ """
98
+
99
+ def __init__(self, **kwargs):
100
+ """Constructor."""
101
+ kwargs.setdefault(
102
+ "validate",
103
+ EDTFValidator(
104
+ types=[
105
+ Date,
106
+ Interval,
107
+ Level1Interval,
108
+ UncertainOrApproximate,
109
+ OneOfASet,
110
+ Level2Interval,
111
+ ]
112
+ ),
113
+ )
114
+ super().__init__(**kwargs)
@@ -0,0 +1,39 @@
1
+ # SPDX-FileCopyrightText: 2016-2020 CERN.
2
+ # SPDX-FileCopyrightText: 2025 Graz University of Technology.
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ """Generated field."""
6
+
7
+ from .contrib import Function, Method
8
+
9
+
10
+ class GeneratedValue(object):
11
+ """Sentinel value class forcing marshmallow missing field generation."""
12
+
13
+ pass
14
+
15
+
16
+ class ForcedFieldDeserializeMixin(object):
17
+ """Mixin that forces deserialization of marshmallow fields."""
18
+
19
+ # Overriding default deserializer since we need to deserialize an
20
+ # initially non-existent field. In this implementation the checks are
21
+ # removed since we expect our deserializer to provide the value.
22
+ def deserialize(self, *args, **kwargs):
23
+ """Deserialize field."""
24
+ # Proceed with _deserialization, skipping all checks.
25
+ output = self._deserialize(*args, **kwargs)
26
+ self._validate(output)
27
+ return output
28
+
29
+
30
+ class GenFunction(ForcedFieldDeserializeMixin, Function):
31
+ """Function field which is always deserialized."""
32
+
33
+ pass
34
+
35
+
36
+ class GenMethod(ForcedFieldDeserializeMixin, Method):
37
+ """Method field which is always deserialized."""
38
+
39
+ pass
@@ -0,0 +1,42 @@
1
+ # SPDX-FileCopyrightText: 2021-2022 CERN.
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """Identifier field."""
5
+
6
+ from marshmallow.fields import List
7
+
8
+
9
+ class IdentifierSet(List):
10
+ """Identifier list with deduplication.
11
+
12
+ It assumes the items of the list contain a *scheme* property.
13
+ """
14
+
15
+ default_error_messages = {
16
+ "multiple_values": "Only one identifier per scheme is allowed.",
17
+ }
18
+
19
+ def _validate(self, value):
20
+ """Validates the list of identifiers."""
21
+ schemes = [identifier["scheme"] for identifier in value]
22
+ if len(value) != len(set(schemes)):
23
+ raise self.make_error(key="multiple_values")
24
+
25
+
26
+ class IdentifierValueSet(List):
27
+ """Identifier list with deduplication.
28
+
29
+ It assumes the items of the list contain a *scheme* property.
30
+ """
31
+
32
+ default_error_messages = {
33
+ "multiple_values": "Duplicated identifier entry is not allowed.",
34
+ }
35
+
36
+ def _validate(self, value):
37
+ """Validates the list of identifiers."""
38
+ identifiers = [
39
+ (identifier["scheme"], identifier["identifier"]) for identifier in value
40
+ ]
41
+ if len(value) != len(set(identifiers)):
42
+ raise self.make_error(key="multiple_values")
@@ -0,0 +1,34 @@
1
+ # SPDX-FileCopyrightText: 2016-2021 CERN.
2
+ # SPDX-FileCopyrightText: 2021 Northwestern University.
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ """Date string field."""
6
+
7
+ import arrow
8
+ from arrow.parser import ParserError
9
+ from marshmallow import fields, missing
10
+
11
+
12
+ class ISODateString(fields.Date):
13
+ """ISO8601-formatted date string.
14
+
15
+ ISODateString serializes to a date string and if it can't, the field is
16
+ ignored (missing).
17
+
18
+ NOTE: It serializes None to None.
19
+ """
20
+
21
+ def _serialize(self, value, attr, obj, **kwargs):
22
+ """Serialize an ISO8601-formatted date."""
23
+ # WHY: arrow.get(None) returns a date but we don't want it to
24
+ if value is None:
25
+ return missing
26
+
27
+ try:
28
+ return super()._serialize(arrow.get(value).date(), attr, obj, **kwargs)
29
+ except ParserError:
30
+ return missing
31
+
32
+ def _deserialize(self, value, attr, data, **kwargs):
33
+ """Deserialize an ISO8601-formatted date."""
34
+ return super()._deserialize(value, attr, data, **kwargs).isoformat()