flask-marshmallow 1.0.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.
@@ -0,0 +1,126 @@
1
+ """
2
+ flask_marshmallow
3
+ ~~~~~~~~~~~~~~~~~
4
+
5
+ Integrates the marshmallow serialization/deserialization library
6
+ with your Flask application.
7
+ """
8
+ import warnings
9
+
10
+ from marshmallow import exceptions, pprint
11
+ from marshmallow import fields as base_fields
12
+
13
+ from . import fields
14
+ from .schema import Schema
15
+
16
+ has_sqla = False
17
+ try:
18
+ import flask_sqlalchemy # noqa: F401
19
+ except ImportError:
20
+ has_sqla = False
21
+ else:
22
+ try:
23
+ from . import sqla
24
+ except ImportError:
25
+ warnings.warn(
26
+ "Flask-SQLAlchemy integration requires "
27
+ "marshmallow-sqlalchemy to be installed.",
28
+ stacklevel=2,
29
+ )
30
+ else:
31
+ has_sqla = True
32
+
33
+ __all__ = [
34
+ "EXTENSION_NAME",
35
+ "Marshmallow",
36
+ "Schema",
37
+ "fields",
38
+ "exceptions",
39
+ "pprint",
40
+ ]
41
+
42
+ EXTENSION_NAME = "flask-marshmallow"
43
+
44
+
45
+ def _attach_fields(obj):
46
+ """Attach all the marshmallow fields classes to ``obj``, including
47
+ Flask-Marshmallow's custom fields.
48
+ """
49
+ for attr in base_fields.__all__:
50
+ if not hasattr(obj, attr):
51
+ setattr(obj, attr, getattr(base_fields, attr))
52
+ for attr in fields.__all__:
53
+ setattr(obj, attr, getattr(fields, attr))
54
+
55
+
56
+ class Marshmallow:
57
+ """Wrapper class that integrates Marshmallow with a Flask application.
58
+
59
+ To use it, instantiate with an application::
60
+
61
+ from flask import Flask
62
+
63
+ app = Flask(__name__)
64
+ ma = Marshmallow(app)
65
+
66
+ The object provides access to the :class:`Schema` class,
67
+ all fields in :mod:`marshmallow.fields`, as well as the Flask-specific
68
+ fields in :mod:`flask_marshmallow.fields`.
69
+
70
+ You can declare schema like so::
71
+
72
+ class BookSchema(ma.Schema):
73
+ class Meta:
74
+ fields = ('id', 'title', 'author', 'links')
75
+
76
+ author = ma.Nested(AuthorSchema)
77
+
78
+ links = ma.Hyperlinks({
79
+ 'self': ma.URLFor('book_detail', values=dict(id='<id>')),
80
+ 'collection': ma.URLFor('book_list')
81
+ })
82
+
83
+
84
+ In order to integrate with Flask-SQLAlchemy, this extension must be initialized
85
+ *after* `flask_sqlalchemy.SQLAlchemy`. ::
86
+
87
+ db = SQLAlchemy(app)
88
+ ma = Marshmallow(app)
89
+
90
+ This gives you access to `ma.SQLAlchemySchema` and `ma.SQLAlchemyAutoSchema`, which
91
+ generate marshmallow `~marshmallow.Schema` classes
92
+ based on the passed in model or table. ::
93
+
94
+ class AuthorSchema(ma.SQLAlchemyAutoSchema):
95
+ class Meta:
96
+ model = Author
97
+
98
+ :param Flask app: The Flask application object.
99
+ """
100
+
101
+ def __init__(self, app=None):
102
+ self.Schema = Schema
103
+ if has_sqla:
104
+ self.SQLAlchemySchema = sqla.SQLAlchemySchema
105
+ self.SQLAlchemyAutoSchema = sqla.SQLAlchemyAutoSchema
106
+ self.auto_field = sqla.auto_field
107
+ self.HyperlinkRelated = sqla.HyperlinkRelated
108
+ _attach_fields(self)
109
+ if app is not None:
110
+ self.init_app(app)
111
+
112
+ def init_app(self, app):
113
+ """Initializes the application with the extension.
114
+
115
+ :param Flask app: The Flask application object.
116
+ """
117
+ app.extensions = getattr(app, "extensions", {})
118
+
119
+ # If using Flask-SQLAlchemy, attach db.session to SQLAlchemySchema
120
+ if has_sqla and "sqlalchemy" in app.extensions:
121
+ db = app.extensions["sqlalchemy"]
122
+ if self.SQLAlchemySchema:
123
+ self.SQLAlchemySchema.OPTIONS_CLASS.session = db.session
124
+ if self.SQLAlchemyAutoSchema:
125
+ self.SQLAlchemyAutoSchema.OPTIONS_CLASS.session = db.session
126
+ app.extensions[EXTENSION_NAME] = self
@@ -0,0 +1,247 @@
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
+
12
+ from flask import current_app, url_for
13
+ from marshmallow import fields, missing
14
+
15
+ __all__ = [
16
+ "URLFor",
17
+ "UrlFor",
18
+ "AbsoluteURLFor",
19
+ "AbsoluteUrlFor",
20
+ "Hyperlinks",
21
+ "File",
22
+ "Config",
23
+ ]
24
+
25
+
26
+ _tpl_pattern = re.compile(r"\s*<\s*(\S*)\s*>\s*")
27
+
28
+
29
+ def _tpl(val):
30
+ """Return value within ``< >`` if possible, else return ``None``."""
31
+ match = _tpl_pattern.match(val)
32
+ if match:
33
+ return match.groups()[0]
34
+ return None
35
+
36
+
37
+ def _get_value(obj, key, default=missing):
38
+ """Slightly-modified version of marshmallow.utils.get_value.
39
+ If a dot-delimited ``key`` is passed and any attribute in the
40
+ path is `None`, return `None`.
41
+ """
42
+ if "." in key:
43
+ return _get_value_for_keys(obj, key.split("."), default)
44
+ else:
45
+ return _get_value_for_key(obj, key, default)
46
+
47
+
48
+ def _get_value_for_keys(obj, keys, default):
49
+ if len(keys) == 1:
50
+ return _get_value_for_key(obj, keys[0], default)
51
+ else:
52
+ value = _get_value_for_key(obj, keys[0], default)
53
+ # XXX This differs from the marshmallow implementation
54
+ if value is None:
55
+ return None
56
+ return _get_value_for_keys(value, keys[1:], default)
57
+
58
+
59
+ def _get_value_for_key(obj, key, default):
60
+ if not hasattr(obj, "__getitem__"):
61
+ return getattr(obj, key, default)
62
+
63
+ try:
64
+ return obj[key]
65
+ except (KeyError, IndexError, TypeError, AttributeError):
66
+ return getattr(obj, key, default)
67
+
68
+
69
+ class URLFor(fields.Field):
70
+ """Field that outputs the URL for an endpoint. Acts identically to
71
+ Flask's ``url_for`` function, except that arguments can be pulled from the
72
+ object to be serialized, and ``**values`` should be passed to the ``values``
73
+ parameter.
74
+
75
+ Usage: ::
76
+
77
+ url = URLFor('author_get', values=dict(id='<id>'))
78
+ https_url = URLFor(
79
+ 'author_get',
80
+ values=dict(id='<id>', _scheme='https', _external=True),
81
+ )
82
+
83
+ :param str endpoint: Flask endpoint name.
84
+ :param dict values: Same keyword arguments as Flask's url_for, except string
85
+ arguments enclosed in `< >` will be interpreted as attributes to pull
86
+ from the object.
87
+ :param kwargs: keyword arguments to pass to marshmallow field (e.g. ``required``).
88
+ """
89
+
90
+ _CHECK_ATTRIBUTE = False
91
+
92
+ def __init__(self, endpoint, values=None, id=None, **kwargs):
93
+ self.endpoint = endpoint
94
+ self.values = values or {}
95
+ fields.Field.__init__(self, **kwargs)
96
+
97
+ def _serialize(self, value, key, obj):
98
+ """Output the URL for the endpoint, given the kwargs passed to
99
+ ``__init__``.
100
+ """
101
+ param_values = {}
102
+ for name, attr_tpl in self.values.items():
103
+ attr_name = _tpl(str(attr_tpl))
104
+ if attr_name:
105
+ attribute_value = _get_value(obj, attr_name, default=missing)
106
+ if attribute_value is None:
107
+ return None
108
+ if attribute_value is not missing:
109
+ param_values[name] = attribute_value
110
+ else:
111
+ raise AttributeError(
112
+ f"{attr_name!r} is not a valid " f"attribute of {obj!r}"
113
+ )
114
+ else:
115
+ param_values[name] = attr_tpl
116
+ return url_for(self.endpoint, **param_values)
117
+
118
+
119
+ UrlFor = URLFor
120
+
121
+
122
+ class AbsoluteURLFor(URLFor):
123
+ """Field that outputs the absolute URL for an endpoint."""
124
+
125
+ def __init__(self, endpoint, values=None, **kwargs):
126
+ if values:
127
+ values["_external"] = True
128
+ else:
129
+ values = {"_external": True}
130
+ URLFor.__init__(self, endpoint=endpoint, values=values, **kwargs)
131
+
132
+
133
+ AbsoluteUrlFor = AbsoluteURLFor
134
+
135
+
136
+ def _rapply(d, func, *args, **kwargs):
137
+ """Apply a function to all values in a dictionary or
138
+ list of dictionaries, recursively.
139
+ """
140
+ if isinstance(d, (tuple, list)):
141
+ return [_rapply(each, func, *args, **kwargs) for each in d]
142
+ if isinstance(d, dict):
143
+ return {key: _rapply(value, func, *args, **kwargs) for key, value in d.items()}
144
+ else:
145
+ return func(d, *args, **kwargs)
146
+
147
+
148
+ def _url_val(val, key, obj, **kwargs):
149
+ """Function applied by `HyperlinksField` to get the correct value in the
150
+ schema.
151
+ """
152
+ if isinstance(val, URLFor):
153
+ return val.serialize(key, obj, **kwargs)
154
+ else:
155
+ return val
156
+
157
+
158
+ class Hyperlinks(fields.Field):
159
+ """Field that outputs a dictionary of hyperlinks,
160
+ given a dictionary schema with :class:`~flask_marshmallow.fields.URLFor`
161
+ objects as values.
162
+
163
+ Example: ::
164
+
165
+ _links = Hyperlinks({
166
+ 'self': URLFor('author', values=dict(id='<id>')),
167
+ 'collection': URLFor('author_list'),
168
+ })
169
+
170
+ `URLFor` objects can be nested within the dictionary. ::
171
+
172
+ _links = Hyperlinks({
173
+ 'self': {
174
+ 'href': URLFor('book', values=dict(id='<id>')),
175
+ 'title': 'book detail'
176
+ }
177
+ })
178
+
179
+ :param dict schema: A dict that maps names to
180
+ :class:`~flask_marshmallow.fields.URLFor` fields.
181
+ """
182
+
183
+ _CHECK_ATTRIBUTE = False
184
+
185
+ def __init__(self, schema, **kwargs):
186
+ self.schema = schema
187
+ fields.Field.__init__(self, **kwargs)
188
+
189
+ def _serialize(self, value, attr, obj):
190
+ return _rapply(self.schema, _url_val, key=attr, obj=obj)
191
+
192
+
193
+ class File(fields.Field):
194
+ """A binary file field for uploaded files.
195
+
196
+ Examples: ::
197
+
198
+ class ImageSchema(Schema):
199
+ image = File(required=True)
200
+ """
201
+
202
+ def __init__(self, *args, **kwargs):
203
+ super().__init__(*args, **kwargs)
204
+ # Metadata used by apispec
205
+ self.metadata["type"] = "string"
206
+ self.metadata["format"] = "binary"
207
+
208
+ default_error_messages = {"invalid": "Not a valid file."}
209
+
210
+ def _deserialize(self, value, attr, data, **kwargs):
211
+ from werkzeug.datastructures import FileStorage
212
+
213
+ if not isinstance(value, FileStorage):
214
+ raise self.make_error("invalid")
215
+ return value
216
+
217
+
218
+ class Config(fields.Field):
219
+ """A field for Flask configuration values.
220
+
221
+ Examples: ::
222
+
223
+ from flask import Flask
224
+
225
+ app = Flask(__name__)
226
+ app.config['API_TITLE'] = 'Pet API'
227
+
228
+ class FooSchema(Schema):
229
+ user = String()
230
+ title = Config('API_TITLE')
231
+
232
+ This field should only be used in an output schema. A ``ValueError`` will
233
+ be raised if the config key is not found in the app config.
234
+
235
+ :param str key: The key of the configuration value.
236
+ """
237
+
238
+ _CHECK_ATTRIBUTE = False
239
+
240
+ def __init__(self, key: str, **kwargs):
241
+ fields.Field.__init__(self, **kwargs)
242
+ self.key = key
243
+
244
+ def _serialize(self, value, attr, obj, **kwargs):
245
+ if self.key not in current_app.config:
246
+ raise ValueError(f"The key {self.key!r} is not found in the app config.")
247
+ return current_app.config[self.key]
@@ -0,0 +1,36 @@
1
+ import flask
2
+ import marshmallow as ma
3
+
4
+ sentinel = object()
5
+
6
+
7
+ class Schema(ma.Schema):
8
+ """Base serializer with which to define custom serializers.
9
+
10
+ See `marshmallow.Schema` for more details about the `Schema` API.
11
+ """
12
+
13
+ def jsonify(self, obj, many=sentinel, *args, **kwargs):
14
+ """Return a JSON response containing the serialized data.
15
+
16
+
17
+ :param obj: Object to serialize.
18
+ :param bool many: Whether `obj` should be serialized as an instance
19
+ or as a collection. If unset, defaults to the value of the
20
+ `many` attribute on this Schema.
21
+ :param kwargs: Additional keyword arguments passed to `flask.jsonify`.
22
+
23
+ .. versionchanged:: 0.6.0
24
+ Takes the same arguments as `marshmallow.Schema.dump`. Additional
25
+ keyword arguments are passed to `flask.jsonify`.
26
+
27
+ .. versionchanged:: 0.6.3
28
+ The `many` argument for this method defaults to the value of
29
+ the `many` attribute on the Schema. Previously, the `many`
30
+ argument of this method defaulted to False, regardless of the
31
+ value of `Schema.many`.
32
+ """
33
+ if many is sentinel:
34
+ many = self.many
35
+ data = self.dump(obj, many=many)
36
+ return flask.jsonify(data, *args, **kwargs)
@@ -0,0 +1,121 @@
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
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
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__(self, endpoint, url_key="id", external=False, **kwargs):
91
+ super().__init__(**kwargs)
92
+ self.endpoint = endpoint
93
+ self.url_key = url_key
94
+ self.external = external
95
+
96
+ def _serialize(self, value, attr, obj):
97
+ if value is None:
98
+ return None
99
+ key = super()._serialize(value, attr, obj)
100
+ kwargs = {self.url_key: key}
101
+ return url_for(self.endpoint, _external=self.external, **kwargs)
102
+
103
+ def _deserialize(self, value, *args, **kwargs):
104
+ if self.external:
105
+ parsed = parse.urlparse(value)
106
+ value = parsed.path
107
+ endpoint, kwargs = self.adapter.match(value)
108
+ if endpoint != self.endpoint:
109
+ raise ValidationError(
110
+ f'Parsed endpoint "{endpoint}" from URL "{value}"; expected '
111
+ f'"{self.endpoint}"'
112
+ )
113
+ if self.url_key not in kwargs:
114
+ raise ValidationError(
115
+ f'URL pattern "{self.url_key}" not found in {kwargs!r}'
116
+ )
117
+ return super()._deserialize(kwargs[self.url_key], *args, **kwargs)
118
+
119
+ @property
120
+ def adapter(self):
121
+ 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-2020 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,171 @@
1
+ Metadata-Version: 2.1
2
+ Name: flask-marshmallow
3
+ Version: 1.0.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
20
+ Requires-Dist: marshmallow>=3.0.0
21
+ Requires-Dist: packaging>=17.0
22
+ Requires-Dist: flask-marshmallow[tests] ; extra == "dev"
23
+ Requires-Dist: tox ; extra == "dev"
24
+ Requires-Dist: pre-commit~=3.5 ; extra == "dev"
25
+ Requires-Dist: marshmallow-sqlalchemy>=0.19.0 ; extra == "docs"
26
+ Requires-Dist: Sphinx==7.2.6 ; extra == "docs"
27
+ Requires-Dist: sphinx-issues==3.0.1 ; extra == "docs"
28
+ Requires-Dist: flask-sqlalchemy>=3.0.0 ; extra == "sqlalchemy"
29
+ Requires-Dist: marshmallow-sqlalchemy>=0.29.0 ; extra == "sqlalchemy"
30
+ Requires-Dist: flask-marshmallow[sqlalchemy] ; extra == "tests"
31
+ Requires-Dist: pytest ; extra == "tests"
32
+ Project-URL: Funding, https://opencollective.com/marshmallow
33
+ Project-URL: Issues, https://github.com/marshmallow-code/flask-marshmallow/issues
34
+ Provides-Extra: dev
35
+ Provides-Extra: docs
36
+ Provides-Extra: sqlalchemy
37
+ Provides-Extra: tests
38
+
39
+ *****************
40
+ Flask-Marshmallow
41
+ *****************
42
+
43
+ |pypi-package| |build-status| |docs| |marshmallow3|
44
+
45
+ Flask + marshmallow for beautiful APIs
46
+ ======================================
47
+
48
+ 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/>`_.
49
+
50
+ Get it now
51
+ ----------
52
+ ::
53
+
54
+ pip install flask-marshmallow
55
+
56
+
57
+ Create your app.
58
+
59
+ .. code-block:: python
60
+
61
+ from flask import Flask
62
+ from flask_marshmallow import Marshmallow
63
+
64
+ app = Flask(__name__)
65
+ ma = Marshmallow(app)
66
+
67
+ Write your models.
68
+
69
+ .. code-block:: python
70
+
71
+ from your_orm import Model, Column, Integer, String, DateTime
72
+
73
+
74
+ class User(Model):
75
+ email = Column(String)
76
+ password = Column(String)
77
+ date_created = Column(DateTime, auto_now_add=True)
78
+
79
+
80
+ Define your output format with marshmallow.
81
+
82
+ .. code-block:: python
83
+
84
+
85
+ class UserSchema(ma.Schema):
86
+ class Meta:
87
+ # Fields to expose
88
+ fields = ("email", "date_created", "_links")
89
+
90
+ # Smart hyperlinking
91
+ _links = ma.Hyperlinks(
92
+ {
93
+ "self": ma.URLFor("user_detail", values=dict(id="<id>")),
94
+ "collection": ma.URLFor("users"),
95
+ }
96
+ )
97
+
98
+
99
+ user_schema = UserSchema()
100
+ users_schema = UserSchema(many=True)
101
+
102
+
103
+ Output the data in your views.
104
+
105
+ .. code-block:: python
106
+
107
+ @app.route("/api/users/")
108
+ def users():
109
+ all_users = User.all()
110
+ return users_schema.dump(all_users)
111
+
112
+
113
+ @app.route("/api/users/<id>")
114
+ def user_detail(id):
115
+ user = User.get(id)
116
+ return user_schema.dump(user)
117
+
118
+
119
+ # {
120
+ # "email": "fred@queen.com",
121
+ # "date_created": "Fri, 25 Apr 2014 06:02:56 -0000",
122
+ # "_links": {
123
+ # "self": "/api/users/42",
124
+ # "collection": "/api/users/"
125
+ # }
126
+ # }
127
+
128
+
129
+ http://flask-marshmallow.readthedocs.io/
130
+ ========================================
131
+
132
+ Learn More
133
+ ==========
134
+
135
+ To learn more about marshmallow, check out its `docs <http://marshmallow.readthedocs.io/en/latest/>`_.
136
+
137
+
138
+
139
+ Project Links
140
+ =============
141
+
142
+ - Docs: https://flask-marshmallow.readthedocs.io/
143
+ - Changelog: http://flask-marshmallow.readthedocs.io/en/latest/changelog.html
144
+ - PyPI: https://pypi.org/project/flask-marshmallow/
145
+ - Issues: https://github.com/marshmallow-code/flask-marshmallow/issues
146
+
147
+ License
148
+ =======
149
+
150
+ MIT licensed. See the bundled `LICENSE <https://github.com/marshmallow-code/flask-marshmallow/blob/master/LICENSE>`_ file for more details.
151
+
152
+
153
+ .. _Flask: http://flask.pocoo.org
154
+ .. _marshmallow: http://marshmallow.readthedocs.io
155
+
156
+ .. |pypi-package| image:: https://badgen.net/pypi/v/flask-marshmallow
157
+ :target: https://pypi.org/project/flask-marshmallow/
158
+ :alt: Latest version
159
+
160
+ .. |build-status| image:: https://github.com/marshmallow-code/flask-marshmallow/actions/workflows/build-release.yml/badge.svg
161
+ :target: https://github.com/marshmallow-code/flask-marshmallow/actions/workflows/build-release.yml
162
+ :alt: Build status
163
+
164
+ .. |docs| image:: https://readthedocs.org/projects/flask-marshmallow/badge/
165
+ :target: https://flask-marshmallow.readthedocs.io/
166
+ :alt: Documentation
167
+
168
+ .. |marshmallow3| image:: https://badgen.net/badge/marshmallow/3
169
+ :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html
170
+ :alt: marshmallow 3 compatible
171
+
@@ -0,0 +1,9 @@
1
+ flask_marshmallow/__init__.py,sha256=7yBQXtfQBYyg86Q3u6-ZJWCCz5DbqNSyqK5NWSyHWDQ,3633
2
+ flask_marshmallow/fields.py,sha256=-D75nBbuC_yets_z0MuOgCeSGztq_gGJqdI_3KFv66o,7132
3
+ flask_marshmallow/schema.py,sha256=k-qf47BoBWN0-__oUDPr6x-FXD_U2ckVvs4ZxJkh814,1296
4
+ flask_marshmallow/sqla.py,sha256=xe0WmWHYdVr3zC6q3vOJ1p-mnXDV5a1tb2SFd91LrFE,3905
5
+ flask_marshmallow/validate.py,sha256=ysyVujOIT-mM4A6DJyoIQNXJBxYpWmcaQVbpu-JZhV8,6534
6
+ flask_marshmallow-1.0.0.dist-info/LICENSE,sha256=WXPl3MILZeTrS0rN8nO7eKGFIpFKtib0lvGwcPfmKU0,1074
7
+ flask_marshmallow-1.0.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
8
+ flask_marshmallow-1.0.0.dist-info/METADATA,sha256=YzGQGw0FwxqoqIlgftyCYGmuPi2OdEqx-5x9Dj0uAPs,5180
9
+ flask_marshmallow-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any