Skip to content

Commit

Permalink
Merge pull request #170 from Syncano/release-4.2.0
Browse files Browse the repository at this point in the history
Release 4.2.0
  • Loading branch information
opalczynski committed Mar 9, 2016
2 parents 7c76d99 + da8b62e commit ebe4660
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 14 deletions.
2 changes: 1 addition & 1 deletion syncano/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os

__title__ = 'Syncano Python'
__version__ = '4.1.0'
__version__ = '4.2.0'
__author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski"
__credits__ = ["Daniel Kopka",
"Michal Kobus",
Expand Down
5 changes: 4 additions & 1 deletion syncano/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,10 @@ def make_request(self, method_name, path, **kwargs):

# JSON dump can be expensive
if syncano.DEBUG:
debug_params = params.copy()
debug_params.update({'files': [f for f in files]}) # show files in debug info;
formatted_params = json.dumps(
params,
debug_params,
sort_keys=True,
indent=2,
separators=(',', ': ')
Expand Down Expand Up @@ -286,6 +288,7 @@ def get_response_content(self, url, response):
content = response.json()
except ValueError:
content = response.text

if is_server_error(response.status_code):
raise SyncanoRequestError(response.status_code, 'Server error.')

Expand Down
8 changes: 6 additions & 2 deletions syncano/models/archetypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,14 +242,18 @@ def to_native(self):
for field in self._meta.fields:
if not field.read_only and field.has_data:
value = getattr(self, field.name)
if not value and field.blank:
if value is None and field.blank:
continue

if field.mapping:
data[field.mapping] = field.to_native(value)
else:

param_name = getattr(field, 'param_name', field.name)
data[param_name] = field.to_native(value)
if param_name == 'files' and param_name in data:
data[param_name].update(field.to_native(value))
else:
data[param_name] = field.to_native(value)
return data

def get_endpoint_data(self):
Expand Down
2 changes: 1 addition & 1 deletion syncano/models/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ def parse_from_date(self, value):
def to_native(self, value):
if value is None:
return
ret = value.isoformat()
ret = value.strftime(self.FORMAT)
if ret.endswith('+00:00'):
ret = ret[:-6] + 'Z'

Expand Down
14 changes: 14 additions & 0 deletions syncano/models/instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ class Meta:
}
}

def rename(self, new_name):
"""
A method for changing the instance name;
:param new_name: the new name for the instance;
:return: a populated Instance object;
"""
rename_path = self.links.get('rename')
data = {'new_name': new_name}
connection = self._get_connection()
response = connection.request('POST', rename_path, data=data)
self.to_python(response)
return self


class ApiKey(Model):
"""
Expand All @@ -72,6 +85,7 @@ class ApiKey(Model):
description = fields.StringField(required=False)
allow_user_create = fields.BooleanField(required=False, default=False)
ignore_acl = fields.BooleanField(required=False, default=False)
allow_anonymous_read = fields.BooleanField(required=False, default=False)
links = fields.HyperlinkedField(links=LINKS)

class Meta:
Expand Down
78 changes: 71 additions & 7 deletions syncano/models/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def __init__(self):
self._limit = None
self._serialize = True
self._connection = None
self._template = None

def __repr__(self): # pragma: no cover
data = list(self[:REPR_OUTPUT_SIZE + 1])
Expand Down Expand Up @@ -675,6 +676,21 @@ def raw(self):
self._serialize = False
return self

@clone
def template(self, name):
"""
Disables serialization. ``request`` method will return raw text.
Usage::
>>> instances = Instance.please.list().template('test')
>>> instances
u'text'
"""
self._serialize = False
self._template = name
return self

@clone
def using(self, connection):
"""
Expand Down Expand Up @@ -725,6 +741,7 @@ def _clone(self):
manager.name = self.name
manager.model = self.model
manager._connection = self._connection
manager._template = self._template
manager.endpoint = self.endpoint
manager.properties = deepcopy(self.properties)
manager._limit = self._limit
Expand Down Expand Up @@ -755,6 +772,19 @@ def serialize(self, data, model=None):

