flask-marshmallow 1.1.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.
- flask_marshmallow/__init__.py +128 -0
- flask_marshmallow/fields.py +260 -0
- flask_marshmallow/py.typed +0 -0
- flask_marshmallow/schema.py +41 -0
- flask_marshmallow/sqla.py +123 -0
- flask_marshmallow/validate.py +183 -0
- flask_marshmallow-1.1.0.dist-info/LICENSE +19 -0
- flask_marshmallow-1.1.0.dist-info/METADATA +170 -0
- flask_marshmallow-1.1.0.dist-info/RECORD +10 -0
- flask_marshmallow-1.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
flask_marshmallow
|
|
3
|
+
~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Integrates the marshmallow serialization/deserialization library
|
|
6
|
+
with your Flask application.
|
|
7
|
+
"""
|
|
8
|
+
import typing
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
from marshmallow import exceptions, pprint
|
|
12
|
+
from marshmallow import fields as base_fields
|
|
13
|
+
|
|
14
|
+
from . import fields
|
|
15
|
+
from .schema import Schema
|
|
16
|
+
|
|
17
|
+
if typing.TYPE_CHECKING:
|
|
18
|
+
from flask import Flask
|
|
19
|
+
|
|
20
|
+
has_sqla = False
|
|
21
|
+
try:
|
|
22
|
+
import flask_sqlalchemy # noqa: F401
|
|
23
|
+
except ImportError:
|
|
24
|
+
has_sqla = False
|
|
25
|
+
else:
|
|
26
|
+
try:
|
|
27
|
+
from . import sqla
|
|
28
|
+
except ImportError:
|
|
29
|
+
warnings.warn(
|
|
30
|
+
"Flask-SQLAlchemy integration requires "
|
|
31
|
+
"marshmallow-sqlalchemy to be installed.",
|
|
32
|
+
stacklevel=2,
|
|
33
|
+
)
|
|
34
|
+
else:
|
|
35
|
+
has_sqla = True
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"EXTENSION_NAME",
|
|
39
|
+
"Marshmallow",
|
|
40
|
+
"Schema",
|
|
41
|
+
"fields",
|
|
42
|
+
"exceptions",
|
|
43
|
+
"pprint",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
EXTENSION_NAME = "flask-marshmallow"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _attach_fields(obj):
|
|
50
|
+
"""Attach all the marshmallow fields classes to ``obj``, including
|
|
51
|
+
Flask-Marshmallow's custom fields.
|
|
52
|
+
"""
|
|
53
|
+
for attr in base_fields.__all__:
|
|
54
|
+
if not hasattr(obj, attr):
|
|
55
|
+
setattr(obj, attr, getattr(base_fields, attr))
|
|
56
|
+
for attr in fields.__all__:
|
|
57
|
+
setattr(obj, attr, getattr(fields, attr))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Marshmallow:
|
|
61
|
+
"""Wrapper class that integrates Marshmallow with a Flask application.
|
|
62
|
+
|
|
63
|
+
To use it, instantiate with an application::
|
|
64
|
+
|
|
65
|
+
from flask import Flask
|
|
66
|
+
|
|
67
|
+
app = Flask(__name__)
|
|
68
|
+
ma = Marshmallow(app)
|
|
69
|
+
|
|
70
|
+
The object provides access to the :class:`Schema` class,
|
|
71
|
+
all fields in :mod:`marshmallow.fields`, as well as the Flask-specific
|
|
72
|
+
fields in :mod:`flask_marshmallow.fields`.
|
|
73
|
+
|
|
74
|
+
You can declare schema like so::
|
|
75
|
+
|
|
76
|
+
class BookSchema(ma.Schema):
|
|
77
|
+
class Meta:
|
|
78
|
+
fields = ('id', 'title', 'author', 'links')
|
|
79
|
+
|
|
80
|
+
author = ma.Nested(AuthorSchema)
|
|
81
|
+
|
|
82
|
+
links = ma.Hyperlinks({
|
|
83
|
+
'self': ma.URLFor('book_detail', values=dict(id='<id>')),
|
|
84
|
+
'collection': ma.URLFor('book_list')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
In order to integrate with Flask-SQLAlchemy, this extension must be initialized
|
|
89
|
+
*after* `flask_sqlalchemy.SQLAlchemy`. ::
|
|
90
|
+
|
|
91
|
+
db = SQLAlchemy(app)
|
|
92
|
+
ma = Marshmallow(app)
|
|
93
|
+
|
|
94
|
+
This gives you access to `ma.SQLAlchemySchema` and `ma.SQLAlchemyAutoSchema`, which
|
|
95
|
+
generate marshmallow `~marshmallow.Schema` classes
|
|
96
|
+
based on the passed in model or table. ::
|
|
97
|
+
|
|
98
|
+
class AuthorSchema(ma.SQLAlchemyAutoSchema):
|
|
99
|
+
class Meta:
|
|
100
|
+
model = Author
|
|
101
|
+
|
|
102
|
+
:param Flask app: The Flask application object.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(self, app: typing.Optional["Flask"] = None):
|
|
106
|
+
self.Schema = Schema
|
|
107
|
+
if has_sqla:
|
|
108
|
+
self.SQLAlchemySchema = sqla.SQLAlchemySchema
|
|
109
|
+
self.SQLAlchemyAutoSchema = sqla.SQLAlchemyAutoSchema
|
|
110
|
+
self.auto_field = sqla.auto_field
|
|
111
|
+
self.HyperlinkRelated = sqla.HyperlinkRelated
|
|
112
|
+
_attach_fields(self)
|
|
113
|
+
if app is not None:
|
|
114
|
+
self.init_app(app)
|
|
115
|
+
|
|
116
|
+
def init_app(self, app: "Flask"):
|
|
117
|
+
"""Initializes the application with the extension.
|
|
118
|
+
|
|
119
|
+
:param Flask app: The Flask application object.
|
|
120
|
+
"""
|
|
121
|
+
app.extensions = getattr(app, "extensions", {})
|
|
122
|
+
|
|
123
|
+
# If using Flask-SQLAlchemy, attach db.session to SQLAlchemySchema
|
|
124
|
+
if has_sqla and "sqlalchemy" in app.extensions:
|
|
125
|
+
db = app.extensions["sqlalchemy"]
|
|
126
|
+
self.SQLAlchemySchema.OPTIONS_CLASS.session = db.session
|
|
127
|
+
self.SQLAlchemyAutoSchema.OPTIONS_CLASS.session = db.session
|
|
128
|
+
app.extensions[EXTENSION_NAME] = self
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""
|
|
2
|
+
flask_marshmallow.fields
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Custom, Flask-specific fields.
|
|
6
|
+
|
|
7
|
+
See the `marshmallow.fields` module for the list of all fields available from the
|
|
8
|
+
marshmallow library.
|
|
9
|
+
"""
|
|
10
|
+
import re
|
|
11
|
+
import typing
|
|
12
|
+
|
|
13
|
+
from flask import current_app, url_for
|
|
14
|
+
from marshmallow import fields, missing
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"URLFor",
|
|
18
|
+
"UrlFor",
|
|
19
|
+
"AbsoluteURLFor",
|
|
20
|
+
"AbsoluteUrlFor",
|
|
21
|
+
"Hyperlinks",
|
|
22
|
+
"File",
|
|
23
|
+
"Config",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_tpl_pattern = re.compile(r"\s*<\s*(\S*)\s*>\s*")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _tpl(val: str) -> typing.Optional[str]:
|
|
31
|
+
"""Return value within ``< >`` if possible, else return ``None``."""
|
|
32
|
+
match = _tpl_pattern.match(val)
|
|
33
|
+
if match:
|
|
34
|
+
return match.groups()[0]
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_value(obj, key, default=missing):
|
|
39
|
+
"""Slightly-modified version of marshmallow.utils.get_value.
|
|
40
|
+
If a dot-delimited ``key`` is passed and any attribute in the
|
|
41
|
+
path is `None`, return `None`.
|
|
42
|
+
"""
|
|
43
|
+
if "." in key:
|
|
44
|
+
return _get_value_for_keys(obj, key.split("."), default)
|
|
45
|
+
else:
|
|
46
|
+
return _get_value_for_key(obj, key, default)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _get_value_for_keys(obj, keys, default):
|
|
50
|
+
if len(keys) == 1:
|
|
51
|
+
return _get_value_for_key(obj, keys[0], default)
|
|
52
|
+
else:
|
|
53
|
+
value = _get_value_for_key(obj, keys[0], default)
|
|
54
|
+
# XXX This differs from the marshmallow implementation
|
|
55
|
+
if value is None:
|
|
56
|
+
return None
|
|
57
|
+
return _get_value_for_keys(value, keys[1:], default)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_value_for_key(obj, key, default):
|
|
61
|
+
if not hasattr(obj, "__getitem__"):
|
|
62
|
+
return getattr(obj, key, default)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
return obj[key]
|
|
66
|
+
except (KeyError, IndexError, TypeError, AttributeError):
|
|
67
|
+
return getattr(obj, key, default)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class URLFor(fields.Field):
|
|
71
|
+
"""Field that outputs the URL for an endpoint. Acts identically to
|
|
72
|
+
Flask's ``url_for`` function, except that arguments can be pulled from the
|
|
73
|
+
object to be serialized, and ``**values`` should be passed to the ``values``
|
|
74
|
+
parameter.
|
|
75
|
+
|
|
76
|
+
Usage: ::
|
|
77
|
+
|
|
78
|
+
url = URLFor('author_get', values=dict(id='<id>'))
|
|
79
|
+
https_url = URLFor(
|
|
80
|
+
'author_get',
|
|
81
|
+
values=dict(id='<id>', _scheme='https', _external=True),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
:param str endpoint: Flask endpoint name.
|
|
85
|
+
:param dict values: Same keyword arguments as Flask's url_for, except string
|
|
86
|
+
arguments enclosed in `< >` will be interpreted as attributes to pull
|
|
87
|
+
from the object.
|
|
88
|
+
:param kwargs: keyword arguments to pass to marshmallow field (e.g. ``required``).
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
_CHECK_ATTRIBUTE = False
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
endpoint: str,
|
|
96
|
+
values: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
97
|
+
**kwargs,
|
|
98
|
+
):
|
|
99
|
+
self.endpoint = endpoint
|
|
100
|
+
self.values = values or {}
|
|
101
|
+
fields.Field.__init__(self, **kwargs)
|
|
102
|
+
|
|
103
|
+
def _serialize(self, value, key, obj):
|
|
104
|
+
"""Output the URL for the endpoint, given the kwargs passed to
|
|
105
|
+
``__init__``.
|
|
106
|
+
"""
|
|
107
|
+
param_values = {}
|
|
108
|
+
for name, attr_tpl in self.values.items():
|
|
109
|
+
attr_name = _tpl(str(attr_tpl))
|
|
110
|
+
if attr_name:
|
|
111
|
+
attribute_value = _get_value(obj, attr_name, default=missing)
|
|
112
|
+
if attribute_value is None:
|
|
113
|
+
return None
|
|
114
|
+
if attribute_value is not missing:
|
|
115
|
+
param_values[name] = attribute_value
|
|
116
|
+
else:
|
|
117
|
+
raise AttributeError(
|
|
118
|
+
f"{attr_name!r} is not a valid " f"attribute of {obj!r}"
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
param_values[name] = attr_tpl
|
|
122
|
+
return url_for(self.endpoint, **param_values)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
UrlFor = URLFor
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class AbsoluteURLFor(URLFor):
|
|
129
|
+
"""Field that outputs the absolute URL for an endpoint."""
|
|
130
|
+
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
endpoint: str,
|
|
134
|
+
values: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
135
|
+
**kwargs,
|
|
136
|
+
):
|
|
137
|
+
if values:
|
|
138
|
+
values["_external"] = True
|
|
139
|
+
else:
|
|
140
|
+
values = {"_external": True}
|
|
141
|
+
URLFor.__init__(self, endpoint=endpoint, values=values, **kwargs)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
AbsoluteUrlFor = AbsoluteURLFor
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _rapply(
|
|
148
|
+
d: typing.Union[dict, typing.Iterable], func: typing.Callable, *args, **kwargs
|
|
149
|
+
):
|
|
150
|
+
"""Apply a function to all values in a dictionary or
|
|
151
|
+
list of dictionaries, recursively.
|
|
152
|
+
"""
|
|
153
|
+
if isinstance(d, (tuple, list)):
|
|
154
|
+
return [_rapply(each, func, *args, **kwargs) for each in d]
|
|
155
|
+
if isinstance(d, dict):
|
|
156
|
+
return {key: _rapply(value, func, *args, **kwargs) for key, value in d.items()}
|
|
157
|
+
else:
|
|
158
|
+
return func(d, *args, **kwargs)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _url_val(val: typing.Any, key: str, obj: typing.Any, **kwargs):
|
|
162
|
+
"""Function applied by `HyperlinksField` to get the correct value in the
|
|
163
|
+
schema.
|
|
164
|
+
"""
|
|
165
|
+
if isinstance(val, URLFor):
|
|
166
|
+
return val.serialize(key, obj, **kwargs)
|
|
167
|
+
else:
|
|
168
|
+
return val
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class Hyperlinks(fields.Field):
|
|
172
|
+
"""Field that outputs a dictionary of hyperlinks,
|
|
173
|
+
given a dictionary schema with :class:`~flask_marshmallow.fields.URLFor`
|
|
174
|
+
objects as values.
|
|
175
|
+
|
|
176
|
+
Example: ::
|
|
177
|
+
|
|
178
|
+
_links = Hyperlinks({
|
|
179
|
+
'self': URLFor('author', values=dict(id='<id>')),
|
|
180
|
+
'collection': URLFor('author_list'),
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
`URLFor` objects can be nested within the dictionary. ::
|
|
184
|
+
|
|
185
|
+
_links = Hyperlinks({
|
|
186
|
+
'self': {
|
|
187
|
+
'href': URLFor('book', values=dict(id='<id>')),
|
|
188
|
+
'title': 'book detail'
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
:param dict schema: A dict that maps names to
|
|
193
|
+
:class:`~flask_marshmallow.fields.URLFor` fields.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
_CHECK_ATTRIBUTE = False
|
|
197
|
+
|
|
198
|
+
def __init__(self, schema: typing.Dict[str, typing.Union[URLFor, str]], **kwargs):
|
|
199
|
+
self.schema = schema
|
|
200
|
+
fields.Field.__init__(self, **kwargs)
|
|
201
|
+
|
|
202
|
+
def _serialize(self, value, attr, obj):
|
|
203
|
+
return _rapply(self.schema, _url_val, key=attr, obj=obj)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class File(fields.Field):
|
|
207
|
+
"""A binary file field for uploaded files.
|
|
208
|
+
|
|
209
|
+
Examples: ::
|
|
210
|
+
|
|
211
|
+
class ImageSchema(Schema):
|
|
212
|
+
image = File(required=True)
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
def __init__(self, *args, **kwargs):
|
|
216
|
+
super().__init__(*args, **kwargs)
|
|
217
|
+
# Metadata used by apispec
|
|
218
|
+
self.metadata["type"] = "string"
|
|
219
|
+
self.metadata["format"] = "binary"
|
|
220
|
+
|
|
221
|
+
default_error_messages = {"invalid": "Not a valid file."}
|
|
222
|
+
|
|
223
|
+
def _deserialize(self, value, attr, data, **kwargs):
|
|
224
|
+
from werkzeug.datastructures import FileStorage
|
|
225
|
+
|
|
226
|
+
if not isinstance(value, FileStorage):
|
|
227
|
+
raise self.make_error("invalid")
|
|
228
|
+
return value
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class Config(fields.Field):
|
|
232
|
+
"""A field for Flask configuration values.
|
|
233
|
+
|
|
234
|
+
Examples: ::
|
|
235
|
+
|
|
236
|
+
from flask import Flask
|
|
237
|
+
|
|
238
|
+
app = Flask(__name__)
|
|
239
|
+
app.config['API_TITLE'] = 'Pet API'
|
|
240
|
+
|
|
241
|
+
class FooSchema(Schema):
|
|
242
|
+
user = String()
|
|
243
|
+
title = Config('API_TITLE')
|
|
244
|
+
|
|
245
|
+
This field should only be used in an output schema. A ``ValueError`` will
|
|
246
|
+
be raised if the config key is not found in the app config.
|
|
247
|
+
|
|
248
|
+
:param str key: The key of the configuration value.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
_CHECK_ATTRIBUTE = False
|
|
252
|
+
|
|
253
|
+
def __init__(self, key: str, **kwargs):
|
|
254
|
+
fields.Field.__init__(self, **kwargs)
|
|
255
|
+
self.key = key
|
|
256
|
+
|
|
257
|
+
def _serialize(self, value, attr, obj, **kwargs):
|
|
258
|
+
if self.key not in current_app.config:
|
|
259
|
+
raise ValueError(f"The key {self.key!r} is not found in the app config.")
|
|
260
|
+
return current_app.config[self.key]
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
import flask
|
|
4
|
+
import marshmallow as ma
|
|
5
|
+
|
|
6
|
+
if typing.TYPE_CHECKING:
|
|
7
|
+
from flask.wrappers import Response
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Schema(ma.Schema):
|
|
11
|
+
"""Base serializer with which to define custom serializers.
|
|
12
|
+
|
|
13
|
+
See `marshmallow.Schema` for more details about the `Schema` API.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def jsonify(
|
|
17
|
+
self, obj: typing.Any, many: typing.Optional[bool] = None, *args, **kwargs
|
|
18
|
+
) -> "Response":
|
|
19
|
+
"""Return a JSON response containing the serialized data.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
:param obj: Object to serialize.
|
|
23
|
+
:param bool many: Whether `obj` should be serialized as an instance
|
|
24
|
+
or as a collection. If None, defaults to the value of the
|
|
25
|
+
`many` attribute on this Schema.
|
|
26
|
+
:param kwargs: Additional keyword arguments passed to `flask.jsonify`.
|
|
27
|
+
|
|
28
|
+
.. versionchanged:: 0.6.0
|
|
29
|
+
Takes the same arguments as `marshmallow.Schema.dump`. Additional
|
|
30
|
+
keyword arguments are passed to `flask.jsonify`.
|
|
31
|
+
|
|
32
|
+
.. versionchanged:: 0.6.3
|
|
33
|
+
The `many` argument for this method defaults to the value of
|
|
34
|
+
the `many` attribute on the Schema. Previously, the `many`
|
|
35
|
+
argument of this method defaulted to False, regardless of the
|
|
36
|
+
value of `Schema.many`.
|
|
37
|
+
"""
|
|
38
|
+
if many is None:
|
|
39
|
+
many = self.many
|
|
40
|
+
data = self.dump(obj, many=many)
|
|
41
|
+
return flask.jsonify(data, *args, **kwargs)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
flask_marshmallow.sqla
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Integration with Flask-SQLAlchemy and marshmallow-sqlalchemy. Provides
|
|
6
|
+
`SQLAlchemySchema <marshmallow_sqlalchemy.SQLAlchemySchema>` and
|
|
7
|
+
`SQLAlchemyAutoSchema <marshmallow_sqlalchemy.SQLAlchemyAutoSchema>` classes
|
|
8
|
+
that use the scoped session from Flask-SQLAlchemy.
|
|
9
|
+
"""
|
|
10
|
+
from urllib import parse
|
|
11
|
+
|
|
12
|
+
import marshmallow_sqlalchemy as msqla
|
|
13
|
+
from flask import current_app, url_for
|
|
14
|
+
from marshmallow.exceptions import ValidationError
|
|
15
|
+
|
|
16
|
+
from .schema import Schema
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DummySession:
|
|
20
|
+
"""Placeholder session object."""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FlaskSQLAlchemyOptsMixin:
|
|
26
|
+
session = DummySession()
|
|
27
|
+
|
|
28
|
+
def __init__(self, meta, **kwargs):
|
|
29
|
+
if not hasattr(meta, "sqla_session"):
|
|
30
|
+
meta.sqla_session = self.session
|
|
31
|
+
super().__init__(meta, **kwargs)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# SQLAlchemySchema and SQLAlchemyAutoSchema are available in newer ma-sqla versions
|
|
35
|
+
if hasattr(msqla, "SQLAlchemySchema"):
|
|
36
|
+
|
|
37
|
+
class SQLAlchemySchemaOpts(FlaskSQLAlchemyOptsMixin, msqla.SQLAlchemySchemaOpts):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
class SQLAlchemySchema(msqla.SQLAlchemySchema, Schema):
|
|
41
|
+
"""SQLAlchemySchema that associates a schema with a model via the
|
|
42
|
+
`model` class Meta option, which should be a
|
|
43
|
+
``db.Model`` class from `flask_sqlalchemy`. Uses the
|
|
44
|
+
scoped session from Flask-SQLAlchemy by default.
|
|
45
|
+
|
|
46
|
+
See `marshmallow_sqlalchemy.SQLAlchemySchema` for more details
|
|
47
|
+
on the `SQLAlchemySchema` API.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
OPTIONS_CLASS = SQLAlchemySchemaOpts
|
|
51
|
+
|
|
52
|
+
else:
|
|
53
|
+
SQLAlchemySchema = None # type: ignore
|
|
54
|
+
|
|
55
|
+
if hasattr(msqla, "SQLAlchemyAutoSchema"):
|
|
56
|
+
|
|
57
|
+
class SQLAlchemyAutoSchemaOpts(
|
|
58
|
+
FlaskSQLAlchemyOptsMixin, msqla.SQLAlchemyAutoSchemaOpts
|
|
59
|
+
):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
class SQLAlchemyAutoSchema(msqla.SQLAlchemyAutoSchema, Schema):
|
|
63
|
+
"""SQLAlchemyAutoSchema that automatically generates marshmallow fields
|
|
64
|
+
from a SQLAlchemy model's or table's column.
|
|
65
|
+
Uses the scoped session from Flask-SQLAlchemy by default.
|
|
66
|
+
|
|
67
|
+
See `marshmallow_sqlalchemy.SQLAlchemyAutoSchema` for more details
|
|
68
|
+
on the `SQLAlchemyAutoSchema` API.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
OPTIONS_CLASS = SQLAlchemyAutoSchemaOpts
|
|
72
|
+
|
|
73
|
+
else:
|
|
74
|
+
SQLAlchemyAutoSchema = None # type: ignore
|
|
75
|
+
|
|
76
|
+
auto_field = getattr(msqla, "auto_field", None)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class HyperlinkRelated(msqla.fields.Related):
|
|
80
|
+
"""Field that generates hyperlinks to indicate references between models,
|
|
81
|
+
rather than primary keys.
|
|
82
|
+
|
|
83
|
+
:param str endpoint: Flask endpoint name for generated hyperlink.
|
|
84
|
+
:param str url_key: The attribute containing the reference's primary
|
|
85
|
+
key. Defaults to "id".
|
|
86
|
+
:param bool external: Set to `True` if absolute URLs should be used,
|
|
87
|
+
instead of relative URLs.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self, endpoint: str, url_key: str = "id", external: bool = False, **kwargs
|
|
92
|
+
):
|
|
93
|
+
super().__init__(**kwargs)
|
|
94
|
+
self.endpoint = endpoint
|
|
95
|
+
self.url_key = url_key
|
|
96
|
+
self.external = external
|
|
97
|
+
|
|
98
|
+
def _serialize(self, value, attr, obj):
|
|
99
|
+
if value is None:
|
|
100
|
+
return None
|
|
101
|
+
key = super()._serialize(value, attr, obj)
|
|
102
|
+
kwargs = {self.url_key: key}
|
|
103
|
+
return url_for(self.endpoint, _external=self.external, **kwargs)
|
|
104
|
+
|
|
105
|
+
def _deserialize(self, value, *args, **kwargs):
|
|
106
|
+
if self.external:
|
|
107
|
+
parsed = parse.urlparse(value)
|
|
108
|
+
value = parsed.path
|
|
109
|
+
endpoint, kwargs = self.adapter.match(value)
|
|
110
|
+
if endpoint != self.endpoint:
|
|
111
|
+
raise ValidationError(
|
|
112
|
+
f'Parsed endpoint "{endpoint}" from URL "{value}"; expected '
|
|
113
|
+
f'"{self.endpoint}"'
|
|
114
|
+
)
|
|
115
|
+
if self.url_key not in kwargs:
|
|
116
|
+
raise ValidationError(
|
|
117
|
+
f'URL pattern "{self.url_key}" not found in {kwargs!r}'
|
|
118
|
+
)
|
|
119
|
+
return super()._deserialize(kwargs[self.url_key], *args, **kwargs)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def adapter(self):
|
|
123
|
+
return current_app.url_map.bind("")
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
flask_marshmallow.validate
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Custom validation classes for various types of data.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import typing
|
|
10
|
+
|
|
11
|
+
from marshmallow.exceptions import ValidationError
|
|
12
|
+
from marshmallow.validate import Validator as Validator
|
|
13
|
+
from werkzeug.datastructures import FileStorage
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_filestorage_size(file):
|
|
17
|
+
"""Return the size of the FileStorage object in bytes."""
|
|
18
|
+
size = len(file.read())
|
|
19
|
+
file.stream.seek(0)
|
|
20
|
+
return size
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# This function is copied from loguru with few modifications.
|
|
24
|
+
# https://github.com/Delgan/loguru/blob/master/loguru/_string_parsers.py#L35
|
|
25
|
+
def _parse_size(size: str) -> float:
|
|
26
|
+
"""Return the value which the ``size`` represents in bytes."""
|
|
27
|
+
size = size.strip()
|
|
28
|
+
reg = re.compile(r"([e\+\-\.\d]+)\s*([kmgtpezy])?(i)?(b)", flags=re.I)
|
|
29
|
+
|
|
30
|
+
match = reg.fullmatch(size)
|
|
31
|
+
|
|
32
|
+
if not match:
|
|
33
|
+
raise ValueError(f"Invalid size value: '{size!r}'")
|
|
34
|
+
|
|
35
|
+
s, u, i, b = match.groups()
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
s = float(s)
|
|
39
|
+
except ValueError as e:
|
|
40
|
+
raise ValueError(f"Invalid float value while parsing size: '{s!r}'") from e
|
|
41
|
+
|
|
42
|
+
u = "kmgtpezy".index(u.lower()) + 1 if u else 0
|
|
43
|
+
i = 1024 if i else 1000
|
|
44
|
+
b = {"b": 8, "B": 1}[b] if b else 1
|
|
45
|
+
return s * i**u / b
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FileSize(Validator):
|
|
49
|
+
"""Validator which succeeds if the file passed to it is within the specified
|
|
50
|
+
size range. If ``min`` is not specified, or is specified as `None`,
|
|
51
|
+
no lower bound exists. If ``max`` is not specified, or is specified as `None`,
|
|
52
|
+
no upper bound exists. The inclusivity of the bounds (if they exist)
|
|
53
|
+
is configurable.
|
|
54
|
+
If ``min_inclusive`` is not specified, or is specified as `True`, then
|
|
55
|
+
the ``min`` bound is included in the range. If ``max_inclusive`` is not specified,
|
|
56
|
+
or is specified as `True`, then the ``max`` bound is included in the range.
|
|
57
|
+
|
|
58
|
+
Example: ::
|
|
59
|
+
|
|
60
|
+
class ImageSchema(Schema):
|
|
61
|
+
image = File(required=True, validate=FileSize(min='1 MiB', max='2 MiB'))
|
|
62
|
+
|
|
63
|
+
:param min: The minimum size (lower bound). If not provided, minimum
|
|
64
|
+
size will not be checked.
|
|
65
|
+
:param max: The maximum size (upper bound). If not provided, maximum
|
|
66
|
+
size will not be checked.
|
|
67
|
+
:param min_inclusive: Whether the ``min`` bound is included in the range.
|
|
68
|
+
:param max_inclusive: Whether the ``max`` bound is included in the range.
|
|
69
|
+
:param error: Error message to raise in case of a validation error.
|
|
70
|
+
Can be interpolated with `{input}`, `{min}` and `{max}`.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
message_min = "Must be {min_op} {{min}}."
|
|
74
|
+
message_max = "Must be {max_op} {{max}}."
|
|
75
|
+
message_all = "Must be {min_op} {{min}} and {max_op} {{max}}."
|
|
76
|
+
|
|
77
|
+
message_gte = "greater than or equal to"
|
|
78
|
+
message_gt = "greater than"
|
|
79
|
+
message_lte = "less than or equal to"
|
|
80
|
+
message_lt = "less than"
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
min: typing.Optional[str] = None,
|
|
85
|
+
max: typing.Optional[str] = None,
|
|
86
|
+
min_inclusive: bool = True,
|
|
87
|
+
max_inclusive: bool = True,
|
|
88
|
+
error: typing.Optional[str] = None,
|
|
89
|
+
):
|
|
90
|
+
self.min = min
|
|
91
|
+
self.max = max
|
|
92
|
+
self.min_size = _parse_size(self.min) if self.min else None
|
|
93
|
+
self.max_size = _parse_size(self.max) if self.max else None
|
|
94
|
+
self.min_inclusive = min_inclusive
|
|
95
|
+
self.max_inclusive = max_inclusive
|
|
96
|
+
self.error = error
|
|
97
|
+
|
|
98
|
+
self.message_min = self.message_min.format(
|
|
99
|
+
min_op=self.message_gte if self.min_inclusive else self.message_gt
|
|
100
|
+
)
|
|
101
|
+
self.message_max = self.message_max.format(
|
|
102
|
+
max_op=self.message_lte if self.max_inclusive else self.message_lt
|
|
103
|
+
)
|
|
104
|
+
self.message_all = self.message_all.format(
|
|
105
|
+
min_op=self.message_gte if self.min_inclusive else self.message_gt,
|
|
106
|
+
max_op=self.message_lte if self.max_inclusive else self.message_lt,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def _repr_args(self):
|
|
110
|
+
return "min={!r}, max={!r}, min_inclusive={!r}, max_inclusive={!r}".format(
|
|
111
|
+
self.min, self.max, self.min_inclusive, self.max_inclusive
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def _format_error(self, value, message):
|
|
115
|
+
return (self.error or message).format(input=value, min=self.min, max=self.max)
|
|
116
|
+
|
|
117
|
+
def __call__(self, value):
|
|
118
|
+
if not isinstance(value, FileStorage):
|
|
119
|
+
raise TypeError(
|
|
120
|
+
f"A FileStorage object is required, not {type(value).__name__!r}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
file_size = _get_filestorage_size(value)
|
|
124
|
+
if self.min_size is not None and (
|
|
125
|
+
file_size < self.min_size
|
|
126
|
+
if self.min_inclusive
|
|
127
|
+
else file_size <= self.min_size
|
|
128
|
+
):
|
|
129
|
+
message = self.message_min if self.max is None else self.message_all
|
|
130
|
+
raise ValidationError(self._format_error(value, message))
|
|
131
|
+
|
|
132
|
+
if self.max_size is not None and (
|
|
133
|
+
file_size > self.max_size
|
|
134
|
+
if self.max_inclusive
|
|
135
|
+
else file_size >= self.max_size
|
|
136
|
+
):
|
|
137
|
+
message = self.message_max if self.min is None else self.message_all
|
|
138
|
+
raise ValidationError(self._format_error(value, message))
|
|
139
|
+
|
|
140
|
+
return value
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class FileType(Validator):
|
|
144
|
+
"""Validator which succeeds if the uploaded file is allowed by a given list
|
|
145
|
+
of extensions.
|
|
146
|
+
|
|
147
|
+
Example: ::
|
|
148
|
+
class ImageSchema(Schema):
|
|
149
|
+
image = File(required=True, validate=FileType(['.png']))
|
|
150
|
+
|
|
151
|
+
:param accept: A sequence of allowed extensions.
|
|
152
|
+
:param error: Error message to raise in case of a validation error.
|
|
153
|
+
Can be interpolated with ``{input}`` and ``{extensions}``.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
default_message = "Not an allowed file type. Allowed file types: [{extensions}]"
|
|
157
|
+
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
accept: typing.Iterable[str],
|
|
161
|
+
error: typing.Optional[str] = None,
|
|
162
|
+
):
|
|
163
|
+
self.allowed_types = {ext.lower() for ext in accept}
|
|
164
|
+
self.error = error or self.default_message
|
|
165
|
+
|
|
166
|
+
def _format_error(self, value):
|
|
167
|
+
return (self.error or self.default_message).format(
|
|
168
|
+
input=value, extensions="".join(self.allowed_types)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def __call__(self, value):
|
|
172
|
+
if not isinstance(value, FileStorage):
|
|
173
|
+
raise TypeError(
|
|
174
|
+
f"A FileStorage object is required, not {type(value).__name__!r}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
_, extension = (
|
|
178
|
+
os.path.splitext(value.filename) if value.filename else (None, None)
|
|
179
|
+
)
|
|
180
|
+
if extension is None or extension.lower() not in self.allowed_types:
|
|
181
|
+
raise ValidationError(self._format_error(value))
|
|
182
|
+
|
|
183
|
+
return value
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright 2014-2024 Steven Loria and contributors
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: flask-marshmallow
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Flask + marshmallow for beautiful APIs
|
|
5
|
+
Maintainer-email: Steven Loria <sloria1@gmail.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/x-rst
|
|
8
|
+
Classifier: Environment :: Web Environment
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
19
|
+
Requires-Dist: Flask>=2.2
|
|
20
|
+
Requires-Dist: marshmallow>=3.0.0
|
|
21
|
+
Requires-Dist: flask-marshmallow[tests] ; extra == "dev"
|
|
22
|
+
Requires-Dist: tox ; extra == "dev"
|
|
23
|
+
Requires-Dist: pre-commit~=3.5 ; extra == "dev"
|
|
24
|
+
Requires-Dist: marshmallow-sqlalchemy>=0.19.0 ; extra == "docs"
|
|
25
|
+
Requires-Dist: Sphinx==7.2.6 ; extra == "docs"
|
|
26
|
+
Requires-Dist: sphinx-issues==3.0.1 ; extra == "docs"
|
|
27
|
+
Requires-Dist: flask-sqlalchemy>=3.0.0 ; extra == "sqlalchemy"
|
|
28
|
+
Requires-Dist: marshmallow-sqlalchemy>=0.29.0 ; extra == "sqlalchemy"
|
|
29
|
+
Requires-Dist: flask-marshmallow[sqlalchemy] ; extra == "tests"
|
|
30
|
+
Requires-Dist: pytest ; extra == "tests"
|
|
31
|
+
Project-URL: Funding, https://opencollective.com/marshmallow
|
|
32
|
+
Project-URL: Issues, https://github.com/marshmallow-code/flask-marshmallow/issues
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Provides-Extra: docs
|
|
35
|
+
Provides-Extra: sqlalchemy
|
|
36
|
+
Provides-Extra: tests
|
|
37
|
+
|
|
38
|
+
*****************
|
|
39
|
+
Flask-Marshmallow
|
|
40
|
+
*****************
|
|
41
|
+
|
|
42
|
+
|pypi-package| |build-status| |docs| |marshmallow3|
|
|
43
|
+
|
|
44
|
+
Flask + marshmallow for beautiful APIs
|
|
45
|
+
======================================
|
|
46
|
+
|
|
47
|
+
Flask-Marshmallow is a thin integration layer for `Flask`_ (a Python web framework) and `marshmallow`_ (an object serialization/deserialization library) that adds additional features to marshmallow, including URL and Hyperlinks fields for HATEOAS-ready APIs. It also (optionally) integrates with `Flask-SQLAlchemy <http://flask-sqlalchemy.pocoo.org/>`_.
|
|
48
|
+
|
|
49
|
+
Get it now
|
|
50
|
+
----------
|
|
51
|
+
::
|
|
52
|
+
|
|
53
|
+
pip install flask-marshmallow
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
Create your app.
|
|
57
|
+
|
|
58
|
+
.. code-block:: python
|
|
59
|
+
|
|
60
|
+
from flask import Flask
|
|
61
|
+
from flask_marshmallow import Marshmallow
|
|
62
|
+
|
|
63
|
+
app = Flask(__name__)
|
|
64
|
+
ma = Marshmallow(app)
|
|
65
|
+
|
|
66
|
+
Write your models.
|
|
67
|
+
|
|
68
|
+
.. code-block:: python
|
|
69
|
+
|
|
70
|
+
from your_orm import Model, Column, Integer, String, DateTime
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class User(Model):
|
|
74
|
+
email = Column(String)
|
|
75
|
+
password = Column(String)
|
|
76
|
+
date_created = Column(DateTime, auto_now_add=True)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
Define your output format with marshmallow.
|
|
80
|
+
|
|
81
|
+
.. code-block:: python
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class UserSchema(ma.Schema):
|
|
85
|
+
class Meta:
|
|
86
|
+
# Fields to expose
|
|
87
|
+
fields = ("email", "date_created", "_links")
|
|
88
|
+
|
|
89
|
+
# Smart hyperlinking
|
|
90
|
+
_links = ma.Hyperlinks(
|
|
91
|
+
{
|
|
92
|
+
"self": ma.URLFor("user_detail", values=dict(id="<id>")),
|
|
93
|
+
"collection": ma.URLFor("users"),
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
user_schema = UserSchema()
|
|
99
|
+
users_schema = UserSchema(many=True)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
Output the data in your views.
|
|
103
|
+
|
|
104
|
+
.. code-block:: python
|
|
105
|
+
|
|
106
|
+
@app.route("/api/users/")
|
|
107
|
+
def users():
|
|
108
|
+
all_users = User.all()
|
|
109
|
+
return users_schema.dump(all_users)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@app.route("/api/users/<id>")
|
|
113
|
+
def user_detail(id):
|
|
114
|
+
user = User.get(id)
|
|
115
|
+
return user_schema.dump(user)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# {
|
|
119
|
+
# "email": "fred@queen.com",
|
|
120
|
+
# "date_created": "Fri, 25 Apr 2014 06:02:56 -0000",
|
|
121
|
+
# "_links": {
|
|
122
|
+
# "self": "/api/users/42",
|
|
123
|
+
# "collection": "/api/users/"
|
|
124
|
+
# }
|
|
125
|
+
# }
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
http://flask-marshmallow.readthedocs.io/
|
|
129
|
+
========================================
|
|
130
|
+
|
|
131
|
+
Learn More
|
|
132
|
+
==========
|
|
133
|
+
|
|
134
|
+
To learn more about marshmallow, check out its `docs <http://marshmallow.readthedocs.io/en/latest/>`_.
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
Project Links
|
|
139
|
+
=============
|
|
140
|
+
|
|
141
|
+
- Docs: https://flask-marshmallow.readthedocs.io/
|
|
142
|
+
- Changelog: http://flask-marshmallow.readthedocs.io/en/latest/changelog.html
|
|
143
|
+
- PyPI: https://pypi.org/project/flask-marshmallow/
|
|
144
|
+
- Issues: https://github.com/marshmallow-code/flask-marshmallow/issues
|
|
145
|
+
|
|
146
|
+
License
|
|
147
|
+
=======
|
|
148
|
+
|
|
149
|
+
MIT licensed. See the bundled `LICENSE <https://github.com/marshmallow-code/flask-marshmallow/blob/master/LICENSE>`_ file for more details.
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
.. _Flask: http://flask.pocoo.org
|
|
153
|
+
.. _marshmallow: http://marshmallow.readthedocs.io
|
|
154
|
+
|
|
155
|
+
.. |pypi-package| image:: https://badgen.net/pypi/v/flask-marshmallow
|
|
156
|
+
:target: https://pypi.org/project/flask-marshmallow/
|
|
157
|
+
:alt: Latest version
|
|
158
|
+
|
|
159
|
+
.. |build-status| image:: https://github.com/marshmallow-code/flask-marshmallow/actions/workflows/build-release.yml/badge.svg
|
|
160
|
+
:target: https://github.com/marshmallow-code/flask-marshmallow/actions/workflows/build-release.yml
|
|
161
|
+
:alt: Build status
|
|
162
|
+
|
|
163
|
+
.. |docs| image:: https://readthedocs.org/projects/flask-marshmallow/badge/
|
|
164
|
+
:target: https://flask-marshmallow.readthedocs.io/
|
|
165
|
+
:alt: Documentation
|
|
166
|
+
|
|
167
|
+
.. |marshmallow3| image:: https://badgen.net/badge/marshmallow/3
|
|
168
|
+
:target: https://marshmallow.readthedocs.io/en/latest/upgrading.html
|
|
169
|
+
:alt: marshmallow 3 compatible
|
|
170
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
flask_marshmallow/__init__.py,sha256=2s46vkrP0wX1wXBT0OF9oTUNIaoX5-uu_AogfhGXupg,3650
|
|
2
|
+
flask_marshmallow/fields.py,sha256=0_mk3dKpyDKT66Tp4KoDZ0uNCxmvGVH3Rk4OAwjR8f4,7486
|
|
3
|
+
flask_marshmallow/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
flask_marshmallow/schema.py,sha256=Z6uJVkqi-gq1QfdPbPuy5Fw3UnXGE9vKiD6IJkPOMR4,1412
|
|
5
|
+
flask_marshmallow/sqla.py,sha256=QKEpKDfrBJ5m1KpnRuEB5Txfxql-aN_pyHznYxzNd6Y,3971
|
|
6
|
+
flask_marshmallow/validate.py,sha256=ysyVujOIT-mM4A6DJyoIQNXJBxYpWmcaQVbpu-JZhV8,6534
|
|
7
|
+
flask_marshmallow-1.1.0.dist-info/LICENSE,sha256=H-cSJC-pIkSF8BRLl27SpHZOUglXoFxnuVdBE2NO50c,1074
|
|
8
|
+
flask_marshmallow-1.1.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
|
|
9
|
+
flask_marshmallow-1.1.0.dist-info/METADATA,sha256=S_tnxO-oXSl3sP8iTP52pB6aKk-ssx08KZoPVXf9MC0,5154
|
|
10
|
+
flask_marshmallow-1.1.0.dist-info/RECORD,,
|