-
Notifications
You must be signed in to change notification settings - Fork 422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Move generic schemas from Resource to kinto.core #1054
Changes from 2 commits
eba6a77
ea52f52
40dd5b6
230d851
e6c3999
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import six | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be nice to have a module-level docstring explaining what's supposed to go in this file vs. what goes in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see many module-level docstrings on |
||
import colander | ||
|
||
from kinto.core.utils import strip_whitespace, msec_time, decode_header, native_value | ||
|
||
|
||
class TimeStamp(colander.SchemaNode): | ||
"""Basic integer schema field that can be set to current server timestamp | ||
in milliseconds if no value is provided. | ||
|
||
.. code-block:: python | ||
|
||
class Book(ResourceSchema): | ||
added_on = TimeStamp() | ||
read_on = TimeStamp(auto_now=False, missing=-1) | ||
""" | ||
schema_type = colander.Integer | ||
|
||
title = 'Epoch timestamp' | ||
"""Default field title.""" | ||
|
||
auto_now = True | ||
"""Set to current server timestamp (*milliseconds*) if not provided.""" | ||
|
||
missing = None | ||
"""Default field value if not provided in record.""" | ||
|
||
def deserialize(self, cstruct=colander.null): | ||
if cstruct is colander.null and self.auto_now: | ||
cstruct = msec_time() | ||
return super(TimeStamp, self).deserialize(cstruct) | ||
|
||
|
||
class URL(colander.SchemaNode): | ||
"""String field representing a URL, with max length of 2048. | ||
This is basically a shortcut for string field with | ||
`~colander:colander.url`. | ||
|
||
.. code-block:: python | ||
|
||
class BookmarkSchema(ResourceSchema): | ||
url = URL() | ||
""" | ||
schema_type = colander.String | ||
validator = colander.All(colander.url, colander.Length(min=1, max=2048)) | ||
|
||
def preparer(self, appstruct): | ||
return strip_whitespace(appstruct) | ||
|
||
|
||
class Any(colander.SchemaType): | ||
"""Colander type agnostic field.""" | ||
|
||
def deserialize(self, node, cstruct): | ||
return cstruct | ||
|
||
|
||
class HeaderField(colander.SchemaNode): | ||
"""Basic header field SchemaNode.""" | ||
|
||
missing = colander.drop | ||
|
||
def deserialize(self, cstruct=colander.null): | ||
if isinstance(cstruct, six.binary_type): | ||
try: | ||
cstruct = decode_header(cstruct) | ||
except UnicodeDecodeError: | ||
raise colander.Invalid(self, msg='Headers should be UTF-8 encoded') | ||
return super(HeaderField, self).deserialize(cstruct) | ||
|
||
|
||
class QueryField(colander.SchemaNode): | ||
"""Basic querystring field SchemaNode.""" | ||
|
||
missing = colander.drop | ||
|
||
def deserialize(self, cstruct=colander.null): | ||
if isinstance(cstruct, six.string_types): | ||
cstruct = native_value(cstruct) | ||
return super(QueryField, self).deserialize(cstruct) | ||
|
||
|
||
class FieldList(QueryField): | ||
"""String field representing a list of attributes.""" | ||
|
||
schema_type = colander.Sequence | ||
error_message = "The value should be a list of comma separated attributes" | ||
missing = colander.drop | ||
fields = colander.SchemaNode(colander.String(), missing=colander.drop) | ||
|
||
def deserialize(self, cstruct=colander.null): | ||
if isinstance(cstruct, six.string_types): | ||
cstruct = cstruct.split(',') | ||
return super(FieldList, self).deserialize(cstruct) | ||
|
||
|
||
class HeaderQuotedInteger(HeaderField): | ||
"""Integer between "" used in precondition headers.""" | ||
|
||
schema_type = colander.String | ||
error_message = "The value should be integer between double quotes" | ||
validator = colander.Any(colander.Regex('^"([0-9]+?)"$', msg=error_message), | ||
colander.Regex('\*')) | ||
|
||
def deserialize(self, cstruct=colander.null): | ||
param = super(HeaderQuotedInteger, self).deserialize(cstruct) | ||
if param is colander.drop or param == '*': | ||
return param | ||
|
||
return int(param[1:-1]) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import colander | ||
import mock | ||
|
||
from kinto.core.testing import unittest | ||
from kinto.core import schema | ||
|
||
|
||
class TimeStampTest(unittest.TestCase): | ||
@mock.patch('kinto.core.schema.msec_time') | ||
def test_default_value_comes_from_timestamper(self, time_mocked): | ||
time_mocked.return_value = 666 | ||
default = schema.TimeStamp().deserialize(colander.null) | ||
self.assertEqual(default, 666) | ||
|
||
|
||
class URLTest(unittest.TestCase): | ||
def test_supports_full_url(self): | ||
url = 'https://user:pass@myserver:9999/feeling.html#anchor' | ||
deserialized = schema.URL().deserialize(url) | ||
self.assertEqual(deserialized, url) | ||
|
||
def test_raises_invalid_if_no_scheme(self): | ||
url = 'myserver/feeling.html#anchor' | ||
self.assertRaises(colander.Invalid, | ||
schema.URL().deserialize, | ||
url) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember we had that before, and we changed... What does the docs say? Will the old location still be accesible? deprecated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, see https://github.com/mozilla-services/cliquet/blob/master/cliquet/schema.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, that's interesting. I agree
ResourceSchema
should be onkinto.core.resource
because it's only related to the resource (and I'm keeping it there), but I'm not so sure about other things like Timestamps or Header fields that could be reused in other parts of the core.Most of these schemas are imported on
kinto.core.resource
so they are still accessible. Only Timestamp and URL aren't, and they aren't used anywhere on Kinto. Do you know why we have them there?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Originally (archeology) they were in core because we thought they were common types of fields. They're just used in third parties or plugins. We can deprecated them, no worries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. I don't have a strong opinion about it. We can keep them too. In case we deprecate or move them, do you think we should add warning messages or we can just make it a breaking change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About the documentation: I couldn't fond mentions to these schemas anywhere. Also in some parts we use a colander String with url validator to implement an url instead of the URL schema.