return model(**properties) if self._serialize else data

def build_request(self, request):
if 'params' not in request and self.query:
request['params'] = self.query

if 'data' not in request and self.data:
request['data'] = self.data

if 'headers' not in request:
request['headers'] = {}

if self._template is not None and 'X-TEMPLATE-RESPONSE' not in request['headers']:
request['headers']['X-TEMPLATE-RESPONSE'] = self._template

def request(self, method=None, path=None, **request):
"""Internal method, which calls Syncano API and returns serialized data."""
meta = self.model._meta
Expand All @@ -768,11 +798,7 @@ def request(self, method=None, path=None, **request):
methods = ', '.join(allowed_methods)
raise SyncanoValueError('Unsupported request method "{0}" allowed are {1}.'.format(method, methods))

if 'params' not in request and self.query:
request['params'] = self.query

if 'data' not in request and self.data:
request['data'] = self.data
self.build_request(request)

try:
response = self.connection.request(method, path, **request)
Expand Down Expand Up @@ -801,7 +827,7 @@ def get_allowed_method(self, *methods):
def iterator(self):
"""Pagination handler"""

response = self.request()
response = self._get_response()
results = 0
while True:
objects = response.get('objects')
Expand All @@ -819,6 +845,9 @@ def iterator(self):

response = self.request(path=next_url)

def _get_response(self):
return self.request()

def _get_instance(self, attrs):
return self.model(**attrs)

Expand Down Expand Up @@ -887,6 +916,10 @@ class for :class:`~syncano.models.base.Object` model.
'eq', 'neq', 'exists', 'in',
]

def __init__(self):
super(ObjectManager, self).__init__()
self._initial_response = None

def serialize(self, data, model=None):
model = model or self.model.get_subclass_model(**self.properties)
return super(ObjectManager, self).serialize(data, model)
Expand All @@ -900,7 +933,7 @@ def count(self):
Object.please.list(instance_name='raptor', class_name='some_class').filter(id__gt=600).count()
Object.please.list(instance_name='raptor', class_name='some_class').count()
Object.please.all(instance_name='raptor', class_name='some_class').count()
:return: The integer with estimated objects count;
:return: The count of the returned objects: count = DataObjects.please.list(...).count();
"""
self.method = 'GET'
self.query.update({
Expand All @@ -910,6 +943,29 @@ def count(self):
response = self.request()
return response['objects_count']

@clone
def with_count(self, page_size=20):
"""
Return the queryset count;
Usage::
Object.please.list(instance_name='raptor', class_name='some_class').filter(id__gt=600).with_count()
Object.please.list(instance_name='raptor', class_name='some_class').with_count(page_size=30)
Object.please.all(instance_name='raptor', class_name='some_class').with_count()
:param page_size: The size of the pagination; Default to 20;
:return: The tuple with objects and the count: objects, count = DataObjects.please.list(...).with_count();
"""
query_data = {
'include_count': True,
'page_size': page_size,
}

self.method = 'GET'
self.query.update(query_data)
response = self.request()
self._initial_response = response
return self, self._initial_response['objects_count']

@clone
def filter(self, **kwargs):
"""
Expand Down Expand Up @@ -962,6 +1018,9 @@ def bulk_create(self, *objects):
"""
return ObjectBulkCreate(objects, self).process()

def _get_response(self):
return self._initial_response or self.request()

def _get_instance(self, attrs):
return self.model.get_subclass_model(**attrs)(**attrs)

Expand Down Expand Up @@ -1035,6 +1094,11 @@ def order_by(self, field):
self.query['order_by'] = field
return self

def _clone(self):
manager = super(ObjectManager, self)._clone()
manager._initial_response = self._initial_response
return manager


class SchemaManager(object):
"""
Expand Down
53 changes: 52 additions & 1 deletion tests/integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import syncano
from syncano.exceptions import SyncanoRequestError, SyncanoValueError
from syncano.models import Class, CodeBox, Instance, Object, Webhook, registry
from syncano.models import ApiKey, Class, CodeBox, Instance, Object, Webhook, registry


