marshmallow-utils 0.14.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- marshmallow_utils/__init__.py +46 -0
- marshmallow_utils/context.py +8 -0
- marshmallow_utils/fields/__init__.py +56 -0
- marshmallow_utils/fields/babel.py +214 -0
- marshmallow_utils/fields/contrib.py +126 -0
- marshmallow_utils/fields/edtfdatestring.py +114 -0
- marshmallow_utils/fields/generated.py +39 -0
- marshmallow_utils/fields/identifier.py +42 -0
- marshmallow_utils/fields/isodate.py +34 -0
- marshmallow_utils/fields/isolanguage.py +30 -0
- marshmallow_utils/fields/links.py +85 -0
- marshmallow_utils/fields/nestedattr.py +20 -0
- marshmallow_utils/fields/sanitizedhtml.py +38 -0
- marshmallow_utils/fields/sanitizedunicode.py +22 -0
- marshmallow_utils/fields/strippedhtml.py +21 -0
- marshmallow_utils/fields/trimmed.py +15 -0
- marshmallow_utils/fields/tzdatetime.py +31 -0
- marshmallow_utils/fields/url.py +215 -0
- marshmallow_utils/html.py +163 -0
- marshmallow_utils/links.py +98 -0
- marshmallow_utils/permissions.py +75 -0
- marshmallow_utils/schemas/__init__.py +15 -0
- marshmallow_utils/schemas/geojson.py +90 -0
- marshmallow_utils/schemas/identifier.py +116 -0
- marshmallow_utils/translations/ar/LC_MESSAGES/messages.po +28 -0
- marshmallow_utils/translations/bg/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/ca/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/cs/LC_MESSAGES/messages.po +30 -0
- marshmallow_utils/translations/da/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/da_DK/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/de/LC_MESSAGES/messages.po +30 -0
- marshmallow_utils/translations/el/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/es/LC_MESSAGES/messages.po +28 -0
- marshmallow_utils/translations/et/LC_MESSAGES/messages.po +28 -0
- marshmallow_utils/translations/fa/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/fi/LC_MESSAGES/messages.po +30 -0
- marshmallow_utils/translations/fr/LC_MESSAGES/messages.po +28 -0
- marshmallow_utils/translations/hr/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/hu/LC_MESSAGES/messages.po +29 -0
- marshmallow_utils/translations/it/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/ja/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/ka/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/ko/LC_MESSAGES/messages.po +28 -0
- marshmallow_utils/translations/lt/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/messages.pot +24 -0
- marshmallow_utils/translations/no/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/pl/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/pt/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/ro/LC_MESSAGES/messages.po +29 -0
- marshmallow_utils/translations/ru/LC_MESSAGES/messages.po +29 -0
- marshmallow_utils/translations/sk/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/sv/LC_MESSAGES/messages.po +28 -0
- marshmallow_utils/translations/th/LC_MESSAGES/messages.po +28 -0
- marshmallow_utils/translations/tr/LC_MESSAGES/messages.po +28 -0
- marshmallow_utils/translations/uk/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/translations/zh_CN/LC_MESSAGES/messages.po +28 -0
- marshmallow_utils/translations/zh_TW/LC_MESSAGES/messages.po +24 -0
- marshmallow_utils/validators/__init__.py +8 -0
- marshmallow_utils/validators/oneof.py +50 -0
- marshmallow_utils-0.14.2.dist-info/METADATA +62 -0
- marshmallow_utils-0.14.2.dist-info/RECORD +65 -0
- marshmallow_utils-0.14.2.dist-info/WHEEL +4 -0
- marshmallow_utils-0.14.2.dist-info/entry_points.txt +2 -0
- marshmallow_utils-0.14.2.dist-info/licenses/AUTHORS.rst +12 -0
- marshmallow_utils-0.14.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,46 @@
|
|
|
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-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
r"""Extras and utilities for Marshmallow.
|
|
7
|
+
|
|
8
|
+
Currently, this library contains a couple of extra fields that helps with
|
|
9
|
+
sanitizing data as shown in the following example:
|
|
10
|
+
|
|
11
|
+
>>> from marshmallow_utils import fields
|
|
12
|
+
>>> from marshmallow import Schema
|
|
13
|
+
>>> class MySchema(Schema):
|
|
14
|
+
... trim = fields.TrimmedString()
|
|
15
|
+
... html = fields.SanitizedHTML()
|
|
16
|
+
... text = fields.SanitizedUnicode()
|
|
17
|
+
... isodate = fields.ISODateString()
|
|
18
|
+
...
|
|
19
|
+
>>> data = MySchema().load({
|
|
20
|
+
... 'trim': ' whitespace ',
|
|
21
|
+
... 'html': '<script>evil()</script>',
|
|
22
|
+
... 'text': 'PDF copy/paste\u200b\u000b\u001b\u0018 ',
|
|
23
|
+
... 'isodate': '1999-10-27',
|
|
24
|
+
... })
|
|
25
|
+
>>> data['trim']
|
|
26
|
+
'whitespace'
|
|
27
|
+
>>> data['html']
|
|
28
|
+
'evil()'
|
|
29
|
+
>>> data['text']
|
|
30
|
+
'PDF copy/paste'
|
|
31
|
+
>>> data['isodate']
|
|
32
|
+
'1999-10-27'
|
|
33
|
+
|
|
34
|
+
Fields:
|
|
35
|
+
|
|
36
|
+
- :py:class:`~fields.SanitizedUnicode`: Integrates the
|
|
37
|
+
`ftfy <https://pypi.org/project/ftfy/>`_ for fixing broken unicode text.
|
|
38
|
+
- :py:class:`~fields.SanitizedHTML`: Integrates the
|
|
39
|
+
`bleach <https://pypi.org/project/bleach/>`_ for HTML sanitization.
|
|
40
|
+
- :py:class:`~fields.ISODateString`: Integrates the
|
|
41
|
+
`arrow <https://pypi.org/project/arrow/>`_ for date parsing.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
__version__ = "0.14.2"
|
|
45
|
+
|
|
46
|
+
__all__ = ("__version__",)
|
|
@@ -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()
|