Skip to content
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

Merged
merged 5 commits into from
Feb 9, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,17 @@ This document describes changes between each past release.

- Remove pypy supports. (#1049)

**Internal changes**

- Permission schema children fields are now set during initialization instead of on
deserialization (#1046).
- Request schemas (including validation and deserialization) are now isolated by method
and endpoint type (#1047).
- Move generic API schemas (e.g TimeStamps and HeaderFields) from `kinto.core.resource.schema`
to a sepate file on `kinto.core.schema`.
Copy link
Contributor

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

@gabisurita gabisurita Feb 1, 2017

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 on kinto.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?

Copy link
Contributor

@leplatrem leplatrem Feb 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know why we have them there?

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.

Copy link
Member Author

@gabisurita gabisurita Feb 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can deprecated them, no worries.

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?

Copy link
Member Author

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.


5.3.2 (unreleased)

5.3.2 (2017-01-31)
------------------

**Bug fixes**
Expand All @@ -30,12 +39,8 @@ This document describes changes between each past release.

- Remove JSON Patch content-type from accepted types on the viewset, since it is handled
in a separate view (#1031).
- Permission schema children fields are now set during initialization instead of on
deserialization (#1046).
- Upgraded to Kinto-Admin 1.8.1
- Configure the Kinto Admin auth methods from the server configuration (#1042)
- Request schemas (including validation and deserialization) are now isolated by method
and endpoint type (#1047).

5.3.0 (2017-01-20)
------------------
Expand Down
114 changes: 2 additions & 112 deletions kinto/core/resource/schema.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import six
import colander
from colander import SchemaNode, String

from kinto.core.utils import strip_whitespace, msec_time, decode_header, native_value
from kinto.core.schema import Any, HeaderField, QueryField, HeaderQuotedInteger, FieldList
from kinto.core.utils import native_value


# Resource related schemas
Expand Down Expand Up @@ -117,115 +116,6 @@ def _get_node_principals(self, perm):
missing=colander.drop)


# Generic schema nodes


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(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 = 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])


# Header schemas


Expand Down
110 changes: 110 additions & 0 deletions kinto/core/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import six
Copy link
Contributor

Choose a reason for hiding this comment

The 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 kinto.core.resource.schema.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see many module-level docstrings on kinto.core (__init__.py is the only exception), that's why I didn't use them, but I agree it would be nice to have this written in the code somewhere.

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])
22 changes: 0 additions & 22 deletions tests/core/resource/test_schema.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,10 @@
import six
import colander
import mock

from kinto.core.testing import unittest
from kinto.core.resource import schema


class TimeStampTest(unittest.TestCase):
@mock.patch('kinto.core.resource.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)


class ResourceSchemaTest(unittest.TestCase):

def test_preserves_unknown_fields_when_specified(self):
Expand Down
26 changes: 26 additions & 0 deletions tests/core/test_schema.py
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)