class IntegrationTest(unittest.TestCase):
Expand Down Expand Up @@ -109,6 +109,15 @@ def test_delete(self):
with self.assertRaises(self.model.DoesNotExist):
self.model.please.get(name=name)

def test_rename(self):
name = 'i%s' % self.generate_hash()[:10]
new_name = 'icy-snow-jon-von-doe-312'

instance = self.model.please.create(name=name, description='rest_rename')
instance = instance.rename(new_name=new_name)

self.assertEqual(instance.name, new_name)


class ClassIntegrationTest(InstanceMixin, IntegrationTest):
model = Class
Expand Down Expand Up @@ -278,6 +287,30 @@ def test_update(self):

author.delete()

def test_count_and_with_count(self):
author_one = self.model.please.create(
instance_name=self.instance.name, class_name=self.author.name,
first_name='john1', last_name='doe1')

author_two = self.model.please.create(
instance_name=self.instance.name, class_name=self.author.name,
first_name='john2', last_name='doe2')

# just created two authors

count = Object.please.list(instance_name=self.instance.name, class_name=self.author.name).count()
self.assertEqual(count, 2)

objects, count = Object.please.list(instance_name=self.instance.name,
class_name=self.author.name).with_count()

self.assertEqual(count, 2)
for o in objects:
self.assertTrue(isinstance(o, self.model))

author_one.delete()
author_two.delete()


class CodeboxIntegrationTest(InstanceMixin, IntegrationTest):
model = CodeBox
Expand Down Expand Up @@ -430,3 +463,21 @@ def test_custom_codebox_run(self):
trace = webhook.run()
self.assertDictEqual(trace, {'one': 1})
webhook.delete()


class ApiKeyIntegrationTest(InstanceMixin, IntegrationTest):
model = ApiKey

def test_api_key_flags(self):
api_key = self.model.please.create(
allow_user_create=True,
ignore_acl=True,
allow_anonymous_read=True,
instance_name=self.instance.name,
)

reloaded_api_key = self.model.please.get(id=api_key.id, instance_name=self.instance.name)

self.assertTrue(reloaded_api_key.allow_user_create, True)
self.assertTrue(reloaded_api_key.ignore_acl, True)
self.assertTrue(reloaded_api_key.allow_anonymous_read, True)
2 changes: 1 addition & 1 deletion tests/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def test_debug(self, debug_mock, dumps_mock, post_mock):
self.connection.make_request('POST', 'test')
self.assertTrue(dumps_mock.called)
dumps_mock.assert_called_once_with(
{'headers': {'content-type': 'application/json'}, 'timeout': 30, 'verify': False},
{'files': [], 'headers': {'content-type': 'application/json'}, 'timeout': 30, 'verify': False},
sort_keys=True, indent=2, separators=(',', ': '))

@mock.patch('requests.Session.post')
Expand Down
11 changes: 11 additions & 0 deletions tests/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,16 @@ def test_raw(self, clone_mock):
self.manager.raw()
self.assertFalse(self.manager._serialize)

@mock.patch('syncano.models.manager.Manager._clone')
def test_template(self, clone_mock):
clone_mock.return_value = self.manager

self.assertTrue(self.manager._serialize)
self.assertIsNone(self.manager._template)
self.manager.template('test')
self.assertFalse(self.manager._serialize)
self.assertEqual(self.manager._template, 'test')

def test_serialize(self):
model = mock.Mock()
self.manager.model = mock.Mock
Expand Down Expand Up @@ -421,6 +431,7 @@ def test_request(self, connection_mock):
'GET',
u'/v1/instances/',
data={'b': 2},
headers={},
params={'a': 1}
)

Expand Down

0 comments on commit ebe4660

Please sign in to comment.